summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Mailman/Defaults.py2
-rw-r--r--Mailman/Handlers/Hold.py5
-rw-r--r--Mailman/Utils.py57
-rw-r--r--Mailman/docs/administrivia.txt101
-rw-r--r--Mailman/docs/hold.txt21
-rw-r--r--Mailman/queue/command.py4
-rw-r--r--Mailman/rules/administrivia.py102
7 files changed, 206 insertions, 86 deletions
diff --git a/Mailman/Defaults.py b/Mailman/Defaults.py
index c4d9d7dbc..75e1c768f 100644
--- a/Mailman/Defaults.py
+++ b/Mailman/Defaults.py
@@ -922,7 +922,7 @@ ${web_page_url}listinfo${cgiext}/${list_name}
DEFAULT_SCRUB_NONDIGEST = False
# Mail command processor will ignore mail command lines after designated max.
-DEFAULT_MAIL_COMMANDS_MAX_LINES = 25
+EMAIL_COMMANDS_MAX_LINES = 10
# Is the list owner notified of admin requests immediately by mail, as well as
# by daily pending-request reminder?
diff --git a/Mailman/Handlers/Hold.py b/Mailman/Handlers/Hold.py
index f53c168c5..494b653da 100644
--- a/Mailman/Handlers/Hold.py
+++ b/Mailman/Handlers/Hold.py
@@ -158,11 +158,6 @@ def process(mlist, msg, msgdata):
if not sender or sender[:len(listname)+6] == adminaddr:
sender = msg.get_sender(use_envelope=0)
#
- # Possible administrivia?
- if mlist.administrivia and Utils.is_administrivia(msg):
- hold_for_approval(mlist, msg, msgdata, Administrivia)
- # no return
- #
# Are there too many recipients to the message?
if mlist.max_num_recipients > 0:
# figure out how many recipients there are
diff --git a/Mailman/Utils.py b/Mailman/Utils.py
index 0bd8fa2a6..1be02e87a 100644
--- a/Mailman/Utils.py
+++ b/Mailman/Utils.py
@@ -537,63 +537,6 @@ def maketext(templatefile, dict=None, raw=False, lang=None, mlist=None):
-ADMINDATA = {
- # admin keyword: (minimum #args, maximum #args)
- 'confirm': (1, 1),
- 'help': (0, 0),
- 'info': (0, 0),
- 'lists': (0, 0),
- 'options': (0, 0),
- 'password': (2, 2),
- 'remove': (0, 0),
- 'set': (3, 3),
- 'subscribe': (0, 3),
- 'unsubscribe': (0, 1),
- 'who': (0, 2),
- }
-
-# Given a Message.Message object, test for administrivia (eg subscribe,
-# unsubscribe, etc). The test must be a good guess -- messages that return
-# true get sent to the list admin instead of the entire list.
-def is_administrivia(msg):
- linecnt = 0
- lines = []
- for line in email.Iterators.body_line_iterator(msg):
- # Strip out any signatures
- if line == '-- ':
- break
- if line.strip():
- linecnt += 1
- if linecnt > config.DEFAULT_MAIL_COMMANDS_MAX_LINES:
- return False
- lines.append(line)
- bodytext = NL.join(lines)
- # See if the body text has only one word, and that word is administrivia
- if ADMINDATA.has_key(bodytext.strip().lower()):
- return True
- # Look at the first N lines and see if there is any administrivia on the
- # line. BAW: N is currently hardcoded to 5. str-ify the Subject: header
- # because it may be an email.Header.Header instance rather than a string.
- bodylines = lines[:5]
- subject = str(msg.get('subject', ''))
- bodylines.append(subject)
- for line in bodylines:
- if not line.strip():
- continue
- words = [word.lower() for word in line.split()]
- minargs, maxargs = ADMINDATA.get(words[0], (None, None))
- if minargs is None and maxargs is None:
- continue
- if minargs <= len(words[1:]) <= maxargs:
- # Special case the `set' keyword. BAW: I don't know why this is
- # here.
- if words[0] == 'set' and words[2] not in ('on', 'off'):
- continue
- return True
- return False
-
-
-
def GetRequestURI(fallback=None, escape=True):
"""Return the full virtual path this CGI script was invoked with.
diff --git a/Mailman/docs/administrivia.txt b/Mailman/docs/administrivia.txt
new file mode 100644
index 000000000..0e48fdd1b
--- /dev/null
+++ b/Mailman/docs/administrivia.txt
@@ -0,0 +1,101 @@
+Administrivia
+=============
+
+The 'administrivia' rule matches when the message contains some common email
+commands in the Subject header or first few lines of the payload. This is
+used to catch messages posted to the list which should have been sent to the
+-request robot address.
+
+ >>> 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.name
+ 'administrivia'
+
+For example, if the Subject header contains the word 'unsubscribe', the rule
+matches.
+
+ >>> msg_1 = message_from_string(u"""\
+ ... From: aperson@example.com
+ ... Subject: unsubscribe
+ ...
+ ... """)
+ >>> rule.check(mlist, msg_1, {})
+ True
+
+Similarly, if the body of the message contains the word 'subscribe' in the
+first few lines of text, the rule matches.
+
+ >>> msg_2 = message_from_string(u"""\
+ ... From: aperson@example.com
+ ... Subject: I wish to join your list
+ ...
+ ... subscribe
+ ... """)
+ >>> rule.check(mlist, msg_2, {})
+ True
+
+In both cases, administrivia checking can be disabled.
+
+ >>> mlist.administrivia = False
+ >>> rule.check(mlist, msg_1, {})
+ False
+ >>> rule.check(mlist, msg_2, {})
+ False
+
+To make the administrivia heuristics a little more robust, the rule actually
+looks for a minimum and maximum number of arguments, so that it really does
+seem like a mis-addressed email command. In this case, the 'confirm' command
+requires at least one argument. We don't give that here so the rule will not
+match.
+
+ >>> mlist.administrivia = True
+ >>> msg = message_from_string(u"""\
+ ... From: aperson@example.com
+ ... Subject: confirm
+ ...
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ False
+
+But a real 'confirm' message will match.
+
+ >>> msg = message_from_string(u"""\
+ ... From: aperson@example.com
+ ... Subject: confirm 12345
+ ...
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ True
+
+We don't show all the other possible email commands, but you get the idea.
+
+
+Non-administrivia
+-----------------
+
+Of course, messages that don't contain administrivia, don't match the rule.
+
+ >>> msg = message_from_string(u"""\
+ ... From: aperson@example.com
+ ... Subject: examine
+ ...
+ ... persuade
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ False
+
+Also, only text/plain parts are checked for administrivia, so any email
+commands in other content type subparts are ignored.
+
+ >>> msg = message_from_string(u"""\
+ ... From: aperson@example.com
+ ... Subject: some administrivia
+ ... Content-Type: text/x-special
+ ...
+ ... subscribe
+ ... """)
+ >>> rule.check(mlist, msg, {})
+ False
diff --git a/Mailman/docs/hold.txt b/Mailman/docs/hold.txt
index a93953435..4ee5f8d67 100644
--- a/Mailman/docs/hold.txt
+++ b/Mailman/docs/hold.txt
@@ -57,27 +57,6 @@ handler returns immediately.
{'approved': True}
-Administrivia
--------------
-
-Mailman scans parts of the message for administrivia, meaning text that looks
-like an email command. This is to prevent people sending 'help' or
-'subscribe' message, etc. to the list members. First, we enable the scanning
-of administrivia for the list.
-
- >>> mlist.administrivia = True
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... Subject: unsubscribe
- ...
- ... """)
- >>> process(mlist, msg, {})
- Traceback (most recent call last):
- ...
- Administrivia
- >>> clear()
-
-
Maximum number of recipients
----------------------------
diff --git a/Mailman/queue/command.py b/Mailman/queue/command.py
index dfbfd1255..8628411de 100644
--- a/Mailman/queue/command.py
+++ b/Mailman/queue/command.py
@@ -88,8 +88,8 @@ class Results:
assert isinstance(body, basestring)
lines = body.splitlines()
# Use no more lines than specified
- self.commands.extend(lines[:config.DEFAULT_MAIL_COMMANDS_MAX_LINES])
- self.ignored.extend(lines[config.DEFAULT_MAIL_COMMANDS_MAX_LINES:])
+ self.commands.extend(lines[:config.EMAIL_COMMANDS_MAX_LINES])
+ self.ignored.extend(lines[config.EMAIL_COMMANDS_MAX_LINES:])
def process(self):
# Now, process each line until we find an error. The first
diff --git a/Mailman/rules/administrivia.py b/Mailman/rules/administrivia.py
new file mode 100644
index 000000000..422503c71
--- /dev/null
+++ b/Mailman/rules/administrivia.py
@@ -0,0 +1,102 @@
+# Copyright (C) 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.
+
+"""The administrivia rule."""
+
+__all__ = ['administrivia_rule']
+__metaclass__ = type
+
+
+from email.iterators import typed_subpart_iterator
+from zope.interface import implements
+
+from Mailman.configuration import config
+from Mailman.i18n import _
+from Mailman.interfaces import IRule
+
+
+# The list of email commands we search for in the Subject header and payload.
+# We probably should get this information from the actual implemented
+# commands.
+EMAIL_COMMANDS = {
+ # keyword: (minimum #args, maximum #args)
+ 'confirm': (1, 1),
+ 'help': (0, 0),
+ 'info': (0, 0),
+ 'lists': (0, 0),
+ 'options': (0, 0),
+ 'password': (2, 2),
+ 'remove': (0, 0),
+ 'set': (3, 3),
+ 'subscribe': (0, 3),
+ 'unsubscribe': (0, 1),
+ 'who': (0, 2),
+ }
+
+
+
+class Administrivia:
+ """The administrivia rule."""
+ implements(IRule)
+
+ name = 'administrivia'
+ description = _('Catch mis-addressed email commands.')
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ # The list must have the administrivia check enabled.
+ if not mlist.administrivia:
+ return False
+ # First check the Subject text.
+ lines_to_check = []
+ subject = msg.get('subject', '')
+ if subject <> '':
+ lines_to_check.append(subject)
+ # Search only the first text/plain subpart of the message. There's
+ # really no good way to find email commands in any other content type.
+ for part in typed_subpart_iterator(msg, 'text', 'plain'):
+ payload = part.get_payload(decode=True)
+ lines = payload.splitlines()
+ # Count lines without using enumerate() because blank lines in the
+ # payload don't count against the maximum examined.
+ lineno = 0
+ for line in lines:
+ line = line.strip()
+ if line == '':
+ continue
+ lineno += 1
+ if lineno > config.EMAIL_COMMANDS_MAX_LINES:
+ break
+ lines_to_check.append(line)
+ # Only look at the first text/plain part.
+ break
+ # For each line we're checking, split the line into words. Then see
+ # if it looks like a command with the min-to-max number of arguments.
+ for line in lines_to_check:
+ words = [word.lower() for word in line.split()]
+ if words[0] not in EMAIL_COMMANDS:
+ # This is not an administrivia command.
+ continue
+ minargs, maxargs = EMAIL_COMMANDS[words[0]]
+ if minargs <= len(words) - 1 <= maxargs:
+ return True
+ return False
+
+
+
+administrivia_rule = Administrivia()
+