summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Mailman/app/chains.py300
-rw-r--r--Mailman/app/moderator.py12
-rw-r--r--Mailman/app/replybot.py34
-rw-r--r--Mailman/app/rules.py49
-rw-r--r--Mailman/configuration.py18
-rw-r--r--Mailman/initialize.py5
-rw-r--r--Mailman/interfaces/chain.py68
-rw-r--r--Mailman/interfaces/rules.py28
-rw-r--r--Mailman/rules/__init__.py65
-rw-r--r--Mailman/rules/any.py40
-rw-r--r--Mailman/templates/en/postauth.txt2
-rw-r--r--setup.py2
12 files changed, 503 insertions, 120 deletions
diff --git a/Mailman/app/chains.py b/Mailman/app/chains.py
new file mode 100644
index 000000000..cb5dfafb7
--- /dev/null
+++ b/Mailman/app/chains.py
@@ -0,0 +1,300 @@
+# Copyright (C) 2007 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
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""The built-in rule chains."""
+
+from __future__ import with_statement
+
+__all__ = [
+ 'AcceptChain',
+ 'DiscardChain',
+ 'HoldChain',
+ 'RejectChain',
+ ]
+__metaclass__ = type
+
+
+import logging
+
+from Mailman import i18n
+from Mailman.Message import UserNotification
+from Mailman.Utils import maketext, oneline, wrap, GetCharSet
+from Mailman.app.bounces import bounce_message
+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.queue import Switchboard
+
+log = logging.getLogger('mailman.vette')
+elog = logging.getLogger('mailman.error')
+SEMISPACE = '; '
+
+
+
+class HeldMessagePendable(dict):
+ implements(IPendable)
+ PEND_KEY = 'held message'
+
+
+
+class DiscardChain:
+ """Discard a message."""
+ implements(IChain)
+
+ name = 'discard'
+ description = _('Discard a message and stop processing.')
+
+ def process(self, mlist, msg, msgdata):
+ """See `IChain`."""
+ log.info('DISCARD: %s', msg.get('message-id', 'n/a'))
+ # Nothing more needs to happen.
+
+
+
+class HoldChain:
+ """Hold a message."""
+ implements(IChain)
+
+ name = 'hold'
+ description = _('Hold a message and stop processing.')
+
+ def process(self, mlist, msg, msgdata):
+ """See `IChain`."""
+ # Start by decorating the message with a header that contains a list
+ # of all the rules that matched.
+ msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(msgdata['rules'])
+ # Hold the message by adding it to the list's request database.
+ # XXX How to calculate the reason?
+ request_id = hold_message(mlist, msg, msgdata, None)
+ # Calculate a confirmation token to send to the author of the
+ # message.
+ pendable = HeldMessagePendable(type=HeldMessagePendable.PEND_KEY,
+ id=request_id)
+ token = config.db.pendings.add(pendable)
+ # Get the language to send the response in. If the sender is a
+ # member, then send it in the member's language, otherwise send it in
+ # the mailing list's preferred language.
+ member = mlist.members.get_member(sender)
+ language = (member.preferred_language
+ if member else mlist.preferred_language)
+ # A substitution dictionary for the email templates.
+ charset = GetCharSet(mlist.preferred_language)
+ original_subject = msg.get('subject')
+ if original_subject is None:
+ original_subject = _('(no subject)')
+ else:
+ original_subject = oneline(original_subject, charset)
+ substitutions = {
+ 'listname' : mlist.fqdn_listname,
+ 'subject' : original_subject,
+ 'reason' : 'XXX', #reason,
+ 'confirmurl': '%s/%s' % (mlist.script_url('confirm'), token),
+ }
+ sender = msg.get_sender()
+ # At this point the message is held, but now we have to craft at least
+ # two responses. The first will go to the original author of the
+ # message and it will contain the token allowing them to approve or
+ # discard the message. The second one will go to the moderators of
+ # the mailing list, if the list is so configured.
+ #
+ # Start by possibly sending a response to the message author. There
+ # are several reasons why we might not go through with this. If the
+ # message was gated from NNTP, the author may not even know about this
+ # list, so don't spam them. If the author specifically requested that
+ # acknowledgments not be sent, or if the message was bulk email, then
+ # we do not send the response. It's also possible that either the
+ # mailing list, or the author (if they are a member) have been
+ # configured to not send such responses.
+ if (not msgdata.get('fromusenet') and
+ can_acknowledge(msg) and
+ mlist.respond_to_post_requests and
+ autorespond_to_sender(mlist, sender, language)):
+ # We can respond to the sender with a message indicating their
+ # posting was held.
+ subject = _(
+ 'Your message to $mlist.fqdn_listname awaits moderator approval')
+ language = msgdata.get('lang', lang)
+ text = maketext('postheld.txt', substitutions,
+ lang=language, mlist=mlist)
+ nmsg = UserNotification(sender, adminaddr, subject, text, language)
+ nmsg.send(mlist)
+ # Now the message for the list moderators. This one should appear to
+ # come from <list>-owner since we really don't need to do bounce
+ # processing on it.
+ if mlist.admin_immed_notify:
+ # Now let's temporarily set the language context to that which the
+ # administrators are expecting.
+ with i18n.using_language(mlist.preferred_language):
+ language = mlist.preferred_language
+ charset = GetCharSet(language)
+ # We need to regenerate or re-translate a few values in the
+ # substitution dictionary.
+ d['reason'] = _(reason)
+ d['subject'] = original_subject
+ # craft the admin notification message and deliver it
+ subject = _(
+ '$mlist.fqdn_listname post from $sender requires approval')
+ nmsg = UserNotification(mlist.owner_address,
+ mlist.owner_address,
+ subject, lang=language)
+ nmsg.set_type('multipart/mixed')
+ text = MIMEText(
+ maketext('postauth.txt', substitution,
+ raw=True, mlist=mlist),
+ _charset=charset)
+ dmsg = MIMEText(wrap(_("""\
+If you reply to this message, keeping the Subject: header intact, Mailman will
+discard the held message. Do this if the message is spam. If you reply to
+this message and include an Approved: header with the list password in it, the
+message will be approved for posting to the list. The Approved: header can
+also appear in the first line of the body of the reply.""")),
+ _charset=GetCharSet(lang))
+ dmsg['Subject'] = 'confirm ' + token
+ dmsg['Sender'] = requestaddr
+ dmsg['From'] = requestaddr
+ dmsg['Date'] = email.utils.formatdate(localtime=True)
+ dmsg['Message-ID'] = email.utils.make_msgid()
+ nmsg.attach(text)
+ nmsg.attach(MIMEMessage(msg))
+ nmsg.attach(MIMEMessage(dmsg))
+ nmsg.send(mlist, **{'tomoderators': 1})
+ # Log the held message
+ log.info('HELD: %s post from %s held, message-id=%s: %s',
+ listname, sender, message_id, reason)
+
+
+
+class RejectChain:
+ """Reject/bounce a message."""
+ implements(IChain)
+
+ name = 'reject'
+ description = _('Reject/bounce a message and stop processing.')
+
+ def process(self, mlist, msg, msgdata):
+ """See `IChain`."""
+ # XXX Exception/reason
+ bounce_message(mlist, msg)
+ log.info('REJECT: %s', msg.get('message-id', 'n/a'))
+
+
+
+class AcceptChain:
+ """Accept the message for posting."""
+ implements(IChain)
+
+ name = 'accept'
+ description = _('Accept a message.')
+
+ def process(self, mlist, msg, msgdata):
+ """See `IChain.`"""
+ accept_queue = Switchboard(config.PREPQUEUE_DIR)
+ accept_queue.enqueue(msg, msgdata)
+ log.info('ACCEPT: %s', msg.get('message-id', 'n/a'))
+
+
+
+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)
+
+ def __init__(self, name, description):
+ self.name = name
+ self.description = description
+ self._links = []
+
+ def append_link(self, link):
+ """See `IMutableChain`."""
+ self._links.append(link)
+
+ def flush(self):
+ """See `IMutableChain`."""
+ self._links = []
+
+ def process(self, mlist, msg, msgdata):
+ """See `IMutableChain`."""
+ msgdata['rules'] = rules = []
+ jump = None
+ for link in self._links:
+ # The None rule always match.
+ if link.rule is None:
+ jump = link.jump
+ break
+ # If the rule hits, just 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):
+ rules.append(link.rule.name)
+ # None is a special jump meaning "keep processing this chain".
+ if link.jump is not None:
+ jump = link.jump
+ break
+ 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)
+
+
+
+def initialize():
+ """Set up chains, both built-in and from the database."""
+ for chain_class in (DiscardChain, HoldChain, RejectChain, AcceptChain):
+ chain = chain_class()
+ assert chain.name not in config.chains, (
+ 'Duplicate chain name: %s' % chain.name)
+ config.chains[chain.name] = chain
+ # Set up a couple of other default chains.
+ default = Chain('built-in', _('The built-in moderation chain'), 'accept')
+ default.append_link(Link('approved', 'accept'))
+ default.append_link(Link('emergency', 'hold'))
+ default.append_link(Link('loop', '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))
+ # Now if any of the above hit, jump to the hold chain.
+ default.append_link(Link('any', 'hold'))
+ # Finally, the builtin chain defaults to acceptance.
+ default.append_link(Link(None, 'accept'))
+ # XXX Read chains from the database and initialize them.
+ pass
diff --git a/Mailman/app/moderator.py b/Mailman/app/moderator.py
index 691268153..956dfb773 100644
--- a/Mailman/app/moderator.py
+++ b/Mailman/app/moderator.py
@@ -51,6 +51,18 @@ slog = logging.getLogger('mailman.subscribe')
def hold_message(mlist, msg, msgdata=None, reason=None):
+ """Hold a message for moderator approval.
+
+ The message is added to the mailing list's request database.
+
+ :param mlist: The mailing list to hold the message on.
+ :param msg: The message to hold.
+ :param msgdata: Optional message metadata to hold. If not given, a new
+ metadata dictionary is created and held with the message.
+ :param reason: Optional string reason why the message is being held. If
+ not given, the empty string is used.
+ :return: An id used to handle the held message later.
+ """
if msgdata is None:
msgdata = {}
else:
diff --git a/Mailman/app/replybot.py b/Mailman/app/replybot.py
index c46931770..d1bc9c487 100644
--- a/Mailman/app/replybot.py
+++ b/Mailman/app/replybot.py
@@ -25,6 +25,7 @@ from __future__ import with_statement
__all__ = [
'autorespond_to_sender',
+ 'can_acknowledge',
]
import logging
@@ -87,3 +88,36 @@ def autorespond_to_sender(mlist, sender, lang=None):
mlist.hold_and_cmd_autoresponses[sender] = (today, count + 1)
return True
+
+
+def can_acknowledge(msg):
+ """A boolean specifying whether this message can be acknowledged.
+
+ There are several reasons why a message should not be acknowledged, mostly
+ related to competing standards or common practices. These include:
+
+ * The message has a X-No-Ack header with any value
+ * The message has an X-Ack header with a 'no' value
+ * The message has a Precedence header
+ * The message has an Auto-Submitted header and that header does not have a
+ value of 'no'
+ * The message has an empty Return-Path header, e.g. <>
+ * The message has any RFC 2369 headers (i.e. List-* headers)
+
+ :param msg: a Message object.
+ :return: Boolean specifying whether the message can be acknowledged or not
+ (which is different from whether it will be acknowledged).
+ """
+ # I wrote it this way for clarity and consistency with the docstring.
+ for header in msg:
+ if header in ('x-no-ack', 'precedence'):
+ return False
+ if header.lower().startswith('list-'):
+ return False
+ if msg.get('x-ack', '').lower() == 'no':
+ return False
+ if msg.get('auto-submitted', 'no').lower() <> 'no'
+ return False
+ if msg.get('return-path') == '<>':
+ return False
+ return True
diff --git a/Mailman/app/rules.py b/Mailman/app/rules.py
index 37f5d9af4..948ee7dd7 100644
--- a/Mailman/app/rules.py
+++ b/Mailman/app/rules.py
@@ -15,50 +15,21 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-"""Process all rules defined by entry points."""
+"""Various rule helpers"""
__all__ = [
- 'find_rule',
- 'process',
+ 'initialize',
]
from Mailman.app.plugins import get_plugins
+from Mailman.configuration import config
-def process(mlist, msg, msgdata, rule_set=None):
- """Default rule processing plugin.
-
- Rules are processed in random order.
-
- :param msg: The message object.
- :param msgdata: The message metadata.
- :param rule_set: The name of the rules to run. None (the default) means
- to run all available rules.
- :return: A set of rule names that matched.
- """
- # Collect all rules from all rule processors.
- rules = set()
- for rule_set_class in get_plugins('mailman.rules'):
- rules |= set(rule for rule in rule_set_class().rules
- if rule_set is None or rule.name in rule_set)
- # Now process all rules, returning the set of rules that match.
- rule_matches = set()
- for rule in rules:
- if rule.check(mlist, msg, msgdata):
- rule_matches.add(rule.name)
- return rule_matches
-
-
-
-def find_rule(rule_name):
- """Find the named rule.
-
- :param rule_name: The name of the rule to find.
- :return: The named rule, or None if no such rule exists.
- """
- for rule_set_class in get_plugins('mailman.rules'):
- rule = rule_set_class().get(rule_name)
- if rule is not None:
- return rule
- return None
+def initialize():
+ """Find and register all rules in all plugins."""
+ for rule_finder in get_plugins('mailman.rules'):
+ for rule in rule_finder():
+ assert rule.name not in config.rules, (
+ 'Duplicate rule "%s" found in %s' % (rule.name, rule_finder))
+ config.rules[rule.name] = rule
diff --git a/Mailman/configuration.py b/Mailman/configuration.py
index c2e88affd..5fc359578 100644
--- a/Mailman/configuration.py
+++ b/Mailman/configuration.py
@@ -128,17 +128,18 @@ class Configuration(object):
self.PRIVATE_ARCHIVE_FILE_DIR = join(VAR_DIR, 'archives', 'private')
# Directories used by the qrunner subsystem
self.QUEUE_DIR = qdir = join(VAR_DIR, 'qfiles')
- self.INQUEUE_DIR = join(qdir, 'in')
- self.OUTQUEUE_DIR = join(qdir, 'out')
- self.CMDQUEUE_DIR = join(qdir, 'commands')
+ self.ARCHQUEUE_DIR = join(qdir, 'archive')
+ self.BADQUEUE_DIR = join(qdir, 'bad')
self.BOUNCEQUEUE_DIR = join(qdir, 'bounces')
+ self.CMDQUEUE_DIR = join(qdir, 'commands')
+ self.INQUEUE_DIR = join(qdir, 'in')
+ self.MAILDIR_DIR = join(qdir, 'maildir')
self.NEWSQUEUE_DIR = join(qdir, 'news')
- self.ARCHQUEUE_DIR = join(qdir, 'archive')
+ self.OUTQUEUE_DIR = join(qdir, 'out')
+ self.PREPQUEUE_DIR = join(qdir, 'prepare')
+ self.RETRYQUEUE_DIR = join(qdir, 'retry')
self.SHUNTQUEUE_DIR = join(qdir, 'shunt')
self.VIRGINQUEUE_DIR = join(qdir, 'virgin')
- self.BADQUEUE_DIR = join(qdir, 'bad')
- self.RETRYQUEUE_DIR = join(qdir, 'retry')
- self.MAILDIR_DIR = join(qdir, 'maildir')
self.MESSAGES_DIR = join(VAR_DIR, 'messages')
# Other useful files
self.PIDFILE = join(datadir, 'master-qrunner.pid')
@@ -174,6 +175,9 @@ class Configuration(object):
# Always add and enable the default server language.
code = self.DEFAULT_SERVER_LANGUAGE
self.languages.enable_language(code)
+ # Create the registry of rules and chains.
+ self.chains = {}
+ self.rules = {}
def add_domain(self, email_host, url_host=None):
"""Add a virtual domain.
diff --git a/Mailman/initialize.py b/Mailman/initialize.py
index dff2677f3..7e191a9d2 100644
--- a/Mailman/initialize.py
+++ b/Mailman/initialize.py
@@ -31,7 +31,9 @@ from zope.interface.verify import verifyObject
import Mailman.configuration
import Mailman.loginit
+from Mailman.app.chains import initialize as initialize_chains
from Mailman.app.plugins import get_plugin
+from Mailman.app.rules import initialize as initialize_rules
from Mailman.interfaces import IDatabase
@@ -63,6 +65,9 @@ def initialize_2(debug=False):
verifyObject(IDatabase, database)
database.initialize(debug)
Mailman.configuration.config.db = database
+ # Initialize the rules and chains.
+ initialize_rules()
+ initialize_chains()
def initialize(config_path=None, propagate_logs=False):
diff --git a/Mailman/interfaces/chain.py b/Mailman/interfaces/chain.py
new file mode 100644
index 000000000..226ad232c
--- /dev/null
+++ b/Mailman/interfaces/chain.py
@@ -0,0 +1,68 @@
+# Copyright (C) 2007 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
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Interface describing the basics of chains and links."""
+
+from munepy import Enum
+from zope.interface import Interface, Attribute
+
+
+
+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.
+
+ :param mlist: The mailing list object.
+ :param msg: The message object.
+ :param msgdata: The message metadata.
+ """
+
+
+
+class IMutableChain(IChain):
+ """Like `IChain` but can be mutated."""
+
+ def append_link(link):
+ """Add a new chain link to the end of this chain.
+
+ :param link: The chain link to add.
+ """
+
+ 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 5549b8e6b..24e479f81 100644
--- a/Mailman/interfaces/rules.py
+++ b/Mailman/interfaces/rules.py
@@ -21,11 +21,6 @@ from zope.interface import Interface, Attribute
-class DuplicateRuleError(Exception):
- """A rule or rule name is added to a processor more than once."""
-
-
-
class IRule(Interface):
"""A basic rule."""
@@ -43,26 +38,3 @@ class IRule(Interface):
:param msg: The message object.
:param msgdata: The message metadata.
"""
-
-
-
-class IRuleSet(Interface):
- """A rule processor."""
-
- rules = Attribute('The set of all rules this processor knows about.')
-
- def __getitem__(rule_name):
- """Return the named rule.
-
- :param rule_name: The name of the rule.
- :return: The IRule given by this name.
- :raise: KeyError if no such rule is known by this processor.
- """
-
- def get(rule_name, default=None):
- """Return the name rule.
-
- :param rule_name: The name of the rule.
- :return: The IRule given by this name, or `default` if no such rule
- is known by this processor.
- """
diff --git a/Mailman/rules/__init__.py b/Mailman/rules/__init__.py
index 299c9f697..a78e9bd05 100644
--- a/Mailman/rules/__init__.py
+++ b/Mailman/rules/__init__.py
@@ -17,57 +17,34 @@
"""The built in rule set."""
-__all__ = ['BuiltinRules']
+__all__ = ['initialize']
__metaclass__ = type
import os
import sys
-from zope.interface import implements
-from Mailman.interfaces import DuplicateRuleError, IRule, IRuleSet
+from Mailman.interfaces import IRule
-class BuiltinRules:
- implements(IRuleSet)
+def initialize():
+ """Initialize the built-in rules.
- def __init__(self):
- """The set of all built-in rules."""
- self._rules = {}
- rule_set = set()
- # Find all rules found in all modules inside our package.
- mypackage = self.__class__.__module__
- here = os.path.dirname(sys.modules[mypackage].__file__)
- for filename in os.listdir(here):
- basename, extension = os.path.splitext(filename)
- if extension <> '.py':
- continue
- module_name = mypackage + '.' + basename
- __import__(module_name, fromlist='*')
- module = sys.modules[module_name]
- for name in module.__all__:
- rule = getattr(module, name)
- if IRule.implementedBy(rule):
- if rule.name in self._rules or rule in rule_set:
- raise DuplicateRuleError(rule.name)
- self._rules[rule.name] = rule
- rule_set.add(rule)
-
- def __getitem__(self, rule_name):
- """See `IRuleSet`."""
- return self._rules[rule_name]()
-
- def get(self, rule_name, default=None):
- """See `IRuleSet`."""
- missing = object()
- rule = self._rules.get(rule_name, missing)
- if rule is missing:
- return default
- return rule()
-
- @property
- def rules(self):
- """See `IRuleSet`."""
- for rule in self._rules.values():
- yield rule()
+ Rules are auto-discovered by searching for IRule implementations in all
+ importable modules in this subpackage.
+ """
+ # Find all rules found in all modules inside our package.
+ import Mailman.rules
+ here = os.path.dirname(Mailman.rules.__file__)
+ for filename in os.listdir(here):
+ basename, extension = os.path.splitext(filename)
+ if extension <> '.py':
+ continue
+ module_name = 'Mailman.rules.' + basename
+ __import__(module_name, fromlist='*')
+ module = sys.modules[module_name]
+ for name in module.__all__:
+ rule = getattr(module, name)
+ if IRule.implementedBy(rule):
+ yield rule
diff --git a/Mailman/rules/any.py b/Mailman/rules/any.py
new file mode 100644
index 000000000..b97ad73d2
--- /dev/null
+++ b/Mailman/rules/any.py
@@ -0,0 +1,40 @@
+# Copyright (C) 2007 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
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Check if any previous rules have matched."""
+
+__all__ = ['Any']
+__metaclass__ = type
+
+
+from zope.interface import implements
+
+from Mailman.i18n import _
+from Mailman.interfaces import IRule
+
+
+
+class Any:
+ """Look for any previous rule match."""
+ implements(IRule)
+
+ name = 'any'
+ description = _('Look for any previous rule hit.')
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ return len(msgdata.get('rules', [])) > 0
diff --git a/Mailman/templates/en/postauth.txt b/Mailman/templates/en/postauth.txt
index a10727716..19f1e384a 100644
--- a/Mailman/templates/en/postauth.txt
+++ b/Mailman/templates/en/postauth.txt
@@ -1,7 +1,7 @@
As list administrator, your authorization is requested for the
following mailing list posting:
- List: %(listname)s@%(hostname)s
+ List: %(listname)s
From: %(sender)s
Subject: %(subject)s
Reason: %(reason)s
diff --git a/setup.py b/setup.py
index 16a4fd3ef..e35202b06 100644
--- a/setup.py
+++ b/setup.py
@@ -86,7 +86,7 @@ Any other spelling is incorrect.""",
'mailman.database' : 'stock = Mailman.database:StockDatabase',
'mailman.styles' : 'default = Mailman.app.styles:DefaultStyle',
'mailman.mta' : 'stock = Mailman.MTA:Manual',
- 'mailman.rules' : 'default = Mailman.rules:BuiltinRules',
+ 'mailman.rules' : 'default = Mailman.rules:initialize',
},
# Third-party requirements.
install_requires = [