diff options
| author | Barry Warsaw | 2008-01-21 00:26:55 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2008-01-21 00:26:55 -0500 |
| commit | 4460aad316db5c8af9b84c392e67441acaac9d72 (patch) | |
| tree | eabc714fdc3c9aff16ca48cfbc5f7974af01e1c1 | |
| parent | 2efcac1ef273b407668826c587c15a0fd8ec3d3c (diff) | |
| download | mailman-4460aad316db5c8af9b84c392e67441acaac9d72.tar.gz mailman-4460aad316db5c8af9b84c392e67441acaac9d72.tar.zst mailman-4460aad316db5c8af9b84c392e67441acaac9d72.zip | |
| -rw-r--r-- | Mailman/Handlers/Decorate.py | 1 | ||||
| -rw-r--r-- | Mailman/Handlers/Emergency.py | 37 | ||||
| -rw-r--r-- | Mailman/app/chains.py | 91 | ||||
| -rw-r--r-- | Mailman/app/moderator.py | 4 | ||||
| -rw-r--r-- | Mailman/app/replybot.py | 4 | ||||
| -rw-r--r-- | Mailman/app/rules.py | 8 | ||||
| -rw-r--r-- | Mailman/database/pending.py | 9 | ||||
| -rw-r--r-- | Mailman/docs/chains.txt | 338 | ||||
| -rw-r--r-- | Mailman/docs/hold.txt | 223 | ||||
| -rw-r--r-- | Mailman/docs/requests.txt | 2 | ||||
| -rw-r--r-- | Mailman/initialize.py | 7 | ||||
| -rw-r--r-- | Mailman/rules/docs/administrivia.txt | 3 | ||||
| -rw-r--r-- | Mailman/rules/docs/approve.txt | 3 | ||||
| -rw-r--r-- | Mailman/rules/docs/emergency.txt | 77 | ||||
| -rw-r--r-- | Mailman/rules/docs/implicit-dest.txt | 3 | ||||
| -rw-r--r-- | Mailman/rules/docs/loop.txt | 3 | ||||
| -rw-r--r-- | Mailman/rules/docs/max-size.txt | 3 | ||||
| -rw-r--r-- | Mailman/rules/docs/moderation.txt | 5 | ||||
| -rw-r--r-- | Mailman/rules/docs/news-moderation.txt | 3 | ||||
| -rw-r--r-- | Mailman/rules/docs/no-subject.txt | 3 | ||||
| -rw-r--r-- | Mailman/rules/docs/recipients.txt | 3 | ||||
| -rw-r--r-- | Mailman/rules/docs/rules.txt | 96 | ||||
| -rw-r--r-- | Mailman/rules/docs/suspicious.txt | 3 | ||||
| -rw-r--r-- | Mailman/rules/emergency.py | 2 |
24 files changed, 546 insertions, 385 deletions
diff --git a/Mailman/Handlers/Decorate.py b/Mailman/Handlers/Decorate.py index 984cd9670..6891fed8d 100644 --- a/Mailman/Handlers/Decorate.py +++ b/Mailman/Handlers/Decorate.py @@ -57,6 +57,7 @@ def process(mlist, msg, msgdata): except Errors.NotAMemberError: pass # These strings are descriptive for the log file and shouldn't be i18n'd + d.update(msgdata.get('decoration-data', {})) header = decorate(mlist, mlist.msg_header, d) footer = decorate(mlist, mlist.msg_footer, d) # Escape hatch if both the footer and header are empty diff --git a/Mailman/Handlers/Emergency.py b/Mailman/Handlers/Emergency.py deleted file mode 100644 index a282da621..000000000 --- a/Mailman/Handlers/Emergency.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (C) 2002-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. - -"""Put an emergency hold on all messages otherwise approved. - -No notices are sent to either the sender or the list owner for emergency -holds. I think they'd be too obnoxious. -""" - -from Mailman import Errors -from Mailman.i18n import _ - - - -class EmergencyHold(Errors.HoldMessage): - reason = _('Emergency hold on all list traffic is in effect') - rejection = _('Your message was deemed inappropriate by the moderator.') - - - -def process(mlist, msg, msgdata): - if mlist.emergency and not msgdata.get('adminapproved'): - mlist.HoldMessage(msg, _(EmergencyHold.reason), msgdata) - raise EmergencyHold diff --git a/Mailman/app/chains.py b/Mailman/app/chains.py index cb5dfafb7..8ac9e9ce7 100644 --- a/Mailman/app/chains.py +++ b/Mailman/app/chains.py @@ -26,10 +26,16 @@ __all__ = [ 'RejectChain', ] __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 @@ -77,8 +83,14 @@ class HoldChain: 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']) + # 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) @@ -90,6 +102,7 @@ class HoldChain: # 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) @@ -101,12 +114,13 @@ class HoldChain: 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), + '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'), } - 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 @@ -129,10 +143,12 @@ class HoldChain: # posting was held. subject = _( 'Your message to $mlist.fqdn_listname awaits moderator approval') - language = msgdata.get('lang', lang) + send_language = msgdata.get('lang', language) text = maketext('postheld.txt', substitutions, - lang=language, mlist=mlist) - nmsg = UserNotification(sender, adminaddr, subject, text, language) + 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 @@ -145,8 +161,8 @@ class HoldChain: charset = GetCharSet(language) # We need to regenerate or re-translate a few values in the # substitution dictionary. - d['reason'] = _(reason) - d['subject'] = original_subject + #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') @@ -155,7 +171,7 @@ class HoldChain: subject, lang=language) nmsg.set_type('multipart/mixed') text = MIMEText( - maketext('postauth.txt', substitution, + maketext('postauth.txt', substitutions, raw=True, mlist=mlist), _charset=charset) dmsg = MIMEText(wrap(_("""\ @@ -164,19 +180,22 @@ 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)) + _charset=GetCharSet(language)) dmsg['Subject'] = 'confirm ' + token - dmsg['Sender'] = requestaddr - dmsg['From'] = requestaddr - dmsg['Date'] = email.utils.formatdate(localtime=True) - dmsg['Message-ID'] = email.utils.make_msgid() + 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 - log.info('HELD: %s post from %s held, message-id=%s: %s', - listname, sender, message_id, reason) + # 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) @@ -189,6 +208,15 @@ class RejectChain: 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. 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')) @@ -204,6 +232,15 @@ class AcceptChain: 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. 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')) @@ -224,9 +261,12 @@ class Chain: 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`.""" @@ -238,23 +278,26 @@ class Chain: def process(self, mlist, msg, msgdata): """See `IMutableChain`.""" - msgdata['rules'] = rules = [] + msgdata['rule_hits'] = hits = [] + msgdata['rule_misses'] = misses = [] 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. + # 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): - rules.append(link.rule.name) + hits.append(link.rule) # None is a special jump meaning "keep processing this chain". if link.jump is not None: jump = link.jump break + else: + misses.append(link.rule) 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 @@ -280,7 +323,7 @@ def initialize(): '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 = 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')) diff --git a/Mailman/app/moderator.py b/Mailman/app/moderator.py index 956dfb773..29577a130 100644 --- a/Mailman/app/moderator.py +++ b/Mailman/app/moderator.py @@ -127,10 +127,8 @@ def handle_message(mlist, id, action, if key.startswith('_mod_'): del msgdata[key] # Add some metadata to indicate this message has now been approved. - # XXX 'adminapproved' is used for backward compatibility, but it - # should really be called 'moderator_approved'. msgdata['approved'] = True - msgdata['adminapproved'] = True + msgdata['moderator_approved'] = True # Calculate a new filebase for the approved message, otherwise # delivery errors will cause duplicates. if 'filebase' in msgdata: diff --git a/Mailman/app/replybot.py b/Mailman/app/replybot.py index d1bc9c487..62d442e82 100644 --- a/Mailman/app/replybot.py +++ b/Mailman/app/replybot.py @@ -109,14 +109,14 @@ def can_acknowledge(msg): (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: + for header in msg.keys(): 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' + if msg.get('auto-submitted', 'no').lower() <> 'no': return False if msg.get('return-path') == '<>': return False diff --git a/Mailman/app/rules.py b/Mailman/app/rules.py index 948ee7dd7..a3846541e 100644 --- a/Mailman/app/rules.py +++ b/Mailman/app/rules.py @@ -21,15 +21,21 @@ __all__ = [ 'initialize', ] + +from zope.interface.verify import verifyObject + from Mailman.app.plugins import get_plugins from Mailman.configuration import config +from Mailman.interfaces import IRule def initialize(): """Find and register all rules in all plugins.""" for rule_finder in get_plugins('mailman.rules'): - for rule in rule_finder(): + for rule_class in rule_finder(): + rule = rule_class() + verifyObject(IRule, rule) 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/database/pending.py b/Mailman/database/pending.py index 1a272391d..c3f54c814 100644 --- a/Mailman/database/pending.py +++ b/Mailman/database/pending.py @@ -112,6 +112,10 @@ class Pendings(object): value = u'__builtin__.float\1%s' % value elif type(value) is bool: value = u'__builtin__.bool\1%s' % value + elif type(value) is list: + # We expect this to be a list of strings. + value = u'Mailman.database.pending.unpack_list\1%s' % ( + '\2'.join(value)) keyval = PendedKeyValue(key=key, value=value) pending.key_values.add(keyval) config.db.store.add(pending) @@ -156,3 +160,8 @@ class Pendings(object): for keyvalue in q: store.remove(keyvalue) store.remove(pending) + + + +def unpack_list(value): + return value.split('\2') diff --git a/Mailman/docs/chains.txt b/Mailman/docs/chains.txt new file mode 100644 index 000000000..1b22b9923 --- /dev/null +++ b/Mailman/docs/chains.txt @@ -0,0 +1,338 @@ +Chains +====== + +When a new message comes into the system, Mailman uses a set of rule chains to +decide whether the message gets posted to the list, rejected, discarded, or +held for moderator approval. + +There are a number of built-in chains available that act as end-points in the +processing of messages. + + +The Discard chain +----------------- + +The Discard chain simply throws the message away. + + >>> from zope.interface.verify import verifyObject + >>> from Mailman.configuration import config + >>> from Mailman.interfaces import IChain + >>> chain = config.chains['discard'] + >>> verifyObject(IChain, chain) + True + >>> chain.name + 'discard' + >>> chain.description + u'Discard a message and stop processing.' + + >>> from Mailman.app.lifecycle import create_list + >>> mlist = create_list(u'_xtest@example.com') + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: _xtest@example.com + ... Subject: My first post + ... Message-ID: <first> + ... + ... An important message. + ... """) + + # XXX This checks the vette log file because there is no other evidence + # that this chain has done anything. + >>> import os + >>> fp = open(os.path.join(config.LOG_DIR, 'vette')) + >>> file_pos = fp.tell() + >>> chain.process(mlist, msg, {}) + >>> fp.seek(file_pos) + >>> print 'LOG:', fp.read() + LOG: ... DISCARD: <first> + <BLANKLINE> + + +The Reject chain +---------------- + +The Reject chain bounces the message back to the original sender, and logs +this action. + + >>> chain = config.chains['reject'] + >>> verifyObject(IChain, chain) + True + >>> chain.name + 'reject' + >>> chain.description + u'Reject/bounce a message and stop processing.' + >>> file_pos = fp.tell() + >>> chain.process(mlist, msg, {}) + >>> fp.seek(file_pos) + >>> print 'LOG:', fp.read() + LOG: ... REJECT: <first> + +The bounce message is now sitting in the Virgin queue. + + >>> from Mailman.queue import Switchboard + >>> virginq = Switchboard(config.VIRGINQUEUE_DIR) + >>> len(virginq.files) + 1 + >>> qmsg, qdata = virginq.dequeue(virginq.files[0]) + >>> print qmsg.as_string() + Subject: My first post + From: _xtest-owner@example.com + To: aperson@example.com + ... + [No bounce details are available] + ... + Content-Type: message/rfc822 + MIME-Version: 1.0 + <BLANKLINE> + From: aperson@example.com + To: _xtest@example.com + Subject: My first post + Message-ID: <first> + <BLANKLINE> + An important message. + <BLANKLINE> + ... + + +The Hold Chain +-------------- + +The Hold chain places the message into the admin request database and +depending on the list's settings, sends a notification to both the original +sender and the list moderators. + + >>> mlist.web_page_url = u'http://www.example.com/' + >>> chain = config.chains['hold'] + >>> verifyObject(IChain, chain) + True + >>> chain.name + 'hold' + >>> chain.description + u'Hold a message and stop processing.' + + >>> file_pos = fp.tell() + >>> chain.process(mlist, msg, {}) + >>> fp.seek(file_pos) + >>> print 'LOG:', fp.read() + LOG: ... HOLD: _xtest@example.com post from aperson@example.com held, + message-id=<first>: n/a + <BLANKLINE> + +There are now two messages in the Virgin queue, one to the list moderators and +one to the original author. + + >>> len(virginq.files) + 2 + >>> qfiles = [] + >>> for filebase in virginq.files: + ... qmsg, qdata = virginq.dequeue(filebase) + ... virginq.finish(filebase) + ... qfiles.append(qmsg) + >>> from operator import itemgetter + >>> qfiles.sort(key=itemgetter('to')) + +This message is addressed to the mailing list moderators. + + >>> print qfiles[0].as_string() + Subject: _xtest@example.com post from aperson@example.com requires approval + From: _xtest-owner@example.com + To: _xtest-owner@example.com + MIME-Version: 1.0 + ... + As list administrator, your authorization is requested for the + following mailing list posting: + <BLANKLINE> + List: _xtest@example.com + From: aperson@example.com + Subject: My first post + Reason: XXX + <BLANKLINE> + At your convenience, visit: + <BLANKLINE> + http://www.example.com/admindb/_xtest@example.com + <BLANKLINE> + to approve or deny the request. + <BLANKLINE> + ... + Content-Type: message/rfc822 + MIME-Version: 1.0 + <BLANKLINE> + From: aperson@example.com + To: _xtest@example.com + Subject: My first post + Message-ID: <first> + X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW + <BLANKLINE> + An important message. + <BLANKLINE> + ... + Content-Type: message/rfc822 + MIME-Version: 1.0 + <BLANKLINE> + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Subject: confirm ... + Sender: _xtest-request@example.com + From: _xtest-request@example.com + ... + <BLANKLINE> + 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. + ... + +This message is addressed to the sender of the message. + + >>> print qfiles[1].as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: Your message to _xtest@example.com awaits moderator approval + From: _xtest-bounces@example.com + To: aperson@example.com + ... + Your mail to '_xtest@example.com' with the subject + <BLANKLINE> + My first post + <BLANKLINE> + Is being held until the list moderator can review it for approval. + <BLANKLINE> + The reason it is being held: + <BLANKLINE> + XXX + <BLANKLINE> + Either the message will get posted to the list, or you will receive + notification of the moderator's decision. If you would like to cancel + this posting, please visit the following URL: + <BLANKLINE> + http://www.example.com/confirm/_xtest@example.com/... + <BLANKLINE> + <BLANKLINE> + +In addition, the pending database is holding the original messages, waiting +for them to be disposed of by the original author or the list moderators. The +database is essentially a dictionary, with the keys being the randomly +selected tokens included in the urls and the values being a 2-tuple where the +first item is a type code and the second item is a message id. + + >>> import re + >>> cookie = None + >>> for line in qfiles[1].get_payload().splitlines(): + ... mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line) + ... if mo: + ... cookie = mo.group('cookie') + ... break + >>> assert cookie is not None, 'No confirmation token found' + >>> data = config.db.pendings.confirm(cookie) + >>> sorted(data.items()) + [(u'id', ...), (u'type', u'held message')] + +The message itself is held in the message store. + + >>> rkey, rdata = config.db.requests.get_list_requests(mlist).get_request( + ... data['id']) + >>> msg = config.db.message_store.get_message_by_id( + ... rdata['_mod_message_id']) + >>> print msg.as_string() + From: aperson@example.com + To: _xtest@example.com + Subject: My first post + Message-ID: <first> + X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW + <BLANKLINE> + An important message. + <BLANKLINE> + + +The Accept chain +---------------- + +The Accept chain sends the message on the 'prep' queue, where it will be +processed and sent on to the list membership. + + >>> chain = config.chains['accept'] + >>> verifyObject(IChain, chain) + True + >>> chain.name + 'accept' + >>> chain.description + u'Accept a message.' + >>> file_pos = fp.tell() + >>> chain.process(mlist, msg, {}) + >>> fp.seek(file_pos) + >>> print 'LOG:', fp.read() + LOG: ... ACCEPT: <first> + + >>> prepq = Switchboard(config.PREPQUEUE_DIR) + >>> len(prepq.files) + 1 + >>> qmsg, qdata = prepq.dequeue(prepq.files[0]) + >>> print qmsg.as_string() + From: aperson@example.com + To: _xtest@example.com + Subject: My first post + Message-ID: <first> + X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW + <BLANKLINE> + An important message. + <BLANKLINE> + + +Run-time chains +--------------- + +We can also define chains at run time, and these chains can be mutated. +Run-time chains are made up of links where each link associates both a rule +and a 'jump'. The rule is really a rule name, which is looked up when +needed. The jump names a chain which is jumped to if the rule matches. + +There is one built-in run-time chain, called appropriately 'built-in'. This +is the default chain to use when no other input chain is defined for a mailing +list. It runs through the default rules, providing functionality similar to +the Hold handler from previous versions of Mailman. + + >>> chain = config.chains['built-in'] + >>> verifyObject(IChain, chain) + True + >>> from Mailman.interfaces import IMutableChain + >>> verifyObject(IMutableChain, chain) + True + >>> chain.name + 'built-in' + >>> chain.description + u'The built-in moderation chain.' + +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, {}) + >>> fp.seek(file_pos) + >>> print 'LOG:', fp.read() + LOG: ... ACCEPT: <first> + + >>> qmsg, qdata = prepq.dequeue(prepq.files[0]) + >>> print qmsg.as_string() + From: aperson@example.com + To: _xtest@example.com + Subject: My first post + 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 + <BLANKLINE> + An important message. + <BLANKLINE> + +In addition, the message metadata now contains lists of all rules that have +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'] diff --git a/Mailman/docs/hold.txt b/Mailman/docs/hold.txt deleted file mode 100644 index 1b8ecea59..000000000 --- a/Mailman/docs/hold.txt +++ /dev/null @@ -1,223 +0,0 @@ -Holding messages -================ - -One of the most important functions of Mailman is to moderate messages by -holding some for approval before they will post to the mailing list. Messages -are held when they meet any of a number of criteria. - - >>> import os - >>> import errno - >>> from Mailman.Handlers.Hold import process - >>> from Mailman.queue import Switchboard - >>> from Mailman.configuration import config - >>> mlist = config.db.list_manager.create(u'_xtest@example.com') - >>> mlist.preferred_language = u'en' - >>> mlist.real_name = u'_XTest' - >>> # XXX This will almost certainly change once we've worked out the web - >>> # space layout for mailing lists now. - >>> mlist.web_page_url = u'http://lists.example.com/' - -Here's a helper function used when we don't care about what's in the virgin -queue or in the pending database. - - >>> switchboard = Switchboard(config.VIRGINQUEUE_DIR) - >>> def clear(): - ... for filebase in switchboard.files: - ... msg, msgdata = switchboard.dequeue(filebase) - ... switchboard.finish(filebase) - ... for holdfile in os.listdir(config.DATA_DIR): - ... if holdfile.startswith('heldmsg-'): - ... os.unlink(os.path.join(config.DATA_DIR, holdfile)) - ... try: - ... os.unlink(os.path.join(config.DATA_DIR, 'pending.db')) - ... except OSError, e: - ... if e.errno <> errno.ENOENT: - ... raise - - -Short circuiting ----------------- - -If the message metadata indicates that the message is pre-approved, then the -handler returns immediately. - - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... - ... An important message. - ... """) - >>> msgdata = {'approved': True} - >>> process(mlist, msg, msgdata) - >>> print msg.as_string() - From: aperson@example.com - <BLANKLINE> - An important message. - <BLANKLINE> - >>> msgdata - {'approved': True} - - - - -X Hold Notifications -X ------------------ -X -X Whenever Mailman holds a message, it sends notifications both to the list -X owner and to the original sender, as long as it is configured to do so. We -X can show this by first holding a message. -X -X >>> mlist.respond_to_post_requests = True -X >>> mlist.admin_immed_notify = True -X >>> msg = message_from_string("""\ -X ... From: aperson@example.com -X ... -X ... """) -X >>> process(mlist, msg, {}) -X Traceback (most recent call last): -X ... -X ImplicitDestination -X -X There should be two messages in the virgin queue, one to the list owner and -X one to the original author. -X -X >>> len(switchboard.files) -X 2 -X >>> qfiles = {} -X >>> for filebase in switchboard.files: -X ... qmsg, qdata = switchboard.dequeue(filebase) -X ... switchboard.finish(filebase) -X ... qfiles[qmsg['to']] = qmsg, qdata -X >>> qmsg, qdata = qfiles['_xtest-owner@example.com'] -X >>> print qmsg.as_string() -X Subject: _xtest post from aperson@example.com requires approval -X From: _xtest-owner@example.com -X To: _xtest-owner@example.com -X MIME-Version: 1.0 -X Content-Type: multipart/mixed; boundary="..." -X Message-ID: ... -X Date: ... -X Precedence: bulk -X <BLANKLINE> -X --... -X Content-Type: text/plain; charset="us-ascii" -X MIME-Version: 1.0 -X Content-Transfer-Encoding: 7bit -X <BLANKLINE> -X As list administrator, your authorization is requested for the -X following mailing list posting: -X <BLANKLINE> -X List: _xtest@example.com -X From: aperson@example.com -X Subject: (no subject) -X Reason: Message has implicit destination -X <BLANKLINE> -X At your convenience, visit: -X <BLANKLINE> -X http://lists.example.com/admindb/_xtest@example.com -X <BLANKLINE> -X to approve or deny the request. -X <BLANKLINE> -X --... -X Content-Type: message/rfc822 -X MIME-Version: 1.0 -X <BLANKLINE> -X From: aperson@example.com -X Message-ID: ... -X X-Message-ID-Hash: ... -X <BLANKLINE> -X <BLANKLINE> -X --... -X Content-Type: message/rfc822 -X MIME-Version: 1.0 -X <BLANKLINE> -X Content-Type: text/plain; charset="us-ascii" -X MIME-Version: 1.0 -X Content-Transfer-Encoding: 7bit -X Subject: confirm ... -X Sender: _xtest-request@example.com -X From: _xtest-request@example.com -X Date: ... -X Message-ID: ... -X <BLANKLINE> -X If you reply to this message, keeping the Subject: header intact, -X Mailman will discard the held message. Do this if the message is -X spam. If you reply to this message and include an Approved: header -X with the list password in it, the message will be approved for posting -X to the list. The Approved: header can also appear in the first line -X of the body of the reply. -X --... -X >>> sorted(qdata.items()) -X [('_parsemsg', False), ('listname', u'_xtest@example.com'), -X ('nodecorate', True), ('received_time', ...), -X ('recips', [u'_xtest-owner@example.com']), -X ('reduced_list_headers', True), -X ('tomoderators', 1), ('version', 3)] -X >>> qmsg, qdata = qfiles['aperson@example.com'] -X >>> print qmsg.as_string() -X MIME-Version: 1.0 -X Content-Type: text/plain; charset="us-ascii" -X Content-Transfer-Encoding: 7bit -X Subject: Your message to _xtest awaits moderator approval -X From: _xtest-bounces@example.com -X To: aperson@example.com -X Message-ID: ... -X Date: ... -X Precedence: bulk -X <BLANKLINE> -X Your mail to '_xtest' with the subject -X <BLANKLINE> -X (no subject) -X <BLANKLINE> -X Is being held until the list moderator can review it for approval. -X <BLANKLINE> -X The reason it is being held: -X <BLANKLINE> -X Message has implicit destination -X <BLANKLINE> -X Either the message will get posted to the list, or you will receive -X notification of the moderator's decision. If you would like to cancel -X this posting, please visit the following URL: -X <BLANKLINE> -X http://lists.example.com/confirm/_xtest@example.com/... -X <BLANKLINE> -X <BLANKLINE> -X >>> sorted(qdata.items()) -X [('_parsemsg', False), ('listname', u'_xtest@example.com'), -X ('nodecorate', True), ('received_time', ...), -X ('recips', [u'aperson@example.com']), -X ('reduced_list_headers', True), ('version', 3)] -X -X In addition, the pending database is holding the original messages, waiting -X for them to be disposed of by the original author or the list moderators. The -X database is essentially a dictionary, with the keys being the randomly -X selected tokens included in the urls and the values being a 2-tuple where the -X first item is a type code and the second item is a message id. -X -X >>> import re -X >>> cookie = None -X >>> qmsg, qdata = qfiles['aperson@example.com'] -X >>> for line in qmsg.get_payload().splitlines(): -X ... mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line) -X ... if mo: -X ... cookie = mo.group('cookie') -X ... break -X >>> data = config.db.pendings.confirm(cookie) -X >>> sorted(data.items()) -X [(u'id', ...), (u'type', u'held message')] -X -X The message itself is held in the message store. -X -X >>> rkey, rdata = config.db.requests.get_list_requests(mlist).get_request( -X ... data['id']) -X >>> msg = config.db.message_store.get_message_by_id( -X ... rdata['_mod_message_id']) -X >>> print msg.as_string() -X From: aperson@example.com -X Message-ID: ... -X X-Message-ID-Hash: ... -X <BLANKLINE> -X <BLANKLINE> -X -X Clean up. -X -X >>> clear() diff --git a/Mailman/docs/requests.txt b/Mailman/docs/requests.txt index ea4dcc75d..6f7dd2f14 100644 --- a/Mailman/docs/requests.txt +++ b/Mailman/docs/requests.txt @@ -318,7 +318,7 @@ indicates that the message has been approved. <BLANKLINE> >>> sorted(qdata.items()) [('_parsemsg', False), - ('adminapproved', True), (u'approved', True), + (u'approved', True), ('moderator_approved', True), (u'received_time', 123.45), (u'sender', u'aperson@example.com'), ('version', 3)] diff --git a/Mailman/initialize.py b/Mailman/initialize.py index 7e191a9d2..3577ebad6 100644 --- a/Mailman/initialize.py +++ b/Mailman/initialize.py @@ -31,9 +31,7 @@ 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 @@ -65,7 +63,10 @@ def initialize_2(debug=False): verifyObject(IDatabase, database) database.initialize(debug) Mailman.configuration.config.db = database - # Initialize the rules and chains. + # Initialize the rules and chains. Do the imports here so as to avoid + # circular imports. + from Mailman.app.chains import initialize as initialize_chains + from Mailman.app.rules import initialize as initialize_rules initialize_rules() initialize_chains() diff --git a/Mailman/rules/docs/administrivia.txt b/Mailman/rules/docs/administrivia.txt index 0e48fdd1b..de802fa85 100644 --- a/Mailman/rules/docs/administrivia.txt +++ b/Mailman/rules/docs/administrivia.txt @@ -9,8 +9,7 @@ used to catch messages posted to the list which should have been sent to the >>> from Mailman.configuration import config >>> mlist = config.db.list_manager.create(u'_xtest@example.com') >>> mlist.administrivia = True - >>> from Mailman.app.rules import find_rule - >>> rule = find_rule('administrivia') + >>> rule = config.rules['administrivia'] >>> rule.name 'administrivia' diff --git a/Mailman/rules/docs/approve.txt b/Mailman/rules/docs/approve.txt index ea07058f8..32367a76b 100644 --- a/Mailman/rules/docs/approve.txt +++ b/Mailman/rules/docs/approve.txt @@ -20,8 +20,7 @@ which is shared among all the administrators. The 'approved' rule determines whether the message contains the proper approval or not. - >>> from Mailman.app.rules import find_rule - >>> rule = find_rule('approved') + >>> rule = config.rules['approved'] >>> rule.name 'approved' diff --git a/Mailman/rules/docs/emergency.txt b/Mailman/rules/docs/emergency.txt new file mode 100644 index 000000000..685d7bcb6 --- /dev/null +++ b/Mailman/rules/docs/emergency.txt @@ -0,0 +1,77 @@ +Emergency +========= + +When the mailing list has its emergency flag set, all messages posted to the +list are held for moderator approval. + + >>> from Mailman.app.lifecycle import create_list + >>> mlist = create_list(u'_xtest@example.com') + >>> mlist.web_page_url = u'http://www.example.com/' + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: _xtest@example.com + ... Subject: My first post + ... Message-ID: <first> + ... + ... 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. + + >>> mlist.emergency = True + >>> chain.process(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 +pending requests. + + >>> from Mailman.queue import Switchboard + >>> virginq = Switchboard(config.VIRGINQUEUE_DIR) + + >>> def get_held_message(): + ... import re + ... qfiles = [] + ... for filebase in virginq.files: + ... qmsg, qdata = virginq.dequeue(filebase) + ... virginq.finish(filebase) + ... qfiles.append(qmsg) + ... from operator import itemgetter + ... qfiles.sort(key=itemgetter('to')) + ... cookie = None + ... for line in qfiles[1].get_payload().splitlines(): + ... mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line) + ... if mo: + ... cookie = mo.group('cookie') + ... break + ... assert cookie is not None, 'No confirmation token found' + ... data = config.db.pendings.confirm(cookie) + ... requestdb = config.db.requests.get_list_requests(mlist) + ... rkey, rdata = requestdb.get_request(data['id']) + ... return config.db.message_store.get_message_by_id( + ... rdata['_mod_message_id']) + + >>> msg = get_held_message() + >>> print msg.as_string() + From: aperson@example.com + To: _xtest@example.com + Subject: My first post + Message-ID: <first> + X-Mailman-Rule-Hits: emergency + X-Mailman-Rule-Misses: approved + X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW + <BLANKLINE> + An important message. + <BLANKLINE> + +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)) + >>> len(virginq.files) + 0 diff --git a/Mailman/rules/docs/implicit-dest.txt b/Mailman/rules/docs/implicit-dest.txt index b6fed2769..5a7f06c0c 100644 --- a/Mailman/rules/docs/implicit-dest.txt +++ b/Mailman/rules/docs/implicit-dest.txt @@ -6,8 +6,7 @@ not explicitly mentioned in the set of message recipients. >>> from Mailman.configuration import config >>> mlist = config.db.list_manager.create(u'_xtest@example.com') - >>> from Mailman.app.rules import find_rule - >>> rule = find_rule('implicit-dest') + >>> rule = config.rules['implicit-dest'] >>> rule.name 'implicit-dest' diff --git a/Mailman/rules/docs/loop.txt b/Mailman/rules/docs/loop.txt index 3174805b9..8fe86cf45 100644 --- a/Mailman/rules/docs/loop.txt +++ b/Mailman/rules/docs/loop.txt @@ -6,8 +6,7 @@ X-BeenThere header with the value of the list's posting address. >>> from Mailman.configuration import config >>> mlist = config.db.list_manager.create(u'_xtest@example.com') - >>> from Mailman.app.rules import find_rule - >>> rule = find_rule('loop') + >>> rule = config.rules['loop'] >>> rule.name 'loop' diff --git a/Mailman/rules/docs/max-size.txt b/Mailman/rules/docs/max-size.txt index b477ecd2b..0d64b0cf7 100644 --- a/Mailman/rules/docs/max-size.txt +++ b/Mailman/rules/docs/max-size.txt @@ -8,8 +8,7 @@ bytes). >>> from Mailman.configuration import config >>> mlist = config.db.list_manager.create(u'_xtest@example.com') - >>> from Mailman.app.rules import find_rule - >>> rule = find_rule('max-size') + >>> rule = config.rules['max-size'] >>> rule.name 'max-size' diff --git a/Mailman/rules/docs/moderation.txt b/Mailman/rules/docs/moderation.txt index 0ce6bee6e..cab8f20d3 100644 --- a/Mailman/rules/docs/moderation.txt +++ b/Mailman/rules/docs/moderation.txt @@ -8,8 +8,7 @@ email the list without having those messages be held for approval. The >>> from Mailman.configuration import config >>> mlist = config.db.list_manager.create(u'_xtest@example.com') - >>> from Mailman.app.rules import find_rule - >>> rule = find_rule('moderation') + >>> rule = config.rules['moderation'] >>> rule.name 'moderation' @@ -50,7 +49,7 @@ Non-members There is another, related rule for matching non-members, which simply matches if the sender is /not/ a member of the mailing list. - >>> rule = find_rule('non-member') + >>> rule = config.rules['non-member'] >>> rule.name 'non-member' diff --git a/Mailman/rules/docs/news-moderation.txt b/Mailman/rules/docs/news-moderation.txt index f69fbb33d..f32919ce5 100644 --- a/Mailman/rules/docs/news-moderation.txt +++ b/Mailman/rules/docs/news-moderation.txt @@ -10,8 +10,7 @@ directly to the mailing list. >>> from Mailman.configuration import config >>> mlist = config.db.list_manager.create(u'_xtest@example.com') - >>> from Mailman.app.rules import find_rule - >>> rule = find_rule('news-moderation') + >>> rule = config.rules['news-moderation'] >>> rule.name 'news-moderation' diff --git a/Mailman/rules/docs/no-subject.txt b/Mailman/rules/docs/no-subject.txt index 3c6dc88bf..3627ac03f 100644 --- a/Mailman/rules/docs/no-subject.txt +++ b/Mailman/rules/docs/no-subject.txt @@ -6,8 +6,7 @@ the empty string when stripped. >>> from Mailman.configuration import config >>> mlist = config.db.list_manager.create(u'_xtest@example.com') - >>> from Mailman.app.rules import find_rule - >>> rule = find_rule('no-subject') + >>> rule = config.rules['no-subject'] >>> rule.name 'no-subject' diff --git a/Mailman/rules/docs/recipients.txt b/Mailman/rules/docs/recipients.txt index 98176c8bb..21d04b8ae 100644 --- a/Mailman/rules/docs/recipients.txt +++ b/Mailman/rules/docs/recipients.txt @@ -6,8 +6,7 @@ number of explicit recipients addressed by the message. >>> from Mailman.configuration import config >>> mlist = config.db.list_manager.create(u'_xtest@example.com') - >>> from Mailman.app.rules import find_rule - >>> rule = find_rule('max-recipients') + >>> rule = config.rules['max-recipients'] >>> rule.name 'max-recipients' diff --git a/Mailman/rules/docs/rules.txt b/Mailman/rules/docs/rules.txt index b9606663c..1f5b147e7 100644 --- a/Mailman/rules/docs/rules.txt +++ b/Mailman/rules/docs/rules.txt @@ -1,63 +1,41 @@ Rules ===== -The rule processor is used to determine the status of a message. Should the -message be posted to the list, or held for moderator approval? Should the -message be discarded or rejected (i.e. bounced back to the original sender)? +Rules are applied to each message as part of a rule chain. Individual rules +simply return a boolean specifying whether the rule matches or not. Chain +links determine what happens when a rule matches. -Actually, these actions are not part of rule processing! Instead, Mailman -first runs through the set of available and requested rules looking for -matches. Then later, the matched rules are prioritized and matched to an -action. Action matching is described elsewhere; this documentation describes -only the rule processing system. - -Rule sets +All rules --------- -IRuleSet is the interface that describes a set of rules. Mailman can be -extended by plugging in additional rule sets, but it also comes with a default -rule set, called the 'built-in rule set'. +Rules are maintained in the configuration object as a dictionary mapping rule +names to rule objects. >>> from zope.interface.verify import verifyObject - >>> from Mailman.interfaces import IRuleSet - >>> from Mailman.rules import BuiltinRules - >>> rule_set = BuiltinRules() - >>> verifyObject(IRuleSet, rule_set) - True - -You can iterator over all the rules in a rule set. - + >>> from Mailman.configuration import config >>> from Mailman.interfaces import IRule - >>> rule = None - >>> for rule in rule_set.rules: - ... if rule.name == 'emergency': - ... break - >>> verifyObject(IRule, rule) - True - >>> rule.name - 'emergency' - >>> print rule.description - The mailing list is in emergency hold and this message was not pre-approved - by the list administrator. + >>> for rule_name in sorted(config.rules): + ... rule = config.rules[rule_name] + ... print rule_name, verifyObject(IRule, rule) + administrivia True + any True + approved True + emergency True + implicit-dest True + loop True + max-recipients True + max-size True + moderation True + news-moderation True + no-subject True + non-member True + suspicious-header True -You can ask for a rule by name. +You can get a rule by name. - >>> rule_set['emergency'].name - 'emergency' - >>> rule_set.get('emergency').name - 'emergency' - -Rule sets act like dictionaries when the rule is missing. - - >>> rule_set['no such rule'] - Traceback (most recent call last): - ... - KeyError: 'no such rule' - >>> print rule_set.get('no such rule') - None - >>> missing = object() - >>> rule_set.get('no such rule', missing) is missing + >>> rule = config.rules['emergency'] + >>> verifyObject(IRule, rule) True @@ -68,7 +46,6 @@ Individual rules can be checked to see if they match, by running the rule's `check()` method. This returns a boolean indicating whether the rule was matched or not. - >>> from Mailman.configuration import config >>> mlist = config.db.list_manager.create(u'_xtest@example.com') >>> msg = message_from_string("""\ ... From: aperson@example.com @@ -80,7 +57,6 @@ For example, the emergency rule just checks to see if the emergency flag is set on the mailing list, and the message has not been pre-approved by the list administrator. - >>> rule = rule_set['emergency'] >>> rule.name 'emergency' >>> mlist.emergency = False @@ -89,23 +65,5 @@ administrator. >>> mlist.emergency = True >>> rule.check(mlist, msg, {}) True - >>> rule.check(mlist, msg, dict(adminapproved=True)) + >>> rule.check(mlist, msg, dict(moderator_approved=True)) False - - -Rule processing ---------------- - -Mailman has a global rule processor which will return a set of all the rule -names that match the current message. You can limit the set of rules the -processor will check by passing in a set of requested rule names. - - >>> emergency_only = set(['emergency']) - >>> from Mailman.app.rules import process - >>> process(mlist, msg, {}, emergency_only) - set(['emergency']) - >>> process(mlist, msg, dict(adminapproved=True), emergency_only) - set([]) - >>> mlist.emergency = False - >>> process(mlist, msg, {}, emergency_only) - set([]) diff --git a/Mailman/rules/docs/suspicious.txt b/Mailman/rules/docs/suspicious.txt index 8646e1b81..6b0eeda35 100644 --- a/Mailman/rules/docs/suspicious.txt +++ b/Mailman/rules/docs/suspicious.txt @@ -7,8 +7,7 @@ confusing to users, and the list attribute that controls this is misnamed. >>> from Mailman.configuration import config >>> mlist = config.db.list_manager.create(u'_xtest@example.com') - >>> from Mailman.app.rules import find_rule - >>> rule = find_rule('suspicious-header') + >>> rule = config.rules['suspicious-header'] >>> rule.name 'suspicious-header' diff --git a/Mailman/rules/emergency.py b/Mailman/rules/emergency.py index e51612940..0e6aa97b4 100644 --- a/Mailman/rules/emergency.py +++ b/Mailman/rules/emergency.py @@ -39,4 +39,4 @@ the list administrator.""") def check(self, mlist, msg, msgdata): """See `IRule`.""" - return mlist.emergency and not msgdata.get('adminapproved') + return mlist.emergency and not msgdata.get('moderator_approved') |
