summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2007-12-29 17:02:46 -0500
committerBarry Warsaw2007-12-29 17:02:46 -0500
commit68cce110887cc9fc46fd4c7798f3b8d893f1038f (patch)
tree8bd390bd78a2b22c5be19540eb7d7bee503c478b
parent9124e1a17f439314b17206f2ad80f17d7b7971e8 (diff)
downloadmailman-68cce110887cc9fc46fd4c7798f3b8d893f1038f.tar.gz
mailman-68cce110887cc9fc46fd4c7798f3b8d893f1038f.tar.zst
mailman-68cce110887cc9fc46fd4c7798f3b8d893f1038f.zip
-rw-r--r--Mailman/Handlers/Hold.py12
-rw-r--r--Mailman/app/bounces.py51
-rw-r--r--Mailman/docs/hold.txt360
-rw-r--r--Mailman/docs/implicit.txt77
-rw-r--r--Mailman/rules/implicit_dest.py99
-rw-r--r--Mailman/rules/max_recipients.py2
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`."""