from ..utils import helpers as _hp
__all__ = ["Rule"]
[docs]
class Rule:
"""Defines an individual mail rule and its necessary attributes
A rule class defines an individual mail rule and includes functions/operations
to modify a rule's behavior and generate parseable formats of a rule.
...
Attributes
----------
name : `str`
Name of the mail rule.
final_rule : `str`
String representing the final formatted rule
emails_list : `list`
List of email addresses (:obj:`str`) that the mail rule applies to
concatenated_emails : `str`
String of email addresses that are concatenated together
rule_header : `str`
Generic string that precedes the unique section of mail rules
rule_footer : `str`
Generic string that appends the unique section of mail rules
"""
def __init__(self, list_of_emails: list = [], rule_defaults: dict = {}, rule_name: str = "Mail Filter") -> None:
"""Initialize a new Rule object
Parameters
----------
list_of_emails : `list`, optional
list of email address to apply rule to, by default `None`
rule_defaults : `dict`, optional
dictionary containing default rule attributes, by default `{}`
rule_name : `str`, optional
name of the mail rule, by default `"Mail Filter"`
"""
self.labels: list = []
"""This is a `list` containing all of the labels that should be applied to this rule"""
self.name: str = rule_name
"""This `str` is the title of the rule"""
self._attribute_order: tuple = ("label", "from", "subject", "hasTheWord", "doesNotHaveTheWord", "shouldNeverSpam", "shouldArchive", "sizeOperator", "sizeUnit")
"""Hard-coded order that the rule attributes should appear in"""
self._possible_attributes: frozenset = frozenset(self._attribute_order)
"""`frozenset` of the valid attributes defined in `self._attribute_order`"""
self.rule_attributes: dict[str, str] = {}
"""This is a `dict` of all of the rule attributes that should be applied"""
for default_attribute_name, default_attribute_value in rule_defaults.items():
self.add_attribute(default_attribute_name, default_attribute_value)
###### CHECK WHETHER RULE RELIES ON SPECIFIC EMAIL ADDRESSES ######
self.emails_list: list = self.flatten_list(list_of_emails)
"""Flattened `list` of emails that will be included in the mail rule"""
self.concatenated_emails: str = self.concatenate(self.emails_list)
"""A `str` of the concatenated email addresses that this rule applies to"""
if list_of_emails != []:
# THIS IS THE CASE WHEN SPECIFIC EMAILS ARE PARSED INTO THE FUNCTION
self.add_attribute("from", self.concatenated_emails)
###### CHECK WHETHER RULE RELIES ON SPECIFIC EMAIL ADDRESSES ######
self.rule_header: str = f"<entry>\n\t<category term='filter'></category>\n\t<title>{self.name}</title>\n\t<content></content>"
"""This is a `str` representing the top section of a mail rule that remains constant"""
self.rule_footer: str = "\n</entry>"
"""This is a `str` representing how each mail rule will end"""
@property
def rule_attributes_xmls(self) -> dict:
"""Converts `dict` of rule attributes into `dict` of attributes where keys are in xml format
Returns
-------
rule_attribute_xmls : `dict`
Dictionary where keys are the attribute and values are the xml representation of the attribute
"""
rule_attributes_xmls = {}
for attribute_name, attribute_value in self.rule_attributes.items():
rule_attributes_xmls[attribute_name] = f"{self.xml_format_rule_attribute(attribute_name, attribute_value)}"
return rule_attributes_xmls
@property
def rule_attributes_xmls_str(self) -> str:
"""Converts :obj:`dict` of rule attributes into ordered `str` for use in final xml
Returns
-------
rule_attribute_xmls_str : `str`
:obj:`str` representing this :obj:`Rule` as an xml
"""
rule_attributes_xmls_str = ""
current_rule_xmls = self.rule_attributes_xmls
for attribute_name in self._attribute_order:
if attribute_name in current_rule_xmls:
rule_attributes_xmls_str += current_rule_xmls[attribute_name]
return rule_attributes_xmls_str
@property
def final_rule_str(self) -> str:
"""This is the final `str` that can be copied and pasted into an xml to define the rule
Returns
-------
str
`str` representing the entire rule in xml format
"""
return self.build_rule()
def _modify_possible_attributes(self, new_attribute: str) -> None:
"""Modify the order of the hard-coded attributes arrays
Parameters
----------
new_attribute : str
This is the attribute to add the hard-coded array
"""
self._attribute_order = self._attribute_order + (new_attribute,)
self._possible_attributes = frozenset(self._attribute_order)
[docs]
def flatten_list(self, list_to_flatten: list) -> list:
"""Converts a list of lists into a single flat list
This function takes a `list` (of potentially nested lists) and recursively
flattens the list so that it is just a single list of elements that are not
of type `list`
Parameters
----------
list_to_flatten : `list`
Input `list` (or list of lists) to be flattened
Returns
-------
list
Returns a final flat list that does not contain any nested lists
"""
if list_to_flatten == []:
return list_to_flatten
if isinstance(list_to_flatten[0], list):
return self.flatten_list(list_to_flatten[0]) + self.flatten_list(list_to_flatten[1:])
return list_to_flatten[:1] + self.flatten_list(list_to_flatten[1:])
[docs]
def concatenate(self, elements_input: list, separator: str = " OR ") -> str:
"""
Given a list of elements `elements`, this returns the concatenation of the elements with a
separator `separator` between them (`" OR "` by default)
Parameters
----------
elements_input : `list`
This is a list of items that should be concatenated together
separator : `str`, default = `" OR "`, optional
This is the string that will be used to separate individual elements, by default `" OR "`
Returns
-------
final_output : `str`
Returns a singular string containing all of the items in `elements` after being
concatenated and separated with the `separator`
"""
final_string = ""
if elements_input == []:
return ""
else:
elements = self.flatten_list(elements_input)
for element in elements:
final_string += f"{element}{separator}"
final_output = final_string[:-len(separator)]
return final_output
[docs]
def add_attribute(self, name: str, value: str, is_custom_attribute: bool = False) -> None:
"""Add an attribute to the mail rule
Parameters
----------
name : str
Name of the attribute to add
value : str
Value of the attribute
is_custom_attribute : bool, optional
Defines whether the attribute being added is custom (use with caution), by default `False`.
Use this with caution, as the mail rule interpreter may not be able to parse a rule with
a custom attribute
Raises
------
KeyError
Raises a `KeyError` when an attribute has already been defined for this rule
KeyError
Raises a `KeyError` if an attribute is not valid
"""
if name in self.rule_attributes:
## Raise Error when rule already contains a value for this attribute
raise KeyError(f"{name} is already an attribute of {self.name}. Consider calling {self.__class__.__name__}.update_attribute() to update the value of this attribute")
if is_custom_attribute:
## Check whether we are using a custom attribute
if name not in self._possible_attributes:
## Add custom attribute name to the possible rule attributes
self._modify_possible_attributes(name)
if name not in self._possible_attributes:
## Raise Error if this attribute name is unallowed
raise KeyError(f"{name} is not a valid filter attribute. Check for typos")
if name == "label":
## Labels are stored in self.labels, not in self.rule_attributes
# raise KeyError(f"Use {self.__class__.__name__}.add_label() or {self.__class__.__name__}.add_labels() to add a label to the rule")
self.add_labels(value)
return
self.rule_attributes[name] = value
[docs]
def add_attributes(self, attributes_to_add: dict) -> None:
"""Add multiple attributes to a `Rule`
Parameters
----------
attributes_to_add : dict
Dictionary where key is the name of the attribute to add (`str`)
and its value (`str`) is the value of the attribute to add corresponding
to that key
Raises
------
TypeError
Raises a `TypeError` if `attributes_to_add` is not a dictionary
"""
if not isinstance(attributes_to_add, dict):
raise TypeError(f"attributes_to_add needs to be a dictionary, but currently it is of type {type(attributes_to_add)}")
for attribute_name, attribute_value in attributes_to_add.items():
self.add_attribute(attribute_name, attribute_value)
[docs]
def add_labels(self, labels: str | list | tuple | set | frozenset | dict) -> None:
"""Adds labels to the mail rule
Parameters
----------
labels : list | tuple | set | frozenset | dict
The label (or labels) to be added to the rule
Raises
------
TypeError
Raises a `TypeError` if the label is not a valid type
"""
if isinstance(labels, str):
self.labels.append(labels)
elif type(labels) in _hp.ITERABLE_DATA_TYPES:
for label in labels:
self.add_labels(label)
else:
raise TypeError(f"The label being added is not a string. It is of type {type(label)}")
[docs]
def add_label(self, label: str) -> None:
"""Alias for :obj:`Rule.add_label()`. Adds labels to the mail rule
Parameters
----------
labels : list | tuple | set | frozenset | dict
The label (or labels) to be added to the rule
Raises
------
TypeError
Raises a `TypeError` if the label is not a valid type
"""
self.add_labels(label)
[docs]
def build_rule(self) -> str:
"""
After all of the details of a rule are defined, this function is run
to actually build the desired mail rule. It takes an optional argument
`rule_name` which is a `str` representing the name of the mail rule,
but when the rule is parsed into Gmail, this gets ignored.
"""
final_rule = ""
if not self.labels:
final_rule += f"{_hp.add_xml_comment(self.name)}\n{self.rule_header}{self.rule_attributes_xmls_str}{self.rule_footer}"
else:
for label in self.labels:
rule_comment = _hp.add_xml_comment(self.name if len(self.labels) == 1 else f'{self.name} ({label})')
final_rule += f"{rule_comment}\n{self.rule_header}{self.xml_format_rule_attribute('label', label)}{self.rule_attributes_xmls_str}{self.rule_footer}\n"
final_rule = final_rule[:-1]
if len(self.labels) > 1:
starting_comment = f"{_hp.add_xml_comment(f'START --- {self.name} --- START')}\n"
ending_comment = f"\n{_hp.add_xml_comment(f'END --- {self.name} --- END')}"
final_rule = f"{starting_comment}{final_rule}{ending_comment}"
final_rule = final_rule.expandtabs(_hp.TAB_SPACING)
return final_rule