class CommandNotify(Command): HANDLER_ID = 'NOTIFY' EXTENSION_NAME = 'enotify' TAGGED_ARGS = { 'from': Tag('FROM', (StringList(1), )), 'importance': Tag('IMPORTANCE', (StringList(1), )), 'options': Tag('OPTIONS', (StringList(), )), 'message': Tag('MESSAGE', (StringList(1), )), } POSITIONAL_ARGS = [StringList(length=1)] def evaluate(self, message: Message, state: EvaluationState) -> None: notify_from = notify_importance = self.notify_message = None notify_options = [] # type: ignore if 'from' in self.tagged_args: notify_from = self.tagged_args['from'][1][0] # type: ignore if 'importance' in self.tagged_args: notify_importance = self.tagged_args['importance'][1][ 0] # type: ignore if 'options' in self.tagged_args: notify_options = self.tagged_args['options'][1] # type: ignore notify_message = None if 'message' in self.tagged_args: notify_message = self.tagged_args['message'][1][0] # type: ignore notify_method = self.positional_args[0][0] # type: ignore state.check_required_extension('enotify', 'NOTIFY') notify_from = expand_variables(notify_from, state) # type: ignore notify_importance = expand_variables(notify_importance, state) # type: ignore notify_options = list( map(lambda s: expand_variables(s, state), notify_options)) notify_message = expand_variables(notify_message, state) # type: ignore notify_method = expand_variables(notify_method, state) m = re.match('^([A-Za-z][A-Za-z0-9.+-]*):', notify_method) if not m: raise RuleSyntaxError( "Notification method must be an URI, e.g. 'mailto:[email protected]'" ) if notify_importance and notify_importance not in ["1", "2", "3"]: raise RuleSyntaxError( "Illegal notify importance '%s' encountered" % notify_importance) notify_method_cls = ExtensionRegistry.get_notification_method( m.group(1).lower()) if not notify_method_cls: raise RuleSyntaxError("Unsupported notification method '%s'" % m.group(1)) (res, msg) = notify_method_cls.test_valid(notify_method) if not res: raise RuleSyntaxError(msg) state.actions.append( 'notify', (notify_method, notify_from, notify_importance, notify_options, notify_message) # type: ignore )
class TestAddress(Test): HANDLER_ID: Text = 'ADDRESS' TAGGED_ARGS = { 'comparator': Comparator(), 'match_type': MatchType(), 'address_part': Tag(('LOCALPART', 'DOMAIN', 'ALL')), } POSITIONAL_ARGS = [ StringList(), StringList(), ] def __init__(self, arguments: Optional[List[Union['TagGrammar', SupportsInt, List[Union[ Text, 'String']]]]] = None, tests: Optional[List['Test']] = None) -> None: super().__init__(arguments, tests) self.headers, self.keylist = self.positional_args self.match_type = self.comparator = self.address_part = None if 'comparator' in self.tagged_args: self.comparator = self.tagged_args['comparator'][1][ 0] # type: ignore if 'match_type' in self.tagged_args: self.match_type = self.tagged_args['match_type'][0] if 'address_part' in self.tagged_args: self.address_part = self.tagged_args['address_part'][0] def evaluate(self, message: Message, state: EvaluationState) -> bool: if not isinstance(self.keylist, list): raise ValueError('TestAddress keylist not iterable') if not isinstance(self.headers, list): raise ValueError('TestAddress headers not iterable') header_values: List[Text] = [] for header in self.headers: header = expand_variables(header, state) # TODO: section 5.1: we should restrict the allowed headers to # those headers that contain an "address-list". this includes at # least: from, to, cc, bcc, sender, resent-from, resent-to. header_values.extend(message.get_all(header, [])) addresses: List[Text] = [] for msg_address in email.utils.getaddresses(header_values): if msg_address[1] != '': addresses.append( sifter.grammar.string.address_part( msg_address[1], cast(Text, self.address_part))) for address in addresses: for key in self.keylist: key = expand_variables(key, state) if sifter.grammar.string.compare(address, key, state, self.comparator, cast(Text, self.match_type)): return True return False
class TestNotifyMethodCapability(Test): HANDLER_ID = 'NOTIFY_METHOD_CAPABILITY' TAGGED_ARGS = { 'comparator': Comparator(), 'match_type': MatchType(), } POSITIONAL_ARGS = [ StringList(1), StringList(1), StringList(), ] def evaluate(self, message: Message, state: EvaluationState) -> bool: state.check_required_extension('enotify', 'NOTIFY') match_type: Text if 'comparator' in self.tagged_args: comparator = self.tagged_args['comparator'][1][0] # type: ignore else: comparator = 'i;ascii-casemap' if 'match_type' in self.tagged_args: match_type = self.tagged_args['match_type'][0] # type: ignore else: match_type = 'IS' notification_uri = self.positional_args[0][0] # type: ignore notification_capability = self.positional_args[1][0] # type: ignore key_list: List[Any] = self.positional_args[2] # type: ignore notification_uri = sifter.grammar.string.expand_variables( notification_uri, state) notification_capability = sifter.grammar.string.expand_variables( notification_capability, state) key_list = list( map(lambda s: sifter.grammar.string.expand_variables(s, state), key_list)) m = re.match('^([A-Za-z][A-Za-z0-9.+-]*):', notification_uri) if not m: return False notify_method_cls = ExtensionRegistry.get_notification_method( m.group(1).lower()) if not notify_method_cls: return False (success, result) = notify_method_cls.test_capability(notification_uri, notification_capability) if not success: return False for key in key_list: if sifter.grammar.string.compare(result, key, state, comparator, match_type): return True return False
class CommandSet(Command): HANDLER_ID = 'SET' EXTENSION_NAME = 'variables' TAGGED_ARGS = { 'lower': Tag('LOWER'), 'upper': Tag('UPPER'), 'lowerfirst': Tag('LOWERFIRST'), 'upperfirst': Tag('UPPERFIRST'), 'quotewildcard': Tag('QUOTEWILDCARD'), 'quoteregex': Tag('QUOTEREGEX'), 'encodeurl': Tag('ENCODEURL'), 'length': Tag('LENGTH'), } POSITIONAL_ARGS = [ StringList(length=1), StringList(length=1), ] def evaluate(self, message: Message, state: EvaluationState) -> None: state.check_required_extension('variables', 'VARIABLES') variable_modifier = self.tagged_args variable_name = self.positional_args[0][0] # type: ignore if (not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', variable_name)): raise RuleSyntaxError("Illegal variable name '%s' encountered" % variable_name) variable_value: Text = self.positional_args[1][0] # type: ignore variable_value = expand_variables(variable_value, state) if 'lower' in variable_modifier: variable_value = variable_value.lower() if 'upper' in variable_modifier: variable_value = variable_value.upper() if 'lowerfirst' in variable_modifier: variable_value = variable_value[:1].lower() + variable_value[1:] if 'upperfirst' in variable_modifier: variable_value = variable_value[:1].upper() + variable_value[1:] if 'quotewildcard' in variable_modifier: variable_value = variable_value.replace('*', '\\*') variable_value = variable_value.replace('?', '\\?') variable_value = variable_value.replace('\\', '\\\\') if 'quoteregex' in variable_modifier: variable_value = re.escape(variable_value) if 'encodeurl' in variable_modifier: variable_value = quote(variable_value, safe='-._~') if 'length' in variable_modifier: variable_value = "" + str(len(variable_value)) state.named_variables[variable_name] = variable_value
class CommandRewrite(Command): HANDLER_ID: Text = 'REWRITE' EXTENSION_NAME = 'rewrite' POSITIONAL_ARGS = [ StringList(length=1), StringList(length=1), ] def evaluate(self, message: Message, state: EvaluationState) -> None: state.check_required_extension('rewrite', 'REWRITE') search = self.positional_args[0][0] # type: ignore replace = self.positional_args[1][0] # type: ignore search = expand_variables(search, state) replace = expand_variables(replace, state) state.actions.append('rewrite', (search, replace)) # type: ignore
class TestValidNotifyMethod(Test): HANDLER_ID = 'VALID_NOTIFY_METHOD' POSITIONAL_ARGS = [ StringList(), ] def evaluate(self, message: Message, state: EvaluationState) -> bool: notify_methods = self.positional_args[0] state.check_required_extension('enotify', 'NOTIFY') notify_methods = list( map(lambda s: sifter.grammar.string.expand_variables(s, state), notify_methods)) # type: ignore for notify_method in notify_methods: m = re.match('^([A-Za-z][A-Za-z0-9.+-]*):', notify_method) if not m: return False notify_method_cls = ExtensionRegistry.get_notification_method( m.group(1).lower()) if not notify_method_cls: return False (res, _) = notify_method_cls.test_valid(notify_method) return res return False
class CommandRedirect(Command): HANDLER_ID = 'REDIRECT' POSITIONAL_ARGS = [StringList(length=1)] def __init__(self, arguments: Optional[List[Union['TagGrammar', SupportsInt, List[Union[ Text, 'String']]]]] = None, tests: Optional[List['Test']] = None, block: Optional[CommandList] = None) -> None: super().__init__(arguments, tests, block) self.email_address = self.positional_args[0][0] # type: ignore # TODO: section 2.4.2.3 constrains the email address to a limited # subset of valid address formats. need to check if python's # email.utils also uses this subset or if we need to do our own # parsing. realname, emailaddr = email.utils.parseaddr(self.email_address) if emailaddr == "": raise RuleSyntaxError( "REDIRECT destination not a valid email address: %s" % self.email_address) def evaluate(self, message: Message, state: EvaluationState) -> None: email_address = expand_variables(self.email_address, state) state.actions.append('redirect', email_address) state.actions.cancel_implicit_keep()
class MockRuleTwoArgs(Rule): HANDLER_TYPE = 'mock' HANDLER_ID = 'MOCKRULE' TAGGED_ARGS = [ Number(), StringList(), ]
class TestHeader(Test): HANDLER_ID = 'HEADER' TAGGED_ARGS = { 'comparator': Comparator(), 'match_type': MatchType(), } POSITIONAL_ARGS = [ StringList(), StringList(), ] _newline_re = re.compile(r'\n+\s+') def __init__(self, arguments: Optional[List[Union['TagGrammar', SupportsInt, List[Union[ Text, 'String']]]]] = None, tests: Optional[List['Test']] = None) -> None: super().__init__(arguments, tests) self.headers = self.positional_args[0] self.keylist = self.positional_args[1] self.match_type: Optional['TagGrammar'] = None self.comparator: Optional[Union[Text, 'TagGrammar']] = None if 'comparator' in self.tagged_args: self.comparator = self.tagged_args['comparator'][1][ 0] # type: ignore if 'match_type' in self.tagged_args: self.match_type = self.tagged_args['match_type'][0] # type: ignore def evaluate(self, message: Message, state: EvaluationState) -> bool: if not isinstance(self.headers, list): raise ValueError("TestHeader.headers is not a list") if not isinstance(self.keylist, list): raise ValueError("TestHeader.keylist is not a list") for header in self.headers: header = expand_variables(header, state) for value in message.get_all(header, []): value = self._newline_re.sub(" ", value) for key in self.keylist: key = expand_variables(key, state) if string_compare(str(value), key, state, self.comparator, self.match_type): return True return False
class CommandAddFlag(Command): HANDLER_ID = 'ADDFLAG' EXTENSION_NAME = 'imap4flags' POSITIONAL_ARGS = [StringList()] def evaluate(self, message: Message, state: EvaluationState) -> None: state.check_required_extension('imap4flags', 'imapflags') flag_list = self.positional_args[0] flag_list = list(map(lambda s: expand_variables(s, state), flag_list)) # type: ignore state.actions.append('addflag', flag_list)
class CommandRequire(Command): HANDLER_ID: Text = 'REQUIRE' POSITIONAL_ARGS = [StringList()] def evaluate(self, message: Message, state: EvaluationState) -> None: ext_name_list = self.positional_args[0] for ext_name in ext_name_list: # type: ignore if not ExtensionRegistry.has_extension(ext_name): raise RuntimeError("Required extension '%s' not supported" % ext_name) state.require_extension(ext_name)
class CommandPipe(Command): HANDLER_ID: Text = 'PIPE' EXTENSION_NAME = 'pipe' POSITIONAL_ARGS = [StringList(length=1)] def evaluate(self, message: Message, state: EvaluationState) -> None: state.check_required_extension('pipe', 'PIPE') pipe_dest = self.positional_args[0] pipe_dest = map(lambda s: expand_variables(s, state), pipe_dest) # type: ignore state.actions.append('pipe', pipe_dest) # type: ignore state.actions.cancel_implicit_keep()
class CommandFileInto(Command): HANDLER_ID = 'FILEINTO' EXTENSION_NAME = 'fileinto' POSITIONAL_ARGS = [StringList(length=1)] def evaluate(self, message: Message, state: EvaluationState) -> None: state.check_required_extension('fileinto', 'FILEINTO') file_dest = self.positional_args[0] file_dest = list(map(lambda s: expand_variables(s, state), file_dest)) # type: ignore state.actions.append('fileinto', file_dest) state.actions.cancel_implicit_keep()
class TestExists(Test): HANDLER_ID = 'EXISTS' POSITIONAL_ARGS = [StringList()] def evaluate(self, message: Message, state: EvaluationState) -> bool: headers = self.positional_args[0] if not isinstance(headers, list): raise ValueError("TestExists.headers must be a list") for header in headers: header = expand_variables(header, state) if header not in message: return False return True
def validate( self, arg_list: List[Union['TagGrammar', SupportsInt, List[Union[Text, 'String']]]], starting_index: int ) -> Optional[int]: validated_args = super(BodyTransform, self).validate(arg_list, starting_index) if validated_args is None: raise ValueError('unexpected return value from super in BodyTransform') if validated_args > 0 and arg_list[starting_index] == 'CONTENT': content_args = StringList().validate(arg_list, starting_index + validated_args) if content_args is None: raise ValueError('unexpected return value from StringList.validate') if content_args > 0: return validated_args + content_args raise RuleSyntaxError("body :content requires argument") return validated_args
class TestIHave(Test): HANDLER_ID = 'IHAVE' EXTENSION_NAME = 'ihave' POSITIONAL_ARGS = [ StringList(), ] def evaluate(self, message: Message, state: EvaluationState) -> bool: state.check_required_extension('ihave', 'conditions on installed extensions') extension_list = self.positional_args[0] ret_val = True for ext_name in extension_list: # type: ignore if ExtensionRegistry.has_extension(ext_name): state.require_extension(ext_name) else: ret_val = False return ret_val
def __init__(self) -> None: super().__init__( ('COMPARATOR',), (StringList(1),), )
class TestBody(Test): HANDLER_ID = 'BODY' EXTENSION_NAME = 'body' TAGGED_ARGS = { 'comparator': Comparator(), 'match_type': MatchType(), 'body_transform': BodyTransform(), } POSITIONAL_ARGS = [ StringList(), ] def __init__(self, arguments: Optional[List[Union['Tag', SupportsInt, List[Union[ Text, 'String']]]]] = None, tests: Optional[List['Test']] = None, validate: bool = True) -> None: super(TestBody, self).__init__(arguments, tests) self.keylist = self.positional_args[0] self.body_transform = self.match_type = self.comparator = None if 'comparator' in self.tagged_args: self.comparator = self.tagged_args['comparator'][1][ 0] # type: ignore if 'match_type' in self.tagged_args: self.match_type = self.tagged_args['match_type'][0] if 'body_transform' in self.tagged_args: body_transform_type = self.tagged_args['body_transform'][0] if body_transform_type == 'RAW': self.body_transform = [] elif body_transform_type == 'TEXT': self.body_transform = ['text'] else: self.body_transform = self.tagged_args['body_transform'][ 1] # type: ignore else: self.body_transform = ['text'] def evaluate(self, message: Message, state: EvaluationState) -> bool: state.check_required_extension('body', 'tests against the email body') if not self.body_transform: # RAW # Flatten message, match header / body separator (two new-lines); # if there are no headers, we match ^\n, which is guaranteed to be there (_, bodystr) = re.split(r'^\r?\n|\r?\n\r?\n', message.as_string(False), 1) return self.evaluate_part(bodystr, state) for msgpart in message.walk(): if msgpart.is_multipart(): # TODO: If "multipart/*" extract prologue and epilogue and make that searcheable # TODO: If "message/rfc822" extract headers and make that searchable # Insetad we skip multipart objects and descend into its children continue msgtxt = msgpart.get_payload() for mimetype in self.body_transform: if not mimetype: # empty body_transform matches all if self.evaluate_part(msgtxt, state): return True match = re.match(r'^([^/]+)(?:/([^/]+))?$', mimetype) if not match: continue # malformed body_transform is skipped (maintype, subtype) = match.groups() if maintype == msgpart.get_content_maintype() and ( not subtype or subtype == msgpart.get_content_subtype()): if self.evaluate_part(msgtxt, state): return True return False def evaluate_part(self, part_str: Text, state: EvaluationState) -> bool: for key in self.keylist: # type: ignore key = expand_variables(key, state) if string_compare(part_str, key, state, self.comparator, self.match_type): # type: ignore return True return False