Exemple #1
0
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
        )
Exemple #2
0
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
Exemple #3
0
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
Exemple #4
0
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
Exemple #5
0
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
Exemple #6
0
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
Exemple #7
0
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()
Exemple #8
0
 class MockRuleTwoArgs(Rule):
     HANDLER_TYPE = 'mock'
     HANDLER_ID = 'MOCKRULE'
     TAGGED_ARGS = [
         Number(),
         StringList(),
     ]
Exemple #9
0
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
Exemple #10
0
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)
Exemple #11
0
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)
Exemple #12
0
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()
Exemple #13
0
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()
Exemple #14
0
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
Exemple #15
0
    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
Exemple #16
0
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
Exemple #17
0
 def __init__(self) -> None:
     super().__init__(
         ('COMPARATOR',),
         (StringList(1),),
     )
Exemple #18
0
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