diff options
| author | Barry Warsaw | 2007-12-29 11:15:10 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2007-12-29 11:15:10 -0500 |
| commit | c70909dbe5cf6b32ddc72963fd02eda0b5bce6d2 (patch) | |
| tree | f328d4ae4e78779c968ea9ab30cd344fb18783db | |
| parent | d5c2865c3b247a96f4cfd8523b725b8eaffbde80 (diff) | |
| download | mailman-c70909dbe5cf6b32ddc72963fd02eda0b5bce6d2.tar.gz mailman-c70909dbe5cf6b32ddc72963fd02eda0b5bce6d2.tar.zst mailman-c70909dbe5cf6b32ddc72963fd02eda0b5bce6d2.zip | |
| -rw-r--r-- | Mailman/Defaults.py | 2 | ||||
| -rw-r--r-- | Mailman/Handlers/Hold.py | 5 | ||||
| -rw-r--r-- | Mailman/Utils.py | 57 | ||||
| -rw-r--r-- | Mailman/docs/administrivia.txt | 101 | ||||
| -rw-r--r-- | Mailman/docs/hold.txt | 21 | ||||
| -rw-r--r-- | Mailman/queue/command.py | 4 | ||||
| -rw-r--r-- | Mailman/rules/administrivia.py | 102 |
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() + |
