diff options
| author | Barry Warsaw | 2008-01-30 23:58:52 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2008-01-30 23:58:52 -0500 |
| commit | c1cc921b691eb60445cf28bc66a59b02b3cd09a4 (patch) | |
| tree | 3af37f53607a37a945ffbf81ce21454d8e9fc776 | |
| parent | 7b853f27c34c2a168b5dbbf796b84517a20c6191 (diff) | |
| download | mailman-c1cc921b691eb60445cf28bc66a59b02b3cd09a4.tar.gz mailman-c1cc921b691eb60445cf28bc66a59b02b3cd09a4.tar.zst mailman-c1cc921b691eb60445cf28bc66a59b02b3cd09a4.zip | |
| -rw-r--r-- | Mailman/app/chains.py | 6 | ||||
| -rw-r--r-- | Mailman/chains/accept.py | 55 | ||||
| -rw-r--r-- | Mailman/chains/base.py | 131 | ||||
| -rw-r--r-- | Mailman/chains/builtin.py | 59 | ||||
| -rw-r--r-- | Mailman/chains/discard.py | 43 | ||||
| -rw-r--r-- | Mailman/chains/headers.py | 116 | ||||
| -rw-r--r-- | Mailman/chains/hold.py | 177 | ||||
| -rw-r--r-- | Mailman/chains/reject.py | 55 | ||||
| -rw-r--r-- | Mailman/interfaces/chain.py | 26 |
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): |
