diff options
| -rw-r--r-- | Mailman/Handlers/Hold.py | 12 | ||||
| -rw-r--r-- | Mailman/app/bounces.py | 51 | ||||
| -rw-r--r-- | Mailman/docs/hold.txt | 360 | ||||
| -rw-r--r-- | Mailman/docs/implicit.txt | 77 | ||||
| -rw-r--r-- | Mailman/rules/implicit_dest.py | 99 | ||||
| -rw-r--r-- | Mailman/rules/max_recipients.py | 2 |
6 files changed, 340 insertions, 261 deletions
diff --git a/Mailman/Handlers/Hold.py b/Mailman/Handlers/Hold.py index 17f49ce11..12bab8a8b 100644 --- a/Mailman/Handlers/Hold.py +++ b/Mailman/Handlers/Hold.py @@ -43,8 +43,7 @@ from Mailman import Errors from Mailman import Message from Mailman import Utils from Mailman import i18n -from Mailman.app.bounces import ( - has_explicit_destination, has_matching_bounce_header) +from Mailman.app.bounces import has_matching_bounce_header from Mailman.app.moderator import hold_message from Mailman.app.replybot import autorespond_to_sender from Mailman.configuration import config @@ -158,15 +157,6 @@ def process(mlist, msg, msgdata): if not sender or sender[:len(listname)+6] == adminaddr: sender = msg.get_sender(use_envelope=0) # - # Implicit destination? Note that message originating from the Usenet - # side of the world should never be checked for implicit destination. - if mlist.require_explicit_destination and \ - not has_explicit_destination(mlist, msg) and \ - not msgdata.get('fromusenet'): - # then - hold_for_approval(mlist, msg, msgdata, ImplicitDestination) - # no return - # # Suspicious headers? if mlist.bounce_matching_headers: triggered = has_matching_bounce_header(mlist, msg) diff --git a/Mailman/app/bounces.py b/Mailman/app/bounces.py index 6df5c8aa6..7c3f6d894 100644 --- a/Mailman/app/bounces.py +++ b/Mailman/app/bounces.py @@ -19,7 +19,6 @@ __all__ = [ 'bounce_message', - 'has_explicit_destination', 'has_matching_bounce_header', ] @@ -65,56 +64,6 @@ def bounce_message(mlist, msg, e=None): -# Helper function used to match a pattern against an address. -def _domatch(pattern, addr): - try: - if re.match(pattern, addr, re.IGNORECASE): - return True - except re.error: - # The pattern is a malformed regexp -- try matching safely, - # with all non-alphanumerics backslashed: - if re.match(re.escape(pattern), addr, re.IGNORECASE): - return True - return False - - -def has_explicit_destination(mlist, msg): - """Does the list's name or an acceptable alias appear in the recipients? - - :param mlist: The mailing list the message is destined for. - :param msg: The email message object. - :return: True if the message is explicitly destined for the mailing list, - otherwise False. - """ - # Check all recipient addresses against the list's explicit addresses, - # specifically To: Cc: and Resent-to: - recipients = [] - to = [] - for header in ('to', 'cc', 'resent-to', 'resent-cc'): - to.extend(getaddresses(msg.get_all(header, []))) - for fullname, address in to: - # It's possible that if the header doesn't have a valid RFC 2822 - # value, we'll get None for the address. So skip it. - if address is None or '@' not in address: - continue - address = address.lower() - if address == mlist.posting_address: - return True - recipients.append(address) - # Match the set of recipients against the list's acceptable aliases. - aliases = mlist.acceptable_aliases.splitlines() - for address in recipients: - for alias in aliases: - stripped = alias.strip() - if not stripped: - # Ignore blank or empty lines - continue - if domatch(stripped, address): - return True - return False - - - 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. diff --git a/Mailman/docs/hold.txt b/Mailman/docs/hold.txt index 40d3e368c..ed3fabdf0 100644 --- a/Mailman/docs/hold.txt +++ b/Mailman/docs/hold.txt @@ -57,42 +57,6 @@ handler returns immediately. {'approved': True} -Implicit destination --------------------- - -Mailman will hold 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 - ... - ... """) - >>> process(mlist, msg, {}) - Traceback (most recent call last): - ... - ImplicitDestination - >>> clear() - -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. - - >>> msgdata = {'fromusenet': True} - >>> process(mlist, msg, msgdata) - >>> print msg.as_string() - From: aperson@example.org - Subject: An implicit message - Message-ID: ... - X-Message-ID-Hash: ... - <BLANKLINE> - <BLANKLINE> - >>> print msgdata - {'fromusenet': True} - - Suspicious headers ------------------ @@ -154,165 +118,165 @@ value is calculated in terms of KB (1024 bytes). >>> clear() -Hold Notifications ------------------- - -Whenever Mailman holds a message, it sends notifications both to the list -owner and to the original sender, as long as it is configured to do so. We -can show this by first holding a message. - - >>> mlist.respond_to_post_requests = True - >>> mlist.admin_immed_notify = True - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... - ... """) - >>> process(mlist, msg, {}) - Traceback (most recent call last): - ... - ImplicitDestination - -There should be two messages in the virgin queue, one to the list owner and -one to the original author. - - >>> len(switchboard.files) - 2 - >>> qfiles = {} - >>> for filebase in switchboard.files: - ... qmsg, qdata = switchboard.dequeue(filebase) - ... switchboard.finish(filebase) - ... qfiles[qmsg['to']] = qmsg, qdata - >>> qmsg, qdata = qfiles['_xtest-owner@example.com'] - >>> print qmsg.as_string() - Subject: _xtest post from aperson@example.com requires approval - From: _xtest-owner@example.com - To: _xtest-owner@example.com - MIME-Version: 1.0 - Content-Type: multipart/mixed; boundary="..." - Message-ID: ... - Date: ... - Precedence: bulk - <BLANKLINE> - --... - Content-Type: text/plain; charset="us-ascii" - MIME-Version: 1.0 - Content-Transfer-Encoding: 7bit - <BLANKLINE> - As list administrator, your authorization is requested for the - following mailing list posting: - <BLANKLINE> - List: _xtest@example.com - From: aperson@example.com - Subject: (no subject) - Reason: Message has implicit destination - <BLANKLINE> - At your convenience, visit: - <BLANKLINE> - http://lists.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 - Message-ID: ... - X-Message-ID-Hash: ... - <BLANKLINE> - <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 - Date: ... - Message-ID: ... - <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. - --... - >>> sorted(qdata.items()) - [('_parsemsg', False), ('listname', u'_xtest@example.com'), - ('nodecorate', True), ('received_time', ...), - ('recips', [u'_xtest-owner@example.com']), - ('reduced_list_headers', True), - ('tomoderators', 1), ('version', 3)] - >>> qmsg, qdata = qfiles['aperson@example.com'] - >>> print qmsg.as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: Your message to _xtest awaits moderator approval - From: _xtest-bounces@example.com - To: aperson@example.com - Message-ID: ... - Date: ... - Precedence: bulk - <BLANKLINE> - Your mail to '_xtest' with the subject - <BLANKLINE> - (no subject) - <BLANKLINE> - Is being held until the list moderator can review it for approval. - <BLANKLINE> - The reason it is being held: - <BLANKLINE> - Message has implicit destination - <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://lists.example.com/confirm/_xtest@example.com/... - <BLANKLINE> - <BLANKLINE> - >>> sorted(qdata.items()) - [('_parsemsg', False), ('listname', u'_xtest@example.com'), - ('nodecorate', True), ('received_time', ...), - ('recips', [u'aperson@example.com']), - ('reduced_list_headers', True), ('version', 3)] - -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 - >>> qmsg, qdata = qfiles['aperson@example.com'] - >>> for line in qmsg.get_payload().splitlines(): - ... mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line) - ... if mo: - ... cookie = mo.group('cookie') - ... break - >>> 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 - Message-ID: ... - X-Message-ID-Hash: ... - <BLANKLINE> - <BLANKLINE> - -Clean up. - - >>> clear() +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/implicit.txt b/Mailman/docs/implicit.txt new file mode 100644 index 000000000..967ddc8c7 --- /dev/null +++ b/Mailman/docs/implicit.txt @@ -0,0 +1,77 @@ +Implicit destination +==================== + +The 'implicit-dest' rule matches when the mailing list's posting address is +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.name + 'implicit-dest' + +Mailman will hold 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(u"""\ + ... 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/Mailman/rules/implicit_dest.py b/Mailman/rules/implicit_dest.py new file mode 100644 index 000000000..8c9ba8899 --- /dev/null +++ b/Mailman/rules/implicit_dest.py @@ -0,0 +1,99 @@ +# 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 implicit destination rule.""" + +__all__ = ['implicit_dest'] +__metaclass__ = type + + +import re +from email.utils import getaddresses +from itertools import chain +from zope.interface import implements + +from Mailman.i18n import _ +from Mailman.interfaces import IRule + + + +class ImplicitDestination: + """The implicit destination rule.""" + implements(IRule) + + name = 'implicit-dest' + description = _('Catch messages with implicit destination.') + + 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 + + + +implicit_dest = ImplicitDestination() diff --git a/Mailman/rules/max_recipients.py b/Mailman/rules/max_recipients.py index 8c2fbb370..6e3451e4e 100644 --- a/Mailman/rules/max_recipients.py +++ b/Mailman/rules/max_recipients.py @@ -34,7 +34,7 @@ class MaximumRecipients: implements(IRule) name = 'max-recipients' - description = _('Catch messages with too many explicit recipients') + description = _('Catch messages with too many explicit recipients.') def check(self, mlist, msg, msgdata): """See `IRule`.""" |
