summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Mailman/Handlers/Decorate.py1
-rw-r--r--Mailman/Handlers/Emergency.py37
-rw-r--r--Mailman/app/chains.py91
-rw-r--r--Mailman/app/moderator.py4
-rw-r--r--Mailman/app/replybot.py4
-rw-r--r--Mailman/app/rules.py8
-rw-r--r--Mailman/database/pending.py9
-rw-r--r--Mailman/docs/chains.txt338
-rw-r--r--Mailman/docs/hold.txt223
-rw-r--r--Mailman/docs/requests.txt2
-rw-r--r--Mailman/initialize.py7
-rw-r--r--Mailman/rules/docs/administrivia.txt3
-rw-r--r--Mailman/rules/docs/approve.txt3
-rw-r--r--Mailman/rules/docs/emergency.txt77
-rw-r--r--Mailman/rules/docs/implicit-dest.txt3
-rw-r--r--Mailman/rules/docs/loop.txt3
-rw-r--r--Mailman/rules/docs/max-size.txt3
-rw-r--r--Mailman/rules/docs/moderation.txt5
-rw-r--r--Mailman/rules/docs/news-moderation.txt3
-rw-r--r--Mailman/rules/docs/no-subject.txt3
-rw-r--r--Mailman/rules/docs/recipients.txt3
-rw-r--r--Mailman/rules/docs/rules.txt96
-rw-r--r--Mailman/rules/docs/suspicious.txt3
-rw-r--r--Mailman/rules/emergency.py2
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')