summaryrefslogtreecommitdiff
path: root/Mailman
diff options
context:
space:
mode:
authorBarry Warsaw2008-01-30 23:58:52 -0500
committerBarry Warsaw2008-01-30 23:58:52 -0500
commitc1cc921b691eb60445cf28bc66a59b02b3cd09a4 (patch)
tree3af37f53607a37a945ffbf81ce21454d8e9fc776 /Mailman
parent7b853f27c34c2a168b5dbbf796b84517a20c6191 (diff)
downloadmailman-c1cc921b691eb60445cf28bc66a59b02b3cd09a4.tar.gz
mailman-c1cc921b691eb60445cf28bc66a59b02b3cd09a4.tar.zst
mailman-c1cc921b691eb60445cf28bc66a59b02b3cd09a4.zip
Diffstat (limited to 'Mailman')
-rw-r--r--Mailman/app/chains.py6
-rw-r--r--Mailman/chains/accept.py55
-rw-r--r--Mailman/chains/base.py131
-rw-r--r--Mailman/chains/builtin.py59
-rw-r--r--Mailman/chains/discard.py43
-rw-r--r--Mailman/chains/headers.py116
-rw-r--r--Mailman/chains/hold.py177
-rw-r--r--Mailman/chains/reject.py55
-rw-r--r--Mailman/interfaces/chain.py26
9 files changed, 659 insertions, 9 deletions
diff --git a/Mailman/app/chains.py b/Mailman/app/chains.py
index a43e23847..fc7899cbe 100644
--- a/Mailman/app/chains.py
+++ b/Mailman/app/chains.py
@@ -49,7 +49,7 @@ def process(mlist, msg, msgdata, start_chain='built-in'):
msgdata['rule_misses'] = misses = []
# Find the starting chain and begin iterating through its links.
chain = config.chains[start_chain]
- chain_iter = iter(chain)
+ chain_iter = chain.get_links(mlist, msg, msgdata)
# Loop until we've reached the end of all processing chains.
while chain:
# Iterate over all links in the chain. Do this outside a for-loop so
@@ -73,14 +73,14 @@ def process(mlist, msg, msgdata, start_chain='built-in'):
# The rule matched so run its action.
if link.action is LinkAction.jump:
chain = config.chains[link.chain]
- chain_iter = iter(chain)
+ chain_iter = chain.get_links(mlist, msg, msgdata)
continue
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((chain, chain_iter))
chain = config.chains[link.chain]
- chain_iter = iter(chain)
+ chain_iter = chain.get_links(mlist, msg, msgdata)
continue
elif link.action is LinkAction.stop:
# Stop all processing.
diff --git a/Mailman/chains/accept.py b/Mailman/chains/accept.py
new file mode 100644
index 000000000..a6a9a45eb
--- /dev/null
+++ b/Mailman/chains/accept.py
@@ -0,0 +1,55 @@
+# 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
+# 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 terminal 'accept' chain."""
+
+__all__ = ['AcceptChain']
+__metaclass__ = type
+
+import logging
+
+from Mailman.chains.base import TerminalChainBase
+from Mailman.configuration import config
+from Mailman.i18n import _
+from Mailman.queue import Switchboard
+
+
+log = logging.getLogger('mailman.vette')
+SEMISPACE = '; '
+
+
+
+class AcceptChain(TerminalChainBase):
+ """Accept the message for posting."""
+
+ name = 'accept'
+ description = _('Accept a message.')
+
+ def _process(self, mlist, msg, msgdata):
+ """See `TerminalChainBase`."""
+ # Start by decorating the message with a header that contains a list
+ # of all the rules that matched. These metadata could be None or an
+ # empty list.
+ rule_hits = msgdata.get('rule_hits')
+ if rule_hits:
+ msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits)
+ rule_misses = msgdata.get('rule_misses')
+ if rule_misses:
+ msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
+ accept_queue = Switchboard(config.PREPQUEUE_DIR)
+ accept_queue.enqueue(msg, msgdata)
+ log.info('ACCEPT: %s', msg.get('message-id', 'n/a'))
diff --git a/Mailman/chains/base.py b/Mailman/chains/base.py
new file mode 100644
index 000000000..30b66b1cf
--- /dev/null
+++ b/Mailman/chains/base.py
@@ -0,0 +1,131 @@
+# Copyright (C) 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
+# 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.
+
+"""Base class for terminal chains."""
+
+__all__ = [
+ 'Chain',
+ 'Link',
+ 'TerminalChainBase',
+ ]
+__metaclass__ = type
+
+from zope.interface import implements
+
+from Mailman.configuration import config
+from Mailman.interfaces import (
+ IChain, IChainIterator, IChainLink, IMutableChain, LinkAction)
+
+
+
+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, IChainIterator)
+
+ def _process(self, mlist, msg, msgdata):
+ """Process the message for the given mailing list.
+
+ This must be overridden by subclasses.
+ """
+ raise NotImplementedError
+
+ def get_rule(self, name):
+ """See `IChain`.
+
+ This always returns the globally registered named rule.
+ """
+ return config.rules[name]
+
+ def get_links(self, mlist, msg, msgdata):
+ """See `IChain`."""
+ return iter(self)
+
+ def __iter__(self):
+ """See `IChainIterator`."""
+ # 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)
+
+
+
+class Chain:
+ """Generic chain base class."""
+ implements(IMutableChain)
+
+ def __init__(self, name, description):
+ assert name not in config.chains, 'Duplicate chain name: %s' % name
+ self.name = name
+ self.description = description
+ self._links = []
+ # Register the chain.
+ config.chains[name] = self
+
+ def append_link(self, link):
+ """See `IMutableChain`."""
+ self._links.append(link)
+
+ def flush(self):
+ """See `IMutableChain`."""
+ self._links = []
+
+ def get_rule(self, name):
+ """See `IChain`.
+
+ This always returns the globally registered named rule.
+ """
+ return config.rules[name]
+
+ def get_links(self, mlist, msg, msgdata):
+ """See `IChain`."""
+ return iter(ChainIterator(self))
+
+ def get_iterator(self):
+ """Return an iterator over the links."""
+ # We do it this way in order to preserve a separation of interfaces,
+ # and allows .get_links() to be overridden.
+ for link in self._links:
+ yield link
+
+
+
+class ChainIterator:
+ """Generic chain iterator."""
+
+ implements(IChainIterator)
+
+ def __init__(self, chain):
+ self._chain = chain
+
+ def __iter__(self):
+ """See `IChainIterator`."""
+ return self._chain.get_iterator()
diff --git a/Mailman/chains/builtin.py b/Mailman/chains/builtin.py
new file mode 100644
index 000000000..e5e97c3fb
--- /dev/null
+++ b/Mailman/chains/builtin.py
@@ -0,0 +1,59 @@
+# 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
+# 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 default built-in starting chain."""
+
+__all__ = ['BuiltInChain']
+__metaclass__ = type
+
+
+import logging
+
+from Mailman.interfaces import LinkAction
+from Mailman.chains.base import Chain, Link
+from Mailman.i18n import _
+
+
+log = logging.getLogger('mailman.vette')
+
+
+
+class BuiltInChain(Chain):
+ """Default built-in chain."""
+
+ def __init__(self):
+ super(BuiltInChain, self).__init__(
+ 'built-in', _('The built-in moderation chain.'))
+ self.append_link(Link('approved', LinkAction.jump, 'accept'))
+ self.append_link(Link('emergency', LinkAction.jump, 'hold'))
+ self.append_link(Link('loop', LinkAction.jump, 'discard'))
+ # Do all of the following before deciding whether to hold the message
+ # for moderation.
+ self.append_link(Link('administrivia', LinkAction.defer))
+ self.append_link(Link('implicit-dest', LinkAction.defer))
+ self.append_link(Link('max-recipients', LinkAction.defer))
+ self.append_link(Link('max-size', LinkAction.defer))
+ self.append_link(Link('news-moderation', LinkAction.defer))
+ self.append_link(Link('no-subject', LinkAction.defer))
+ self.append_link(Link('suspicious-header', LinkAction.defer))
+ # Now if any of the above hit, jump to the hold chain.
+ self.append_link(Link('any', LinkAction.jump, 'hold'))
+ # Take a detour through the self header matching chain, which we'll
+ # create later.
+ self.append_link(Link('truth', LinkAction.detour, 'header-match'))
+ # Finally, the builtin chain selfs to acceptance.
+ self.append_link(Link('truth', LinkAction.jump, 'accept'))
diff --git a/Mailman/chains/discard.py b/Mailman/chains/discard.py
new file mode 100644
index 000000000..d4640f260
--- /dev/null
+++ b/Mailman/chains/discard.py
@@ -0,0 +1,43 @@
+# 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
+# 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 terminal 'discard' chain."""
+
+__all__ = ['DiscardChain']
+__metaclass__ = type
+
+
+import logging
+
+from Mailman.chains.base import TerminalChainBase
+from Mailman.i18n import _
+
+
+log = logging.getLogger('mailman.vette')
+
+
+
+class DiscardChain(TerminalChainBase):
+ """Discard a message."""
+
+ name = 'discard'
+ description = _('Discard a message and stop processing.')
+
+ def _process(self, mlist, msg, msgdata):
+ """See `TerminalChainBase`."""
+ log.info('DISCARD: %s', msg.get('message-id', 'n/a'))
+ # Nothing more needs to happen.
diff --git a/Mailman/chains/headers.py b/Mailman/chains/headers.py
new file mode 100644
index 000000000..a802eaab4
--- /dev/null
+++ b/Mailman/chains/headers.py
@@ -0,0 +1,116 @@
+# 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
+# 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 header-matching chain."""
+
+__all__ = ['HeaderMatchChain']
+__metaclass__ = type
+
+
+import re
+import logging
+
+from zope.interface import implements
+
+from Mailman.interfaces import IRule, LinkAction
+from Mailman.chains.base import Chain, Link
+from Mailman.i18n import _
+from Mailman.configuration import config
+
+
+log = logging.getLogger('mailman.vette')
+
+
+
+class HeaderMatchRule:
+ """Header matching rule used by header-match chain."""
+ implements(IRule)
+
+ # Sequential rule counter.
+ _count = 1
+
+ def __init__(self, header, pattern):
+ self._header = header
+ self._pattern = pattern
+ self.name = 'header-match-%002d' % HeaderMatchRule._count
+ HeaderMatchRule._count += 1
+ self.description = u'%s: %s' % (header, pattern)
+ # XXX I think we should do better here, somehow recording that a
+ # particular header matched a particular pattern, but that gets ugly
+ # with RFC 2822 headers. It also doesn't match well with the rule
+ # name concept. For now, we just record the rather useless numeric
+ # rule name. I suppose we could do the better hit recording in the
+ # check() method, and set self.record = False.
+ self.record = True
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ for value in msg.get_all(self._header, []):
+ if re.search(self._pattern, value, re.IGNORECASE):
+ return True
+ return False
+
+
+
+class HeaderMatchChain(Chain):
+ """Default header matching chain.
+
+ This could be extended by header match rules in the database.
+ """
+
+ def __init__(self):
+ super(HeaderMatchChain, self).__init__(
+ 'header-match', _('The built-in header matching chain'))
+ # The header match rules are not global, so don't register them.
+ # These are the only rules that the header match chain can execute.
+ self._links = []
+ self._rules = {}
+ # Initialize header check rules with those from the global
+ # HEADER_MATCHES variable.
+ for entry in config.HEADER_MATCHES:
+ if len(entry) == 2:
+ header, pattern = entry
+ chain = 'hold'
+ elif len(entry) == 3:
+ header, pattern, chain = entry
+ # We don't assert that the chain exists here because the jump
+ # chain may not yet have been created.
+ else:
+ raise AssertionError(
+ 'Bad entry for HEADER_MATCHES: %s' % entry)
+ self.extend(header, pattern, chain)
+
+ def extend(self, header, pattern, chain='hold'):
+ """Extend the existing header matches.
+
+ :param header: The case-insensitive header field name.
+ :param pattern: The pattern to match the header's value again. The
+ match is not anchored and is done case-insensitively.
+ :param chain: Option chain to jump to if the pattern matches any of
+ the named header values. If not given, the 'hold' chain is used.
+ """
+ rule = HeaderMatchRule(header, pattern)
+ self._rules[rule.name] = rule
+ link = Link(rule.name, LinkAction.jump, chain)
+ self._links.append(link)
+
+ def get_rule(self, name):
+ """See `IChain`.
+
+ Only local rules are findable by this chain.
+ """
+ return self._rules[name]
diff --git a/Mailman/chains/hold.py b/Mailman/chains/hold.py
new file mode 100644
index 000000000..7d4c64d45
--- /dev/null
+++ b/Mailman/chains/hold.py
@@ -0,0 +1,177 @@
+# 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
+# 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 terminal 'hold' chain."""
+
+from __future__ import with_statement
+
+__all__ = ['HoldChain']
+__metaclass__ = type
+__i18n_templates__ = True
+
+
+import logging
+
+from email.mime.message import MIMEMessage
+from email.mime.text import MIMEText
+from email.utils import formatdate, make_msgid
+from zope.interface import implements
+
+from Mailman import i18n
+from Mailman.Message import UserNotification
+from Mailman.Utils import maketext, oneline, wrap, GetCharSet
+from Mailman.app.moderator import hold_message
+from Mailman.app.replybot import autorespond_to_sender, can_acknowledge
+from Mailman.chains.base import TerminalChainBase
+from Mailman.configuration import config
+from Mailman.interfaces import IPendable
+
+
+log = logging.getLogger('mailman.vette')
+SEMISPACE = '; '
+_ = i18n._
+
+
+
+class HeldMessagePendable(dict):
+ implements(IPendable)
+ PEND_KEY = 'held message'
+
+
+
+class HoldChain(TerminalChainBase):
+ """Hold a message."""
+
+ name = 'hold'
+ description = _('Hold a message and stop processing.')
+
+ def _process(self, mlist, msg, msgdata):
+ """See `TerminalChainBase`."""
+ # Start by decorating the message with a header that contains a list
+ # of all the rules that matched. These metadata could be None or an
+ # empty list.
+ rule_hits = msgdata.get('rule_hits')
+ if rule_hits:
+ msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits)
+ rule_misses = msgdata.get('rule_misses')
+ if rule_misses:
+ msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
+ # 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.
+ sender = msg.get_sender()
+ 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,
+ 'sender' : sender,
+ 'reason' : 'XXX', #reason,
+ 'confirmurl' : '%s/%s' % (mlist.script_url('confirm'), token),
+ 'admindb_url': mlist.script_url('admindb'),
+ }
+ # 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')
+ send_language = msgdata.get('lang', language)
+ text = maketext('postheld.txt', substitutions,
+ lang=send_language, mlist=mlist)
+ adminaddr = mlist.bounces_address
+ nmsg = UserNotification(sender, adminaddr, subject, text,
+ send_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) # XXX reason
+ substitutions['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', substitutions,
+ 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(language))
+ dmsg['Subject'] = 'confirm ' + token
+ dmsg['Sender'] = mlist.request_address
+ dmsg['From'] = mlist.request_address
+ dmsg['Date'] = formatdate(localtime=True)
+ dmsg['Message-ID'] = make_msgid()
+ nmsg.attach(text)
+ nmsg.attach(MIMEMessage(msg))
+ nmsg.attach(MIMEMessage(dmsg))
+ nmsg.send(mlist, **{'tomoderators': 1})
+ # Log the held message
+ # XXX reason
+ reason = 'n/a'
+ log.info('HOLD: %s post from %s held, message-id=%s: %s',
+ mlist.fqdn_listname, sender,
+ msg.get('message-id', 'n/a'), reason)
diff --git a/Mailman/chains/reject.py b/Mailman/chains/reject.py
new file mode 100644
index 000000000..ffec2bdc9
--- /dev/null
+++ b/Mailman/chains/reject.py
@@ -0,0 +1,55 @@
+# 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
+# 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 terminal 'reject' chain."""
+
+__all__ = ['RejectChain']
+__metaclass__ = type
+
+
+import logging
+
+from Mailman.app.bounces import bounce_message
+from Mailman.chains.base import TerminalChainBase
+from Mailman.i18n import _
+
+
+log = logging.getLogger('mailman.vette')
+SEMISPACE = '; '
+
+
+
+class RejectChain(TerminalChainBase):
+ """Reject/bounce a message."""
+
+ name = 'reject'
+ description = _('Reject/bounce a message and stop processing.')
+
+ def _process(self, mlist, msg, msgdata):
+ """See `TerminalChainBase`."""
+ # Start by decorating the message with a header that contains a list
+ # of all the rules that matched. These metadata could be None or an
+ # empty list.
+ rule_hits = msgdata.get('rule_hits')
+ if rule_hits:
+ msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits)
+ rule_misses = msgdata.get('rule_misses')
+ if rule_misses:
+ msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
+ # XXX Exception/reason
+ bounce_message(mlist, msg)
+ log.info('REJECT: %s', msg.get('message-id', 'n/a'))
diff --git a/Mailman/interfaces/chain.py b/Mailman/interfaces/chain.py
index eca663b30..63d7eb7f7 100644
--- a/Mailman/interfaces/chain.py
+++ b/Mailman/interfaces/chain.py
@@ -63,12 +63,6 @@ class IChain(Interface):
name = Attribute('Chain name; must be unique.')
description = Attribute('A brief description of the chain.')
- def __iter__():
- """Iterate over all the IChainLinks in this chain.
-
- :return: an IChainLink.
- """
-
def get_rule(name):
"""Lookup and return the named rule.
@@ -79,6 +73,26 @@ class IChain(Interface):
:raises: KeyError if the named rule cannot be found.
"""
+ def get_links(mlist, msg, msgdata):
+ """Get an `IChainIterator` for processing.
+
+ :param mlist: the IMailingList object
+ :param msg: the message being processed
+ :param msgdata: the message metadata dictionary
+ :return: An `IChainIterator`.
+ """
+
+
+
+class IChainIterator(Interface):
+ """An iterator over chain rules."""
+
+ def __iter__():
+ """Iterate over all the IChainLinks in this chain.
+
+ :return: an IChainLink.
+ """
+
class IMutableChain(IChain):