summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Mailman/app/chains.py163
-rw-r--r--Mailman/app/rules.py20
-rw-r--r--Mailman/docs/chains.txt11
-rw-r--r--Mailman/interfaces/chain.py60
-rw-r--r--Mailman/interfaces/rules.py13
-rw-r--r--Mailman/rules/administrivia.py3
-rw-r--r--Mailman/rules/any.py3
-rw-r--r--Mailman/rules/approved.py3
-rw-r--r--Mailman/rules/docs/emergency.txt13
-rw-r--r--Mailman/rules/docs/rules.txt1
-rw-r--r--Mailman/rules/emergency.py10
-rw-r--r--Mailman/rules/implicit_dest.py3
-rw-r--r--Mailman/rules/loop.py3
-rw-r--r--Mailman/rules/max_recipients.py3
-rw-r--r--Mailman/rules/max_size.py3
-rw-r--r--Mailman/rules/moderation.py4
-rw-r--r--Mailman/rules/news_moderation.py10
-rw-r--r--Mailman/rules/no_subject.py3
-rw-r--r--Mailman/rules/suspicious.py3
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`."""