diff options
| author | Barry Warsaw | 2008-01-23 23:47:50 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2008-01-23 23:47:50 -0500 |
| commit | df637148d8fa2d5c101a990ee6766ea8547f000a (patch) | |
| tree | 92e3e1c218c6f59031a0d0383a98a38e38dc2f9f | |
| parent | 4460aad316db5c8af9b84c392e67441acaac9d72 (diff) | |
| download | mailman-df637148d8fa2d5c101a990ee6766ea8547f000a.tar.gz mailman-df637148d8fa2d5c101a990ee6766ea8547f000a.tar.zst mailman-df637148d8fa2d5c101a990ee6766ea8547f000a.zip | |
More changes to rules and chains.
Now a link has a rule, action, chain, and function, not all of which needs to
be specified. The action is a LinkAction enum adn specifies what to do should
the rule match. The use of the chain or function depends on what the action
is.
Several interface changes now make it easier to jump to other chains, push
(i.e. detour) to chains, etc. Rules can also now specify that they should not
be recorded in X-* headers.
Added a TruthRule which always matches.
| -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`.""" |
