diff options
| -rw-r--r-- | Mailman/app/chains.py | 163 | ||||
| -rw-r--r-- | Mailman/app/rules.py | 20 | ||||
| -rw-r--r-- | Mailman/docs/chains.txt | 11 | ||||
| -rw-r--r-- | Mailman/interfaces/chain.py | 60 | ||||
| -rw-r--r-- | Mailman/interfaces/rules.py | 13 | ||||
| -rw-r--r-- | Mailman/rules/administrivia.py | 3 | ||||
| -rw-r--r-- | Mailman/rules/any.py | 3 | ||||
| -rw-r--r-- | Mailman/rules/approved.py | 3 | ||||
| -rw-r--r-- | Mailman/rules/docs/emergency.txt | 13 | ||||
| -rw-r--r-- | Mailman/rules/docs/rules.txt | 1 | ||||
| -rw-r--r-- | Mailman/rules/emergency.py | 10 | ||||
| -rw-r--r-- | Mailman/rules/implicit_dest.py | 3 | ||||
| -rw-r--r-- | Mailman/rules/loop.py | 3 | ||||
| -rw-r--r-- | Mailman/rules/max_recipients.py | 3 | ||||
| -rw-r--r-- | Mailman/rules/max_size.py | 3 | ||||
| -rw-r--r-- | Mailman/rules/moderation.py | 4 | ||||
| -rw-r--r-- | Mailman/rules/news_moderation.py | 10 | ||||
| -rw-r--r-- | Mailman/rules/no_subject.py | 3 | ||||
| -rw-r--r-- | Mailman/rules/suspicious.py | 3 |
19 files changed, 218 insertions, 114 deletions
diff --git a/Mailman/app/chains.py b/Mailman/app/chains.py index 8ac9e9ce7..38c7325ab 100644 --- a/Mailman/app/chains.py +++ b/Mailman/app/chains.py @@ -21,9 +21,13 @@ from __future__ import with_statement __all__ = [ 'AcceptChain', + 'Chain', 'DiscardChain', 'HoldChain', + 'Link', 'RejectChain', + 'initialize', + 'process', ] __metaclass__ = type __i18n_templates__ = True @@ -44,7 +48,8 @@ from Mailman.app.moderator import hold_message from Mailman.app.replybot import autorespond_to_sender, can_acknowledge from Mailman.configuration import config from Mailman.i18n import _ -from Mailman.interfaces import IChain, IChainLink, IMutableChain, IPendable +from Mailman.interfaces import ( + IChain, IChainLink, IMutableChain, IPendable, LinkAction) from Mailman.queue import Switchboard log = logging.getLogger('mailman.vette') @@ -59,7 +64,37 @@ class HeldMessagePendable(dict): -class DiscardChain: +class Link: + """A chain link.""" + implements(IChainLink) + + def __init__(self, rule, action=None, chain=None, function=None): + self.rule = rule + self.action = (LinkAction.defer if action is None else action) + self.chain = chain + self.function = function + + + +class TerminalChainBase: + """A base chain that always matches and executes a method. + + The method is called 'process' and must be provided by the subclass. + """ + implements(IChain) + + def __iter__(self): + """See `IChain`.""" + # First, yield a link that always runs the process method. + yield Link('truth', LinkAction.run, function=self.process) + # Now yield a rule that stops all processing. + yield Link('truth', LinkAction.stop) + + def process(self, mlist, msg, msgdata): + raise NotImplementedError + + +class DiscardChain(TerminalChainBase): """Discard a message.""" implements(IChain) @@ -73,7 +108,7 @@ class DiscardChain: -class HoldChain: +class HoldChain(TerminalChainBase): """Hold a message.""" implements(IChain) @@ -199,7 +234,7 @@ also appear in the first line of the body of the reply.""")), -class RejectChain: +class RejectChain(TerminalChainBase): """Reject/bounce a message.""" implements(IChain) @@ -223,7 +258,7 @@ class RejectChain: -class AcceptChain: +class AcceptChain(TerminalChainBase): """Accept the message for posting.""" implements(IChain) @@ -247,15 +282,6 @@ class AcceptChain: -class Link: - """A chain link.""" - implements(IChainLink) - - def __init__(self, rule, jump): - self.rule = rule - self.jump = jump - - class Chain: """Default built-in moderation chain.""" implements(IMutableChain) @@ -276,42 +302,63 @@ class Chain: """See `IMutableChain`.""" self._links = [] - def process(self, mlist, msg, msgdata): - """See `IMutableChain`.""" - msgdata['rule_hits'] = hits = [] - msgdata['rule_misses'] = misses = [] - jump = None + def __iter__(self): + """See `IChain`.""" for link in self._links: - # The None rule always match. - if link.rule is None: - jump = link.jump - break - # If the rule hits, jump to the given chain. - rule = config.rules.get(link.rule) - if rule is None: - elog.error('Rule not found: %s', rule) - elif rule.check(mlist, msg, msgdata): + yield link + + + +def process(start_chain, mlist, msg, msgdata): + """Process the message through a chain. + + :param start_chain: The name of the chain to start the processing with. + :param mlist: the IMailingList for this message. + :param msg: The Message object. + :param msgdata: The message metadata dictionary. + """ + # Find the starting chain. + current_chain = iter(config.chains[start_chain]) + chain_stack = [] + msgdata['rule_hits'] = hits = [] + msgdata['rule_misses'] = misses = [] + while current_chain: + try: + link = current_chain.next() + except StopIteration: + # This chain is exhausted. Pop the last chain on the stack and + # continue. + if len(chain_stack) == 0: + return + current_chain = chain_stack.pop() + continue + # Process this link. + rule = config.rules[link.rule] + if rule.check(mlist, msg, msgdata): + if rule.record: hits.append(link.rule) - # None is a special jump meaning "keep processing this chain". - if link.jump is not None: - jump = link.jump - break + # The rule matched so run its action. + if link.action is LinkAction.jump: + current_chain = iter(config.chains[link.chain]) + elif link.action is LinkAction.detour: + # Push the current chain so that we can return to it when the + # next chain is finished. + chain_stack.append(current_chain) + current_chain = iter(config.chains[link.chain]) + elif link.action is LinkAction.stop: + # Stop all processing. + return + elif link.action is LinkAction.defer: + # Just process the next link in the chain. + pass + elif link.action is LinkAction.run: + link.function(mlist, msg, msgdata) else: - misses.append(link.rule) + raise AssertionError('Unknown link action: %s' % link.action) else: - # We got through the entire chain without a jumping rule match, so - # we really don't know what to do. Rather than raise an - # exception, jump to the discard chain. - log.info('Jumping to the discard chain by default.') - jump = 'discard' - # Find the named chain. - chain = config.chains.get(jump) - if chain is None: - elog.error('Chain not found: %s', chain) - # Well, what now? Nothing much left to do but discard the - # message, which we can do by simply returning. - else: - chain.process(mlist, msg, msgdata) + # The rule did not match; keep going. + if rule.record: + misses.append(link.rule) @@ -324,20 +371,20 @@ def initialize(): config.chains[chain.name] = chain # Set up a couple of other default chains. default = Chain('built-in', _('The built-in moderation chain.')) - default.append_link(Link('approved', 'accept')) - default.append_link(Link('emergency', 'hold')) - default.append_link(Link('loop', 'discard')) + default.append_link(Link('approved', LinkAction.jump, 'accept')) + default.append_link(Link('emergency', LinkAction.jump, 'hold')) + default.append_link(Link('loop', LinkAction.jump, 'discard')) # Do all these before deciding whether to hold the message for moderation. - default.append_link(Link('administrivia', None)) - default.append_link(Link('implicit-dest', None)) - default.append_link(Link('max-recipients', None)) - default.append_link(Link('max-size', None)) - default.append_link(Link('news-moderation', None)) - default.append_link(Link('no-subject', None)) - default.append_link(Link('suspicious', None)) + default.append_link(Link('administrivia', LinkAction.defer)) + default.append_link(Link('implicit-dest', LinkAction.defer)) + default.append_link(Link('max-recipients', LinkAction.defer)) + default.append_link(Link('max-size', LinkAction.defer)) + default.append_link(Link('news-moderation', LinkAction.defer)) + default.append_link(Link('no-subject', LinkAction.defer)) + default.append_link(Link('suspicious-header', LinkAction.defer)) # Now if any of the above hit, jump to the hold chain. - default.append_link(Link('any', 'hold')) + default.append_link(Link('any', LinkAction.jump, 'hold')) # Finally, the builtin chain defaults to acceptance. - default.append_link(Link(None, 'accept')) + default.append_link(Link('truth', LinkAction.jump, 'accept')) # XXX Read chains from the database and initialize them. pass diff --git a/Mailman/app/rules.py b/Mailman/app/rules.py index a3846541e..f0209c767 100644 --- a/Mailman/app/rules.py +++ b/Mailman/app/rules.py @@ -18,10 +18,13 @@ """Various rule helpers""" __all__ = [ + 'TruthRule', 'initialize', ] +__metaclass__ = type +from zope.interface import implements from zope.interface.verify import verifyObject from Mailman.app.plugins import get_plugins @@ -30,8 +33,25 @@ from Mailman.interfaces import IRule +class TruthRule: + """A rule that always matches.""" + implements(IRule) + + name = 'truth' + description = 'A rule which always matches.' + record = False + + def check(self, mlist, msg, msgdata): + """See `IRule`.""" + return True + + + def initialize(): """Find and register all rules in all plugins.""" + # Register built in rules. + config.rules[TruthRule.name] = TruthRule() + # Find rules in plugins. for rule_finder in get_plugins('mailman.rules'): for rule_class in rule_finder(): rule = rule_class() diff --git a/Mailman/docs/chains.txt b/Mailman/docs/chains.txt index 1b22b9923..0a93683f7 100644 --- a/Mailman/docs/chains.txt +++ b/Mailman/docs/chains.txt @@ -310,7 +310,8 @@ The previously created message is innocuous enough that it should pass through all default rules. This message will end up in the prep queue. >>> file_pos = fp.tell() - >>> chain.process(mlist, msg, {}) + >>> from Mailman.app.chains import process + >>> process('built-in', mlist, msg, {}) >>> fp.seek(file_pos) >>> print 'LOG:', fp.read() LOG: ... ACCEPT: <first> @@ -323,7 +324,8 @@ all default rules. This message will end up in the prep queue. Message-ID: <first> X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW X-Mailman-Rule-Misses: approved; emergency; loop; administrivia; implicit-dest; - max-recipients; max-size; news-moderation; no-subject; any + max-recipients; max-size; news-moderation; no-subject; + suspicious-header <BLANKLINE> An important message. <BLANKLINE> @@ -334,5 +336,6 @@ hit and all rules that have missed. >>> sorted(qdata['rule_hits']) [] >>> sorted(qdata['rule_misses']) - ['administrivia', 'any', 'approved', 'emergency', 'implicit-dest', 'loop', - 'max-recipients', 'max-size', 'news-moderation', 'no-subject'] + ['administrivia', 'approved', 'emergency', 'implicit-dest', 'loop', + 'max-recipients', 'max-size', 'news-moderation', 'no-subject', + 'suspicious-header'] diff --git a/Mailman/interfaces/chain.py b/Mailman/interfaces/chain.py index 226ad232c..8c0837820 100644 --- a/Mailman/interfaces/chain.py +++ b/Mailman/interfaces/chain.py @@ -22,23 +22,51 @@ from zope.interface import Interface, Attribute +class LinkAction(Enum): + # Jump to another chain. + jump = 0 + # Take a detour to another chain, returning to the original chain when + # completed (if no other jump occurs). + detour = 1 + # Stop processing all chains. + stop = 2 + # Continue processing the next link in the chain. + defer = 3 + # Run a function and continue processing. + run = 4 + + + +class IChainLink(Interface): + """A link in the chain.""" + + rule = Attribute('The rule to run for this link.') + + action = Attribute('The LinkAction to take if this rule matches.') + + chain = Attribute('The chain to jump or detour to.') + + function = Attribute( + """The function to execute. + + The function takes three arguments and returns nothing. + :param mlist: the IMailingList object + :param msg: the message being processed + :param msgdata: the message metadata dictionary + """) + + + class IChain(Interface): """A chain of rules.""" name = Attribute('Chain name; must be unique.') description = Attribute('A brief description of the chain.') - def process(mlist, msg, msgdata): - """Process the message through the chain. - - Processing a message involves running through each link in the chain, - until a jump to another chain occurs or the chain reaches the end. - Reaching the end of the chain with no other disposition is equivalent - to discarding the message. + def __iter__(): + """Iterate over all the IChainLinks in this chain. - :param mlist: The mailing list object. - :param msg: The message object. - :param msgdata: The message metadata. + :return: an IChainLink. """ @@ -54,15 +82,3 @@ class IMutableChain(IChain): def flush(): """Delete all links in this chain.""" - - - -class IChainLink(Interface): - """A link in the chain.""" - - rule = Attribute('The rule to run for this link.') - - jump = Attribute( - """The jump action to perform when the rule matches. This may be None - to simply process the next link in the chain. - """) diff --git a/Mailman/interfaces/rules.py b/Mailman/interfaces/rules.py index 24e479f81..e92b354e1 100644 --- a/Mailman/interfaces/rules.py +++ b/Mailman/interfaces/rules.py @@ -25,16 +25,21 @@ class IRule(Interface): """A basic rule.""" name = Attribute('Rule name; must be unique.') + description = Attribute('A brief description of the rule.') + record = Attribute( + """Should this rule's success or failure be recorded? + + This is a boolean; if True then this rule's hit or miss will be + recorded in a message header. If False, it won't. + """) + def check(mlist, msg, msgdata): """Run the rule. - The effects of running the rule can be as simple as appending the rule - name to `msgdata['rules']` when the rule matches. The rule is allowed - to do other things, such as modify the message or metadata. - :param mlist: The mailing list object. :param msg: The message object. :param msgdata: The message metadata. + :returns: a boolean specifying whether the rule matched or not. """ diff --git a/Mailman/rules/administrivia.py b/Mailman/rules/administrivia.py index 589c4a56b..76848fecf 100644 --- a/Mailman/rules/administrivia.py +++ b/Mailman/rules/administrivia.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007 by the Free Software Foundation, Inc. +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -55,6 +55,7 @@ class Administrivia: name = 'administrivia' description = _('Catch mis-addressed email commands.') + record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" diff --git a/Mailman/rules/any.py b/Mailman/rules/any.py index b97ad73d2..c0755b58f 100644 --- a/Mailman/rules/any.py +++ b/Mailman/rules/any.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007 by the Free Software Foundation, Inc. +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -34,6 +34,7 @@ class Any: name = 'any' description = _('Look for any previous rule hit.') + record = False def check(self, mlist, msg, msgdata): """See `IRule`.""" diff --git a/Mailman/rules/approved.py b/Mailman/rules/approved.py index 98b158dd9..f3c1dc412 100644 --- a/Mailman/rules/approved.py +++ b/Mailman/rules/approved.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007 by the Free Software Foundation, Inc. +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -39,6 +39,7 @@ class Approved: name = 'approved' description = _('The message has a matching Approve or Approved header.') + record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" diff --git a/Mailman/rules/docs/emergency.txt b/Mailman/rules/docs/emergency.txt index 685d7bcb6..1375c3bc9 100644 --- a/Mailman/rules/docs/emergency.txt +++ b/Mailman/rules/docs/emergency.txt @@ -16,15 +16,12 @@ list are held for moderator approval. ... An important message. ... """) -The emergency rule is matched as part of the built-in chain. - - >>> from Mailman.configuration import config - >>> chain = config.chains['built-in'] - -The emergency rule matches if the flag is set on the mailing list. +The emergency rule is matched as part of the built-in chain. The emergency +rule matches if the flag is set on the mailing list. + >>> from Mailman.app.chains import process >>> mlist.emergency = True - >>> chain.process(mlist, msg, {}) + >>> process('built-in', mlist, msg, {}) There are two messages in the virgin queue. The one addressed to the original sender will contain a token we can use to grab the held message out of the @@ -72,6 +69,6 @@ However, if the message metadata has a 'moderator_approved' key set, then even if the mailing list has its emergency flag set, the message still goes through to the membership. - >>> chain.process(mlist, msg, dict(moderator_approved=True)) + >>> process('built-in', mlist, msg, dict(moderator_approved=True)) >>> len(virginq.files) 0 diff --git a/Mailman/rules/docs/rules.txt b/Mailman/rules/docs/rules.txt index 1f5b147e7..d2d291331 100644 --- a/Mailman/rules/docs/rules.txt +++ b/Mailman/rules/docs/rules.txt @@ -31,6 +31,7 @@ names to rule objects. no-subject True non-member True suspicious-header True + truth True You can get a rule by name. diff --git a/Mailman/rules/emergency.py b/Mailman/rules/emergency.py index 0e6aa97b4..6d924b399 100644 --- a/Mailman/rules/emergency.py +++ b/Mailman/rules/emergency.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007 by the Free Software Foundation, Inc. +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -33,9 +33,11 @@ class Emergency: implements(IRule) name = 'emergency' - description = _("""\ -The mailing list is in emergency hold and this message was not pre-approved by -the list administrator.""") + description = _( + """The mailing list is in emergency hold and this message was not + pre-approved by the list administrator. + """) + record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" diff --git a/Mailman/rules/implicit_dest.py b/Mailman/rules/implicit_dest.py index 19a096aa5..1b459caed 100644 --- a/Mailman/rules/implicit_dest.py +++ b/Mailman/rules/implicit_dest.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007 by the Free Software Foundation, Inc. +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -36,6 +36,7 @@ class ImplicitDestination: name = 'implicit-dest' description = _('Catch messages with implicit destination.') + record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" diff --git a/Mailman/rules/loop.py b/Mailman/rules/loop.py index a88858d6a..93bd1241d 100644 --- a/Mailman/rules/loop.py +++ b/Mailman/rules/loop.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007 by the Free Software Foundation, Inc. +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -34,6 +34,7 @@ class Loop: name = 'loop' description = _("""Look for a posting loop, via the X-BeenThere header.""") + record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" diff --git a/Mailman/rules/max_recipients.py b/Mailman/rules/max_recipients.py index dfa23f659..e4fb3faf3 100644 --- a/Mailman/rules/max_recipients.py +++ b/Mailman/rules/max_recipients.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007 by the Free Software Foundation, Inc. +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -35,6 +35,7 @@ class MaximumRecipients: name = 'max-recipients' description = _('Catch messages with too many explicit recipients.') + record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" diff --git a/Mailman/rules/max_size.py b/Mailman/rules/max_size.py index b723fbf07..e54b68c9c 100644 --- a/Mailman/rules/max_size.py +++ b/Mailman/rules/max_size.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007 by the Free Software Foundation, Inc. +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -34,6 +34,7 @@ class MaximumSize: name = 'max-size' description = _('Catch messages that are bigger than a specified maximum.') + record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" diff --git a/Mailman/rules/moderation.py b/Mailman/rules/moderation.py index 9fa7cd34d..29a478662 100644 --- a/Mailman/rules/moderation.py +++ b/Mailman/rules/moderation.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007 by the Free Software Foundation, Inc. +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -37,6 +37,7 @@ class Moderation: name = 'moderation' description = _('Match messages sent by moderated members.') + record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" @@ -54,6 +55,7 @@ class NonMember: name = 'non-member' description = _('Match messages sent by non-members.') + record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" diff --git a/Mailman/rules/news_moderation.py b/Mailman/rules/news_moderation.py index 9caf8fb4a..56c9fef37 100644 --- a/Mailman/rules/news_moderation.py +++ b/Mailman/rules/news_moderation.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007 by the Free Software Foundation, Inc. +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -33,9 +33,11 @@ class ModeratedNewsgroup: implements(IRule) name = 'news-moderation' - description = _(u"""\ -Match all messages posted to a mailing list that gateways to a moderated -newsgroup.""") + description = _( + u"""Match all messages posted to a mailing list that gateways to a + moderated newsgroup. + """) + record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" diff --git a/Mailman/rules/no_subject.py b/Mailman/rules/no_subject.py index c36d742b8..4a8c6ac99 100644 --- a/Mailman/rules/no_subject.py +++ b/Mailman/rules/no_subject.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007 by the Free Software Foundation, Inc. +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -34,6 +34,7 @@ class NoSubject: name = 'no-subject' description = _('Catch messages with no, or empty, Subject headers.') + record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" diff --git a/Mailman/rules/suspicious.py b/Mailman/rules/suspicious.py index 0464a6336..4936734e5 100644 --- a/Mailman/rules/suspicious.py +++ b/Mailman/rules/suspicious.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007 by the Free Software Foundation, Inc. +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -36,6 +36,7 @@ class SuspiciousHeader: name = 'suspicious-header' description = _('Catch messages with suspicious headers.') + record = True def check(self, mlist, msg, msgdata): """See `IRule`.""" |
