+ An important message.
+ --AAA--
+
+
+Stripping text/html parts
+-------------------------
+
+Because some mail readers will include both a text/plain part and a text/html
+alternative, the 'approved' rule has to search the alternatives and strip
+anything that looks like an Approve or Approved headers.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... MIME-Version: 1.0
+ ... Content-Type: multipart/mixed; boundary="AAA"
+ ...
+ ... --AAA
+ ... Content-Type: text/html
+ ...
+ ...
+ ...
+ ...
+ ... Approved: abcxyz
+ ... The above line will be ignored.
+ ...
+ ...
+ ...
+ ... --AAA
+ ... Content-Type: text/plain
+ ...
+ ... Approved: abcxyz
+ ... An important message.
+ ... --AAA--
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ True
+
+And the header-like text in the text/html part was stripped.
+
+ >>> print msg.as_string()
+ From: aperson@example.com
+ MIME-Version: 1.0
+ Content-Type: multipart/mixed; boundary="AAA"
+
+ --AAA
+ Content-Transfer-Encoding: 7bit
+ MIME-Version: 1.0
+ Content-Type: text/html; charset="us-ascii"
+
+
+
+
+
+ The above line will be ignored.
+
+
+
+ --AAA
+ Content-Transfer-Encoding: 7bit
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+
+ An important message.
+ --AAA--
+
+
+This is true even if the rule does not match.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... MIME-Version: 1.0
+ ... Content-Type: multipart/mixed; boundary="AAA"
+ ...
+ ... --AAA
+ ... Content-Type: text/html
+ ...
+ ...
+ ...
+ ...
+ ... Approve: 123456
+ ... The above line will be ignored.
+ ...
+ ...
+ ...
+ ... --AAA
+ ... Content-Type: text/plain
+ ...
+ ... Approve: 123456
+ ... An important message.
+ ... --AAA--
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ False
+
+ >>> print msg.as_string()
+ From: aperson@example.com
+ MIME-Version: 1.0
+ Content-Type: multipart/mixed; boundary="AAA"
+
+ --AAA
+ Content-Transfer-Encoding: 7bit
+ MIME-Version: 1.0
+ Content-Type: text/html; charset="us-ascii"
+
+
+
+
+
+ The above line will be ignored.
+
+
+
+ --AAA
+ Content-Transfer-Encoding: 7bit
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+
+ An important message.
+ --AAA--
+
diff --git a/src/mailman/rules/docs/emergency.txt b/src/mailman/rules/docs/emergency.txt
new file mode 100644
index 000000000..9d80fdb40
--- /dev/null
+++ b/src/mailman/rules/docs/emergency.txt
@@ -0,0 +1,72 @@
+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')
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... To: _xtest@example.com
+ ... Subject: My first post
+ ... Message-ID:
+ ...
+ ... An important message.
+ ... """)
+
+The emergency rule is matched as part of the built-in chain. The emergency
+rule matches if the flag is set on the mailing list.
+
+ >>> from mailman.core.chains import process
+ >>> mlist.emergency = True
+ >>> process(mlist, msg, {}, 'built-in')
+
+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.
+
+ >>> virginq = config.switchboards['virgin']
+
+ >>> 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.*)$', 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:
+ X-Mailman-Rule-Hits: emergency
+ X-Mailman-Rule-Misses: approved
+ X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
+
+ An important message.
+
+
+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.
+
+ >>> process(mlist, msg, dict(moderator_approved=True), 'built-in')
+ >>> len(virginq.files)
+ 0
diff --git a/src/mailman/rules/docs/header-matching.txt b/src/mailman/rules/docs/header-matching.txt
new file mode 100644
index 000000000..417000d67
--- /dev/null
+++ b/src/mailman/rules/docs/header-matching.txt
@@ -0,0 +1,144 @@
+Header matching
+===============
+
+Mailman can do pattern based header matching during its normal rule
+processing. There is a set of site-wide default header matches specified in
+the configuration file under the [spam.headers] section.
+
+ >>> from mailman.app.lifecycle import create_list
+ >>> mlist = create_list(u'_xtest@example.com')
+
+Because the default [spam.headers] section is empty, we'll just extend the
+current header matching chain with a pattern that matches 4 or more stars,
+discarding the message if it hits.
+
+ >>> chain = config.chains['header-match']
+ >>> chain.extend('x-spam-score', '[*]{4,}', 'discard')
+
+First, if the message has no X-Spam-Score header, the message passes through
+the chain untouched (i.e. no disposition).
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... To: _xtest@example.com
+ ... Subject: Not spam
+ ... Message-ID:
+ ...
+ ... This is a message.
+ ... """)
+
+ >>> from mailman.core.chains import process
+
+Pass through is seen as nothing being in the log file after processing.
+
+ # 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'))
+ >>> fp.seek(0, 2)
+ >>> file_pos = fp.tell()
+ >>> process(mlist, msg, {}, 'header-match')
+ >>> fp.seek(file_pos)
+ >>> print 'LOG:', fp.read()
+ LOG:
+
+
+Now, if the header exists but does not match, then it also passes through
+untouched.
+
+ >>> msg['X-Spam-Score'] = '***'
+ >>> del msg['subject']
+ >>> msg['Subject'] = 'This is almost spam'
+ >>> del msg['message-id']
+ >>> msg['Message-ID'] = ''
+ >>> file_pos = fp.tell()
+ >>> process(mlist, msg, {}, 'header-match')
+ >>> fp.seek(file_pos)
+ >>> print 'LOG:', fp.read()
+ LOG:
+
+
+But now if the header matches, then the message gets discarded.
+
+ >>> del msg['x-spam-score']
+ >>> msg['X-Spam-Score'] = '****'
+ >>> del msg['subject']
+ >>> msg['Subject'] = 'This is spam, but barely'
+ >>> del msg['message-id']
+ >>> msg['Message-ID'] = ''
+ >>> file_pos = fp.tell()
+ >>> process(mlist, msg, {}, 'header-match')
+ >>> fp.seek(file_pos)
+ >>> print 'LOG:', fp.read()
+ LOG: ... DISCARD:
+
+
+For kicks, let's show a message that's really spammy.
+
+ >>> del msg['x-spam-score']
+ >>> msg['X-Spam-Score'] = '**********'
+ >>> del msg['subject']
+ >>> msg['Subject'] = 'This is really spammy'
+ >>> del msg['message-id']
+ >>> msg['Message-ID'] = ''
+ >>> file_pos = fp.tell()
+ >>> process(mlist, msg, {}, 'header-match')
+ >>> fp.seek(file_pos)
+ >>> print 'LOG:', fp.read()
+ LOG: ... DISCARD:
+
+
+Flush out the extended header matching rules.
+
+ >>> chain.flush()
+
+
+List-specific header matching
+-----------------------------
+
+Each mailing list can also be configured with a set of header matching regular
+expression rules. These are used to impose list-specific header filtering
+with the same semantics as the global [spam.headers] section.
+
+The list administrator wants to match not on four stars, but on three plus
+signs, but only for the current mailing list.
+
+ >>> mlist.header_matches = [('x-spam-score', '[+]{3,}', 'discard')]
+
+A message with a spam score of two pluses does not match.
+
+ >>> del msg['x-spam-score']
+ >>> msg['X-Spam-Score'] = '++'
+ >>> del msg['message-id']
+ >>> msg['Message-ID'] = ''
+ >>> file_pos = fp.tell()
+ >>> process(mlist, msg, {}, 'header-match')
+ >>> fp.seek(file_pos)
+ >>> print 'LOG:', fp.read()
+ LOG:
+
+A message with a spam score of three pluses does match.
+
+ >>> del msg['x-spam-score']
+ >>> msg['X-Spam-Score'] = '+++'
+ >>> del msg['message-id']
+ >>> msg['Message-ID'] = ''
+ >>> file_pos = fp.tell()
+ >>> process(mlist, msg, {}, 'header-match')
+ >>> fp.seek(file_pos)
+ >>> print 'LOG:', fp.read()
+ LOG: ... DISCARD:
+
+
+As does a message with a spam score of four pluses.
+
+ >>> del msg['x-spam-score']
+ >>> msg['X-Spam-Score'] = '+++'
+ >>> del msg['message-id']
+ >>> msg['Message-ID'] = ''
+ >>> file_pos = fp.tell()
+ >>> process(mlist, msg, {}, 'header-match')
+ >>> fp.seek(file_pos)
+ >>> print 'LOG:', fp.read()
+ LOG: ... DISCARD:
+
diff --git a/src/mailman/rules/docs/implicit-dest.txt b/src/mailman/rules/docs/implicit-dest.txt
new file mode 100644
index 000000000..e5c340dcd
--- /dev/null
+++ b/src/mailman/rules/docs/implicit-dest.txt
@@ -0,0 +1,75 @@
+Implicit destination
+====================
+
+The 'implicit-dest' rule matches when the mailing list's posting address is
+not explicitly mentioned in the set of message recipients.
+
+ >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
+ >>> rule = config.rules['implicit-dest']
+ >>> print rule.name
+ implicit-dest
+
+This rule matches messages that have implicit destination, meaning that the
+mailing list's posting address isn't included in the explicit recipients.
+
+ >>> mlist.require_explicit_destination = True
+ >>> mlist.acceptable_aliases = u''
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.org
+ ... Subject: An implicit message
+ ...
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ True
+
+You can disable implicit destination checks for the mailing list.
+
+ >>> mlist.require_explicit_destination = False
+ >>> rule.check(mlist, msg, {})
+ False
+
+Even with some recipients, if the posting address is not included, the rule
+will match.
+
+ >>> mlist.require_explicit_destination = True
+ >>> msg['To'] = 'myfriend@example.com'
+ >>> rule.check(mlist, msg, {})
+ True
+
+Add the posting address as a recipient and the rule will no longer match.
+
+ >>> msg['Cc'] = '_xtest@example.com'
+ >>> rule.check(mlist, msg, {})
+ False
+
+Alternatively, if one of the acceptable aliases is in the recipients list,
+then the rule will not match.
+
+ >>> del msg['cc']
+ >>> rule.check(mlist, msg, {})
+ True
+ >>> mlist.acceptable_aliases = u'myfriend@example.com'
+ >>> rule.check(mlist, msg, {})
+ False
+
+A message gated from NNTP will obviously have an implicit destination. Such
+gated messages will not be held for implicit destination because it's assumed
+that Mailman pulled it from the appropriate news group.
+
+ >>> rule.check(mlist, msg, dict(fromusenet=True))
+ False
+
+
+Alias patterns
+--------------
+
+It's also possible to specify an alias pattern, i.e. a regular expression to
+match against the recipients. For example, we can say that if there is a
+recipient in the example.net domain, then the rule does not match.
+
+ >>> mlist.acceptable_aliases = u'^.*@example.net'
+ >>> rule.check(mlist, msg, {})
+ True
+ >>> msg['To'] = 'you@example.net'
+ >>> rule.check(mlist, msg, {})
+ False
diff --git a/src/mailman/rules/docs/loop.txt b/src/mailman/rules/docs/loop.txt
new file mode 100644
index 000000000..61612cd75
--- /dev/null
+++ b/src/mailman/rules/docs/loop.txt
@@ -0,0 +1,48 @@
+Posting loops
+=============
+
+To avoid a posting loop, Mailman has a rule to check for the existence of an
+X-BeenThere header with the value of the list's posting address.
+
+ >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
+ >>> rule = config.rules['loop']
+ >>> print rule.name
+ loop
+
+The header could be missing, in which case the rule does not match.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... An important message.
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ False
+
+The header could be present, but not match the list's posting address.
+
+ >>> msg['X-BeenThere'] = u'not-this-list@example.com'
+ >>> rule.check(mlist, msg, {})
+ False
+
+If the header is present and does match the posting address, the rule
+matches.
+
+ >>> del msg['x-beenthere']
+ >>> msg['X-BeenThere'] = mlist.posting_address
+ >>> rule.check(mlist, msg, {})
+ True
+
+Even if there are multiple X-BeenThere headers, as long as one with the
+posting address exists, the rule matches.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... X-BeenThere: not-this-list@example.com
+ ... X-BeenThere: _xtest@example.com
+ ... X-BeenThere: foo@example.com
+ ...
+ ... An important message.
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ True
diff --git a/src/mailman/rules/docs/max-size.txt b/src/mailman/rules/docs/max-size.txt
new file mode 100644
index 000000000..117691e59
--- /dev/null
+++ b/src/mailman/rules/docs/max-size.txt
@@ -0,0 +1,39 @@
+Message size
+============
+
+The 'message-size' rule matches when the posted message is bigger than a
+specified maximum. Generally this is used to prevent huge attachments from
+getting posted to the list. This value is calculated in terms of KB (1024
+bytes).
+
+ >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
+ >>> rule = config.rules['max-size']
+ >>> print rule.name
+ max-size
+
+For example, setting the maximum message size to 1 means that any message
+bigger than that will match the rule.
+
+ >>> mlist.max_message_size = 1 # 1024 bytes
+ >>> one_line = u'x' * 79
+ >>> big_body = u'\n'.join([one_line] * 15)
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... To: _xtest@example.com
+ ...
+ ... """ + big_body)
+ >>> rule.check(mlist, msg, {})
+ True
+
+Setting the maximum message size to zero means no size check is performed.
+
+ >>> mlist.max_message_size = 0
+ >>> rule.check(mlist, msg, {})
+ False
+
+Of course, if the maximum size is larger than the message's size, then it's
+still okay.
+
+ >>> mlist.max_message_size = msg.original_size/1024.0 + 1
+ >>> rule.check(mlist, msg, {})
+ False
diff --git a/src/mailman/rules/docs/moderation.txt b/src/mailman/rules/docs/moderation.txt
new file mode 100644
index 000000000..65be0d7da
--- /dev/null
+++ b/src/mailman/rules/docs/moderation.txt
@@ -0,0 +1,69 @@
+Member moderation
+=================
+
+Each user has a moderation flag. When set, and the list is set to moderate
+postings, then only members with a cleared moderation flag will be able to
+email the list without having those messages be held for approval. The
+'moderation' rule determines whether the message should be moderated or not.
+
+ >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
+ >>> rule = config.rules['moderation']
+ >>> print rule.name
+ moderation
+
+In the simplest case, the sender is not a member of the mailing list, so the
+moderation rule can't match.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.org
+ ... To: _xtest@example.com
+ ... Subject: A posted message
+ ...
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ False
+
+Let's add the message author as a non-moderated member.
+
+ >>> user = config.db.user_manager.create_user(
+ ... u'aperson@example.org', u'Anne Person')
+ >>> address = list(user.addresses)[0]
+ >>> from mailman.interfaces.member import MemberRole
+ >>> member = address.subscribe(mlist, MemberRole.member)
+ >>> member.is_moderated
+ False
+ >>> rule.check(mlist, msg, {})
+ False
+
+Once the member's moderation flag is set though, the rule matches.
+
+ >>> member.is_moderated = True
+ >>> rule.check(mlist, msg, {})
+ True
+
+
+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 = config.rules['non-member']
+ >>> print rule.name
+ non-member
+
+If the sender is a member of this mailing list, the rule does not match.
+
+ >>> rule.check(mlist, msg, {})
+ False
+
+But if the sender is not a member of this mailing list, the rule matches.
+
+ >>> msg = message_from_string("""\
+ ... From: bperson@example.org
+ ... To: _xtest@example.com
+ ... Subject: A posted message
+ ...
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ True
diff --git a/src/mailman/rules/docs/news-moderation.txt b/src/mailman/rules/docs/news-moderation.txt
new file mode 100644
index 000000000..4c095cc81
--- /dev/null
+++ b/src/mailman/rules/docs/news-moderation.txt
@@ -0,0 +1,36 @@
+Newsgroup moderation
+====================
+
+The 'news-moderation' rule matches all messages posted to mailing lists that
+gateway to a moderated newsgroup. The reason for this is that such messages
+must get forwarded on to the newsgroup moderator. From there it will get
+posted to the newsgroup, and from there, gated to the mailing list. It's a
+circuitous route, but it works nonetheless by holding all messages posted
+directly to the mailing list.
+
+ >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
+ >>> rule = config.rules['news-moderation']
+ >>> print rule.name
+ news-moderation
+
+Set the list configuraiton variable to enable newsgroup moderation.
+
+ >>> from mailman.interfaces import NewsModeration
+ >>> mlist.news_moderation = NewsModeration.moderated
+
+And now all messages will match the rule.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.org
+ ... Subject: An announcment
+ ...
+ ... Great things are happening.
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ True
+
+When moderation is turned off, the rule does not match.
+
+ >>> mlist.news_moderation = NewsModeration.none
+ >>> rule.check(mlist, msg, {})
+ False
diff --git a/src/mailman/rules/docs/no-subject.txt b/src/mailman/rules/docs/no-subject.txt
new file mode 100644
index 000000000..576111cd7
--- /dev/null
+++ b/src/mailman/rules/docs/no-subject.txt
@@ -0,0 +1,33 @@
+No Subject header
+=================
+
+This rule matches if the message has no Subject header, or if the header is
+the empty string when stripped.
+
+ >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
+ >>> rule = config.rules['no-subject']
+ >>> print rule.name
+ no-subject
+
+A message with a non-empty subject does not match the rule.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.org
+ ... To: _xtest@example.com
+ ... Subject: A posted message
+ ...
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ False
+
+Delete the Subject header and the rule matches.
+
+ >>> del msg['subject']
+ >>> rule.check(mlist, msg, {})
+ True
+
+Even a Subject header with only whitespace still matches the rule.
+
+ >>> msg['Subject'] = u' '
+ >>> rule.check(mlist, msg, {})
+ True
diff --git a/src/mailman/rules/docs/recipients.txt b/src/mailman/rules/docs/recipients.txt
new file mode 100644
index 000000000..3cd49d501
--- /dev/null
+++ b/src/mailman/rules/docs/recipients.txt
@@ -0,0 +1,40 @@
+Maximum number of recipients
+============================
+
+The 'max-recipients' rule matches when there are more than the maximum allowed
+number of explicit recipients addressed by the message.
+
+ >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
+ >>> rule = config.rules['max-recipients']
+ >>> print rule.name
+ max-recipients
+
+In this case, we'll create a message with 5 recipients. These include all
+addresses in the To and CC headers.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... To: _xtest@example.com, bperson@example.com
+ ... Cc: cperson@example.com
+ ... Cc: dperson@example.com (Dan Person)
+ ... To: Elly Q. Person
+ ...
+ ... Hey folks!
+ ... """)
+
+For backward compatibility, the message must have fewer than the maximum
+number of explicit recipients.
+
+ >>> mlist.max_num_recipients = 5
+ >>> rule.check(mlist, msg, {})
+ True
+
+ >>> mlist.max_num_recipients = 6
+ >>> rule.check(mlist, msg, {})
+ False
+
+Zero means any number of recipients are allowed.
+
+ >>> mlist.max_num_recipients = 0
+ >>> rule.check(mlist, msg, {})
+ False
diff --git a/src/mailman/rules/docs/rules.txt b/src/mailman/rules/docs/rules.txt
new file mode 100644
index 000000000..095d11466
--- /dev/null
+++ b/src/mailman/rules/docs/rules.txt
@@ -0,0 +1,69 @@
+Rules
+=====
+
+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.
+
+
+All rules
+---------
+
+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.rules import IRule
+ >>> 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
+ truth True
+
+You can get a rule by name.
+
+ >>> rule = config.rules['emergency']
+ >>> verifyObject(IRule, rule)
+ True
+
+
+Rule checks
+-----------
+
+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.
+
+ >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... An important message.
+ ... """)
+
+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.
+
+ >>> print rule.name
+ emergency
+ >>> mlist.emergency = False
+ >>> rule.check(mlist, msg, {})
+ False
+ >>> mlist.emergency = True
+ >>> rule.check(mlist, msg, {})
+ True
+ >>> rule.check(mlist, msg, dict(moderator_approved=True))
+ False
diff --git a/src/mailman/rules/docs/suspicious.txt b/src/mailman/rules/docs/suspicious.txt
new file mode 100644
index 000000000..190a34aca
--- /dev/null
+++ b/src/mailman/rules/docs/suspicious.txt
@@ -0,0 +1,35 @@
+Suspicious headers
+==================
+
+Suspicious headers are a way for Mailman to hold messages that match a
+particular regular expression. This mostly historical feature is fairly
+confusing to users, and the list attribute that controls this is misnamed.
+
+ >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
+ >>> rule = config.rules['suspicious-header']
+ >>> print rule.name
+ suspicious-header
+
+Set the so-called suspicious header configuration variable.
+
+ >>> mlist.bounce_matching_headers = u'From: .*person@(blah.)?example.com'
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... To: _xtest@example.com
+ ... Subject: An implicit message
+ ...
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ True
+
+But if the header doesn't match the regular expression, the rule won't match.
+This one comes from a .org address.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.org
+ ... To: _xtest@example.com
+ ... Subject: An implicit message
+ ...
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ False
diff --git a/src/mailman/rules/docs/truth.txt b/src/mailman/rules/docs/truth.txt
new file mode 100644
index 000000000..f331e852b
--- /dev/null
+++ b/src/mailman/rules/docs/truth.txt
@@ -0,0 +1,9 @@
+Truth
+=====
+
+The 'truth' rule always matches. This makes it useful as a terminus rule for
+unconditionally jumping to another chain.
+
+ >>> rule = config.rules['truth']
+ >>> rule.check(False, False, False)
+ True
diff --git a/src/mailman/rules/emergency.py b/src/mailman/rules/emergency.py
new file mode 100644
index 000000000..c2cee06c4
--- /dev/null
+++ b/src/mailman/rules/emergency.py
@@ -0,0 +1,50 @@
+# Copyright (C) 2007-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see .
+
+"""The emergency hold rule."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'Emergency',
+ ]
+
+
+from zope.interface import implements
+
+from mailman.i18n import _
+from mailman.interfaces.rules import IRule
+
+
+
+class Emergency:
+ """The emergency hold rule."""
+ implements(IRule)
+
+ name = 'emergency'
+
+ description = _(
+ """The mailing list is in emergency hold and this message was not
+ pre-approved by the list administrator.
+ """)
+
+ record = True
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ return mlist.emergency and not msgdata.get('moderator_approved')
diff --git a/src/mailman/rules/implicit_dest.py b/src/mailman/rules/implicit_dest.py
new file mode 100644
index 000000000..3ddffa2cf
--- /dev/null
+++ b/src/mailman/rules/implicit_dest.py
@@ -0,0 +1,99 @@
+# Copyright (C) 2007-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see .
+
+"""The implicit destination rule."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'ImplicitDestination',
+ ]
+
+
+import re
+from email.utils import getaddresses
+from zope.interface import implements
+
+from mailman.i18n import _
+from mailman.interfaces.rules import IRule
+
+
+
+class ImplicitDestination:
+ """The implicit destination rule."""
+ implements(IRule)
+
+ name = 'implicit-dest'
+ description = _('Catch messages with implicit destination.')
+ record = True
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ # Implicit destination checking must be enabled in the mailing list.
+ if not mlist.require_explicit_destination:
+ return False
+ # Messages gated from NNTP will always have an implicit destination so
+ # are never checked.
+ if msgdata.get('fromusenet'):
+ return False
+ # Calculate the list of acceptable aliases. If the alias starts with
+ # a caret (i.e. ^), then it's a regular expression to match against.
+ aliases = set()
+ alias_patterns = set()
+ for alias in mlist.acceptable_aliases.splitlines():
+ alias = alias.strip().lower()
+ if alias.startswith('^'):
+ alias_patterns.add(alias)
+ elif '@' in alias:
+ aliases.add(alias)
+ else:
+ # This is not a regular expression, nor a fully-qualified
+ # email address, so skip it.
+ pass
+ # Add the list's posting address, i.e. the explicit address, to the
+ # set of acceptable aliases.
+ aliases.add(mlist.posting_address)
+ # Look at all the recipients. If the recipient is any acceptable
+ # alias (or the explicit posting address), then this rule does not
+ # match. If not, then add it to the set of recipients we'll check
+ # against the alias patterns later.
+ recipients = set()
+ for header in ('to', 'cc', 'resent-to', 'resent-cc'):
+ for fullname, address in getaddresses(msg.get_all(header, [])):
+ address = address.lower()
+ if address in aliases:
+ return False
+ recipients.add(address)
+ # Now for all alias patterns, see if any of the recipients matches a
+ # pattern. If so, then this rule does not match.
+ for pattern in alias_patterns:
+ escaped = re.escape(pattern)
+ for recipient in recipients:
+ try:
+ if re.match(pattern, recipient, re.IGNORECASE):
+ return False
+ except re.error:
+ # The pattern is a malformed regular expression. Try
+ # matching again with the pattern escaped.
+ try:
+ if re.match(escaped, recipient, re.IGNORECASE):
+ return False
+ except re.error:
+ pass
+ # Nothing matched.
+ return True
diff --git a/src/mailman/rules/loop.py b/src/mailman/rules/loop.py
new file mode 100644
index 000000000..564d20dc6
--- /dev/null
+++ b/src/mailman/rules/loop.py
@@ -0,0 +1,48 @@
+# Copyright (C) 2007-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see .
+
+"""Look for a posting loop."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'Loop',
+ ]
+
+
+from zope.interface import implements
+
+from mailman.i18n import _
+from mailman.interfaces.rules import IRule
+
+
+
+class Loop:
+ """Look for a posting loop."""
+ implements(IRule)
+
+ name = 'loop'
+ description = _('Look for a posting loop, via the X-BeenThere header.')
+ record = True
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ # Has this message already been posted to this list?
+ been_theres = [value.strip().lower()
+ for value in msg.get_all('x-beenthere', [])]
+ return mlist.posting_address in been_theres
diff --git a/src/mailman/rules/max_recipients.py b/src/mailman/rules/max_recipients.py
new file mode 100644
index 000000000..a9cfd4a7f
--- /dev/null
+++ b/src/mailman/rules/max_recipients.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2007-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see .
+
+"""The maximum number of recipients rule."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'MaximumRecipients',
+ ]
+
+
+from email.utils import getaddresses
+from zope.interface import implements
+
+from mailman.i18n import _
+from mailman.interfaces.rules import IRule
+
+
+
+class MaximumRecipients:
+ """The maximum number of recipients rule."""
+ implements(IRule)
+
+ name = 'max-recipients'
+ description = _('Catch messages with too many explicit recipients.')
+ record = True
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ # Zero means any number of recipients are allowed.
+ if mlist.max_num_recipients == 0:
+ return False
+ # Figure out how many recipients there are
+ recipients = getaddresses(msg.get_all('to', []) +
+ msg.get_all('cc', []))
+ return len(recipients) >= mlist.max_num_recipients
diff --git a/src/mailman/rules/max_size.py b/src/mailman/rules/max_size.py
new file mode 100644
index 000000000..bac79bbab
--- /dev/null
+++ b/src/mailman/rules/max_size.py
@@ -0,0 +1,50 @@
+# Copyright (C) 2007-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see .
+
+"""The maximum message size rule."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'MaximumSize',
+ ]
+
+
+from zope.interface import implements
+
+from mailman.i18n import _
+from mailman.interfaces.rules import IRule
+
+
+
+class MaximumSize:
+ """The implicit destination rule."""
+ implements(IRule)
+
+ name = 'max-size'
+ description = _('Catch messages that are bigger than a specified maximum.')
+ record = True
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ if mlist.max_message_size == 0:
+ return False
+ assert hasattr(msg, 'original_size'), (
+ 'Message was not sized on initial parsing.')
+ # The maximum size is specified in 1024 bytes.
+ return msg.original_size / 1024.0 > mlist.max_message_size
diff --git a/src/mailman/rules/moderation.py b/src/mailman/rules/moderation.py
new file mode 100644
index 000000000..708983f6b
--- /dev/null
+++ b/src/mailman/rules/moderation.py
@@ -0,0 +1,68 @@
+# Copyright (C) 2007-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see .
+
+"""Membership related rules."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'Moderation',
+ 'NonMember',
+ ]
+
+
+from zope.interface import implements
+
+from mailman.i18n import _
+from mailman.interfaces.rules import IRule
+
+
+
+class Moderation:
+ """The member moderation rule."""
+ implements(IRule)
+
+ name = 'moderation'
+ description = _('Match messages sent by moderated members.')
+ record = True
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ for sender in msg.get_senders():
+ member = mlist.members.get_member(sender)
+ if member is not None and member.is_moderated:
+ return True
+ return False
+
+
+
+class NonMember:
+ """The non-membership rule."""
+ implements(IRule)
+
+ name = 'non-member'
+ description = _('Match messages sent by non-members.')
+ record = True
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ for sender in msg.get_senders():
+ if mlist.members.get_member(sender) is not None:
+ # The sender is a member of the mailing list.
+ return False
+ return True
diff --git a/src/mailman/rules/news_moderation.py b/src/mailman/rules/news_moderation.py
new file mode 100644
index 000000000..3ead80086
--- /dev/null
+++ b/src/mailman/rules/news_moderation.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2007-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see .
+
+"""The news moderation rule."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'ModeratedNewsgroup',
+ ]
+
+
+from zope.interface import implements
+
+from mailman.i18n import _
+from mailman.interfaces import NewsModeration
+from mailman.interfaces.rules import IRule
+
+
+
+class ModeratedNewsgroup:
+ """The news moderation rule."""
+ implements(IRule)
+
+ name = 'news-moderation'
+ description = _(
+ """Match all messages posted to a mailing list that gateways to a
+ moderated newsgroup.
+ """)
+ record = True
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ return mlist.news_moderation == NewsModeration.moderated
diff --git a/src/mailman/rules/no_subject.py b/src/mailman/rules/no_subject.py
new file mode 100644
index 000000000..2487867e7
--- /dev/null
+++ b/src/mailman/rules/no_subject.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2007-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see .
+
+"""The no-Subject header rule."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'NoSubject',
+ ]
+
+
+from zope.interface import implements
+
+from mailman.i18n import _
+from mailman.interfaces.rules import IRule
+
+
+
+class NoSubject:
+ """The no-Subject rule."""
+ implements(IRule)
+
+ name = 'no-subject'
+ description = _('Catch messages with no, or empty, Subject headers.')
+ record = True
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ subject = msg.get('subject', '').strip()
+ return subject == ''
diff --git a/src/mailman/rules/suspicious.py b/src/mailman/rules/suspicious.py
new file mode 100644
index 000000000..00e9a5e9e
--- /dev/null
+++ b/src/mailman/rules/suspicious.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2007-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see .
+
+"""The historical 'suspicious header' rule."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'SuspiciousHeader',
+ ]
+
+
+import re
+import logging
+
+from zope.interface import implements
+
+from mailman.i18n import _
+from mailman.interfaces.rules import IRule
+
+log = logging.getLogger('mailman.error')
+
+
+
+class SuspiciousHeader:
+ """The historical 'suspicious header' rule."""
+ implements(IRule)
+
+ name = 'suspicious-header'
+ description = _('Catch messages with suspicious headers.')
+ record = True
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ return (mlist.bounce_matching_headers and
+ has_matching_bounce_header(mlist, msg))
+
+
+
+def _parse_matching_header_opt(mlist):
+ """Return a list of triples [(field name, regex, line), ...]."""
+ # - Blank lines and lines with '#' as first char are skipped.
+ # - Leading whitespace in the matchexp is trimmed - you can defeat
+ # that by, eg, containing it in gratuitous square brackets.
+ all = []
+ for line in mlist.bounce_matching_headers.splitlines():
+ line = line.strip()
+ # Skip blank lines and lines *starting* with a '#'.
+ if not line or line.startswith('#'):
+ continue
+ i = line.find(':')
+ if i < 0:
+ # This didn't look like a header line. BAW: should do a
+ # better job of informing the list admin.
+ log.error('bad bounce_matching_header line: %s\n%s',
+ mlist.real_name, line)
+ else:
+ header = line[:i]
+ value = line[i+1:].lstrip()
+ try:
+ cre = re.compile(value, re.IGNORECASE)
+ except re.error as error:
+ # The regexp was malformed. BAW: should do a better
+ # job of informing the list admin.
+ log.error("""\
+bad regexp in bounce_matching_header line: %s
+\n%s (cause: %s)""", mlist.real_name, value, error)
+ else:
+ all.append((header, cre, line))
+ return all
+
+
+def has_matching_bounce_header(mlist, msg):
+ """Does the message have a matching bounce header?
+
+ :param mlist: The mailing list the message is destined for.
+ :param msg: The email message object.
+ :return: True if a header field matches a regexp in the
+ bounce_matching_header mailing list variable.
+ """
+ for header, cre, line in _parse_matching_header_opt(mlist):
+ for value in msg.get_all(header, []):
+ if cre.search(value):
+ return True
+ return False
diff --git a/src/mailman/rules/truth.py b/src/mailman/rules/truth.py
new file mode 100644
index 000000000..45b5560c2
--- /dev/null
+++ b/src/mailman/rules/truth.py
@@ -0,0 +1,45 @@
+# Copyright (C) 2008-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see .
+
+"""A rule which always matches."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'Truth',
+ ]
+
+
+from zope.interface import implements
+
+from mailman.i18n import _
+from mailman.interfaces.rules import IRule
+
+
+
+class Truth:
+ """Look for any previous rule match."""
+ implements(IRule)
+
+ name = 'truth'
+ description = _('A rule which always matches.')
+ record = False
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ return True
--
cgit v1.2.3-70-g09d2