------------------------------------------------------------ revno: 6564 committer: Barry Warsaw <[EMAIL PROTECTED]> branch nick: 3.0 timestamp: Sat 2007-10-06 15:09:34 -0400 message: Changes to support the Approved/Approve header with the new user model. Specifically, where a mailing list used to have both a password and a moderator password, both of which could be used in the Approved header, now a mailing list has only a shared moderator password. This moderator password's only purpose in life is to allow for Approved header posting. test_handlers.py is now completely ported to doctests, so it's removed. removed: Mailman/tests/test_handlers.py added: Mailman/docs/approve.txt modified: Mailman/Handlers/Approve.py Mailman/database/model/mailinglist.py
=== removed file 'Mailman/tests/test_handlers.py' --- a/Mailman/tests/test_handlers.py 2007-09-28 02:33:04 +0000 +++ b/Mailman/tests/test_handlers.py 1970-01-01 00:00:00 +0000 @@ -1,116 +0,0 @@ -# Copyright (C) 2001-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. - -"""Unit tests for the various Mailman/Handlers/*.py modules.""" - -import email -import unittest - -from Mailman import Errors -from Mailman import Message -from Mailman import passwords -from Mailman.configuration import config - -from Mailman.Handlers import Approve -# Don't test handlers such as SMTPDirect and Sendmail here - - - -def password(cleartext): - return passwords.make_secret(cleartext, passwords.Schemes.ssha) - - - -class TestApprove(unittest.TestCase): - def test_short_circuit(self): - msgdata = {'approved': 1} - rtn = Approve.process(self._mlist, None, msgdata) - # Not really a great test, but there's little else to assert - self.assertEqual(rtn, None) - - def test_approved_moderator(self): - mlist = self._mlist - mlist.mod_password = password('wazoo') - msg = email.message_from_string("""\ -Approved: wazoo - -""") - msgdata = {} - Approve.process(mlist, msg, msgdata) - self.failUnless(msgdata.has_key('approved')) - self.assertEqual(msgdata['approved'], 1) - - def test_approve_moderator(self): - mlist = self._mlist - mlist.mod_password = password('wazoo') - msg = email.message_from_string("""\ -Approve: wazoo - -""") - msgdata = {} - Approve.process(mlist, msg, msgdata) - self.failUnless(msgdata.has_key('approved')) - self.assertEqual(msgdata['approved'], 1) - - def test_approved_admin(self): - mlist = self._mlist - mlist.password = password('wazoo') - msg = email.message_from_string("""\ -Approved: wazoo - -""") - msgdata = {} - Approve.process(mlist, msg, msgdata) - self.failUnless(msgdata.has_key('approved')) - self.assertEqual(msgdata['approved'], 1) - - def test_approve_admin(self): - mlist = self._mlist - mlist.password = password('wazoo') - msg = email.message_from_string("""\ -Approve: wazoo - -""") - msgdata = {} - Approve.process(mlist, msg, msgdata) - self.failUnless(msgdata.has_key('approved')) - self.assertEqual(msgdata['approved'], 1) - - def test_unapproved(self): - mlist = self._mlist - mlist.password = password('zoowa') - msg = email.message_from_string("""\ -Approve: wazoo - -""") - msgdata = {} - Approve.process(mlist, msg, msgdata) - self.assertEqual(msgdata.get('approved'), None) - - def test_trip_beentheres(self): - mlist = self._mlist - msg = email.message_from_string("""\ -X-BeenThere: %s - -""" % mlist.GetListEmail()) - self.assertRaises(Errors.LoopError, Approve.process, mlist, msg, {}) - - - -def test_suite(): - suite = unittest.TestSuite() - return suite === added file 'Mailman/docs/approve.txt' --- a/Mailman/docs/approve.txt 1970-01-01 00:00:00 +0000 +++ b/Mailman/docs/approve.txt 2007-10-06 19:09:34 +0000 @@ -0,0 +1,418 @@ +Pre-approved postings +===================== + +Messages can contain a pre-approval, which is used to bypass the message +approval queue. This has several use cases: + +- A list administrator can send an emergency message to the mailing list from + an unregistered address, say if they are away from their normal email. + +- An automated script can be programmed to send a message to an otherwise + moderated list. + +In order to support this, a mailing list can be given a 'moderator password' +which is shared among all the administrators. + + >>> from Mailman.Handlers.Approve import process + >>> from Mailman.database import flush + >>> from Mailman.configuration import config + >>> mlist = config.db.list_manager.create('[EMAIL PROTECTED]') + + +Short circuiting +---------------- + +The message may have been approved by some other means, as evident in the +message metadata. In this case, the handler returns immediately. + + >>> from email import message_from_string + >>> from Mailman.Message import Message + >>> msg = message_from_string("""\ + ... From: [EMAIL PROTECTED] + ... + ... An important message. + ... """, Message) + >>> msgdata = {'approved': True} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + <BLANKLINE> + An important message. + <BLANKLINE> + >>> msgdata + {'approved': True} + + +The Approved header +------------------- + +If the moderator password is given in an Approved header, then the message +gets sent through with no further posting moderation. The Approved header is +not stripped in this handler module, but instead in the Cleanse module. This +ensures that no moderator approval password in the headers will leak out. + + >>> mlist.moderator_password = 'abcxyz' + >>> flush() + >>> msg['Approved'] = 'abcxyz' + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + Approved: abcxyz + <BLANKLINE> + An important message. + <BLANKLINE> + >>> sorted(msgdata.items()) + [('adminapproved', True), ('approved', True)] + +But if the wrong password is given, then the message is not marked as being +approved. The header is still removed though. + + >>> del msg['Approved'] + >>> msg['Approved'] = '123456' + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + Approved: 123456 + <BLANKLINE> + An important message. + <BLANKLINE> + >>> msgdata + {} + +In the spirit of being liberal in what you accept, using an Approve header is +completely synonymous. + + >>> del msg['Approved'] + >>> msg['Approve'] = 'abcxyz' + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + Approve: abcxyz + <BLANKLINE> + An important message. + <BLANKLINE> + >>> sorted(msgdata.items()) + [('adminapproved', True), ('approved', True)] + + >>> del msg['Approve'] + >>> msg['Approve'] = '123456' + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + Approve: 123456 + <BLANKLINE> + An important message. + <BLANKLINE> + >>> msgdata + {} + + +Using a pseudo-header +--------------------- + +Different mail user agents have varying degrees to which they support custom +headers like Approve and Approved. For this reason, Mailman also supports +using a 'pseudo-header', which is really just the first non-whitespace line in +the payload of the message of the message. If this pseudo-header looks like a +matching Approve or Approved header, the message is similarly allowed to pass. + + >>> msg = message_from_string("""\ + ... From: [EMAIL PROTECTED] + ... + ... Approved: abcxyz + ... An important message. + ... """, Message) + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + <BLANKLINE> + An important message. + <BLANKLINE> + >>> sorted(msgdata.items()) + [('adminapproved', True), ('approved', True)] + + >>> msg = message_from_string("""\ + ... From: [EMAIL PROTECTED] + ... + ... Approve: abcxyz + ... An important message. + ... """, Message) + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + <BLANKLINE> + An important message. + <BLANKLINE> + >>> sorted(msgdata.items()) + [('adminapproved', True), ('approved', True)] + +As before, a mismatch in the pseudo-header does not approve the message, but +the pseudo-header line is still removed. + + >>> msg = message_from_string("""\ + ... From: [EMAIL PROTECTED] + ... + ... Approved: 123456 + ... An important message. + ... """, Message) + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + <BLANKLINE> + An important message. + <BLANKLINE> + >>> msgdata + {} + + >>> msg = message_from_string("""\ + ... From: [EMAIL PROTECTED] + ... + ... Approve: 123456 + ... An important message. + ... """, Message) + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + <BLANKLINE> + An important message. + <BLANKLINE> + >>> msgdata + {} + + +MIME multipart support +---------------------- + +Mailman searches for the pseudo-header as the first non-whitespace line in the +first text/plain message part of the message. This allows the feature to be +used with MIME documents. + + >>> msg = message_from_string("""\ + ... From: [EMAIL PROTECTED] + ... MIME-Version: 1.0 + ... Content-Type: multipart/mixed; boundary="AAA" + ... + ... --AAA + ... Content-Type: application/x-ignore + ... + ... Approved: 123456 + ... The above line will be ignored. + ... + ... --AAA + ... Content-Type: text/plain + ... + ... Approved: abcxyz + ... An important message. + ... --AAA-- + ... """, Message) + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="AAA" + <BLANKLINE> + --AAA + Content-Type: application/x-ignore + <BLANKLINE> + Approved: 123456 + The above line will be ignored. + <BLANKLINE> + --AAA + Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + <BLANKLINE> + An important message. + --AAA-- + <BLANKLINE> + >>> sorted(msgdata.items()) + [('adminapproved', True), ('approved', True)] + + >>> msg = message_from_string("""\ + ... From: [EMAIL PROTECTED] + ... MIME-Version: 1.0 + ... Content-Type: multipart/mixed; boundary="AAA" + ... + ... --AAA + ... Content-Type: application/x-ignore + ... + ... Approve: 123456 + ... The above line will be ignored. + ... + ... --AAA + ... Content-Type: text/plain + ... + ... Approve: abcxyz + ... An important message. + ... --AAA-- + ... """, Message) + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="AAA" + <BLANKLINE> + --AAA + Content-Type: application/x-ignore + <BLANKLINE> + Approve: 123456 + The above line will be ignored. + <BLANKLINE> + --AAA + Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + <BLANKLINE> + An important message. + --AAA-- + <BLANKLINE> + >>> sorted(msgdata.items()) + [('adminapproved', True), ('approved', True)] + +Here, the correct password is in the non-text/plain part, so it is ignored. + + >>> msg = message_from_string("""\ + ... From: [EMAIL PROTECTED] + ... MIME-Version: 1.0 + ... Content-Type: multipart/mixed; boundary="AAA" + ... + ... --AAA + ... Content-Type: application/x-ignore + ... + ... Approve: abcxyz + ... The above line will be ignored. + ... + ... --AAA + ... Content-Type: text/plain + ... + ... Approve: 123456 + ... An important message. + ... --AAA-- + ... """, Message) + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="AAA" + <BLANKLINE> + --AAA + Content-Type: application/x-ignore + <BLANKLINE> + Approve: abcxyz + The above line will be ignored. + <BLANKLINE> + --AAA + Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + <BLANKLINE> + An important message. + --AAA-- + >>> msgdata + {} + + >>> msg = message_from_string("""\ + ... From: [EMAIL PROTECTED] + ... MIME-Version: 1.0 + ... Content-Type: multipart/mixed; boundary="AAA" + ... + ... --AAA + ... Content-Type: application/x-ignore + ... + ... Approve: abcxyz + ... The above line will be ignored. + ... + ... --AAA + ... Content-Type: text/plain + ... + ... Approve: 123456 + ... An important message. + ... --AAA-- + ... """, Message) + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="AAA" + <BLANKLINE> + --AAA + Content-Type: application/x-ignore + <BLANKLINE> + Approve: abcxyz + The above line will be ignored. + <BLANKLINE> + --AAA + Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + <BLANKLINE> + An important message. + --AAA-- + >>> msgdata + {} + + >>> msg = message_from_string("""\ + ... From: [EMAIL PROTECTED] + ... MIME-Version: 1.0 + ... Content-Type: multipart/mixed; boundary="AAA" + ... + ... --AAA + ... Content-Type: application/x-ignore + ... + ... Approved: abcxyz + ... The above line will be ignored. + ... + ... --AAA + ... Content-Type: text/plain + ... + ... Approved: 123456 + ... An important message. + ... --AAA-- + ... """, Message) + >>> msgdata = {} + >>> process(mlist, msg, msgdata) + >>> print msg.as_string() + From: [EMAIL PROTECTED] + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="AAA" + <BLANKLINE> + --AAA + Content-Type: application/x-ignore + <BLANKLINE> + Approved: abcxyz + The above line will be ignored. + <BLANKLINE> + --AAA + Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + <BLANKLINE> + An important message. + --AAA-- + <BLANKLINE> + >>> msgdata + {} === modified file 'Mailman/Handlers/Approve.py' --- a/Mailman/Handlers/Approve.py 2007-06-09 19:31:51 +0000 +++ b/Mailman/Handlers/Approve.py 2007-10-06 19:09:34 +0000 @@ -15,13 +15,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. -"""Determine whether the message is approved for delivery. - -This module only tests for definitive approvals. IOW, this module only -determines whether the message is definitively approved or definitively -denied. Situations that could hold a message for approval or confirmation are -not tested by this module. -""" +"""Determine whether the message is pre-approved for delivery.""" import re @@ -30,7 +24,7 @@ from Mailman import Errors from Mailman.configuration import config -NL = '\n' +EMPTYSTRING = '' @@ -38,18 +32,15 @@ # Short circuits if msgdata.get('approved'): # Digests, Usenet postings, and some other messages come pre-approved. - # TBD: we may want to further filter Usenet messages, so the test - # above may not be entirely correct. + # XXX we may want to further filter Usenet messages, so the test above + # may not be entirely correct. return # See if the message has an Approved or Approve header with a valid - # list-moderator, list-admin. Also look at the first non-whitespace line - # in the file to see if it looks like an Approved header. We are - # specifically /not/ allowing the site admins password to work here - # because we want to discourage the practice of sending the site admin - # password through email in the clear. - missing = [] - passwd = msg.get('approved', msg.get('approve', missing)) - if passwd is missing: + # moderator password. Also look at the first non-whitespace line in the + # file to see if it looks like an Approved header. + missing = object() + password = msg.get('approved', msg.get('approve', missing)) + if password is missing: # Find the first text/plain part in the message part = None stripped = False @@ -57,22 +48,19 @@ break # XXX I'm not entirely sure why, but it is possible for the payload of # the part to be None, and you can't splitlines() on None. - if part is not None and part.get_payload() is not None: - lines = part.get_payload(decode=True).splitlines() - line = '' - for lineno, line in zip(range(len(lines)), lines): + if part and part.get_payload() is not None: + lines = part.get_payload(decode=True).splitlines(True) + for lineno, line in enumerate(lines): if line.strip(): break - i = line.find(':') - if i >= 0: - name = line[:i] - value = line[i+1:] - if name.lower() in ('approve', 'approved'): - passwd = value.lstrip() + if ':' in line: + header, value = line.split(':', 1) + if header.lower() in ('approved', 'approve'): + password = value.strip() # Now strip the first line from the payload so the # password doesn't leak. del lines[lineno] - reset_payload(part, NL.join(lines)) + reset_payload(part, EMPTYSTRING.join(lines)) stripped = True if stripped: # MAS: Bug 1181161 - Now try all the text parts in case it's @@ -84,35 +72,35 @@ # # This will process all the multipart/alternative parts in the # message as well as all other text parts. We shouldn't find the - # pattern outside the mp/a parts, but if we do, it is probably - # best to delete it anyway as it does contain the password. + # pattern outside the multipart/alternative parts, but if we do, + # it is probably best to delete it anyway as it does contain the + # password. # # Make a pattern to delete. We can't just delete a line because # line of HTML or other fancy text may include additional message # text. This pattern works with HTML. It may not work with rtf # or whatever else is possible. - pattern = name + ':(\s| )*' + re.escape(passwd) + pattern = header + ':(\s| )*' + re.escape(password) for part in typed_subpart_iterator(msg, 'text'): if part is not None and part.get_payload() is not None: lines = part.get_payload(decode=True) if re.search(pattern, lines): reset_payload(part, re.sub(pattern, '', lines)) - if passwd is not missing and mlist.Authenticate((config.AuthListModerator, - config.AuthListAdmin), - passwd): + if password is not missing and password == mlist.moderator_password: # BAW: should we definitely deny if the password exists but does not # match? For now we'll let it percolate up for further determination. msgdata['approved'] = True # Used by the Emergency module msgdata['adminapproved'] = True - # has this message already been posted to this list? + # Has this message already been posted to this list? beentheres = [s.strip().lower() for s in msg.get_all('x-beenthere', [])] - if mlist.GetListEmail().lower() in beentheres: + if mlist.posting_address in beentheres: raise Errors.LoopError + def reset_payload(part, payload): # Set decoded payload maintaining content-type, format and delsp. - # TK: Message with 'charset=' cause trouble. So, instead of + # TK: Messages with 'charset=' cause trouble. So, instead of # part.get_content_charset('us-ascii') ... cset = part.get_content_charset() or 'us-ascii' ctype = part.get_content_type() === modified file 'Mailman/database/model/mailinglist.py' --- a/Mailman/database/model/mailinglist.py 2007-09-28 02:15:00 +0000 +++ b/Mailman/database/model/mailinglist.py 2007-10-06 19:09:34 +0000 @@ -120,7 +120,7 @@ has_field('member_moderation_action', Boolean), has_field('member_moderation_notice', Unicode), has_field('mime_is_default_digest', Boolean), - has_field('mod_password', Unicode), + has_field('moderator_password', Unicode), has_field('msg_footer', Unicode), has_field('msg_header', Unicode), has_field('new_member_options', Integer), @@ -132,7 +132,6 @@ has_field('obscure_addresses', Boolean), has_field('pass_filename_extensions', PickleType), has_field('pass_mime_types', PickleType), - has_field('password', Unicode), has_field('personalize', Integer), has_field('post_id', Integer), has_field('preferred_language', Unicode), -- https://code.launchpad.net/~mailman-coders/mailman/3.0 You are receiving this branch notification because you are subscribed to it. To unsubscribe from this branch go to https://code.launchpad.net/~mailman-coders/mailman/3.0/+subscription/mailman-checkins. _______________________________________________ Mailman-checkins mailing list Mailman-checkins@python.org Unsubscribe: http://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org