← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~sinzui/launchpad/mailing-list-email-0 into lp:launchpad

 

Curtis Hovey has proposed merging lp:~sinzui/launchpad/mailing-list-email-0 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #689431 change the mailing lists default signature
  https://bugs.launchpad.net/bugs/689431


Fix the footer in mailman emails.

    Launchpad bug: https://bugs.launchpad.net/bugs/689431
    Pre-implementation: no one
    Test command: ./bin/test -vv -t mailman.tests.test_

Bug #689431 [change the mailing lists default signature]
    The mailman email signature is not interpreted by the mail clients like a
    real signature and appear in the quote when you write a reply.

--------------------------------------------------------------------

RULES

Bug #689431 [change the mailing lists default signature]
    * Used '-- ' like all other Lp emails
    * Replace the decorations test with a unittest
    * If time permits, add tests for lpstanding and lphandler

QA

Bug #689431 [change the mailing lists default signature]
    * Send an email to a list.
    * verify the footer is separated from the body by '-- '


LINT

    lib/lp/services/mailman/doc/basic-integration.txt
    lib/lp/services/mailman/doc/contact-address.txt
    lib/lp/services/mailman/doc/postings.txt
    lib/lp/services/mailman/monkeypatches/lphandler.py
    lib/lp/services/mailman/monkeypatches/lpstanding.py
    lib/lp/services/mailman/monkeypatches/mm_cfg.py.in
    lib/lp/services/mailman/testing/__init__.py
    lib/lp/services/mailman/tests/test_lphandler.py
    lib/lp/services/mailman/tests/test_lpheaders.py
    lib/lp/services/mailman/tests/test_lpstanding.py

^ There is lint in the old doctests. I can clean up the white space and
line length issues before I land my branch.


IMPLEMENTATION

Changed the mailman footer, made it like other footers from Launchpad. Added
test_lpheaders to replace decorations.txt.  The `mlist.use_dollar_strings`
change was required to test the actual formatting because lp lists are
automatically set to use $substitutions.
    lib/lp/services/mailman/doc/basic-integration.txt
    lib/lp/services/mailman/doc/contact-address.txt
    lib/lp/services/mailman/doc/postings.txt
    lib/lp/services/mailman/monkeypatches/mm_cfg.py.in
    lib/lp/services/mailman/testing/__init__.p
    lib/lp/services/mailman/tests/test_lpheaders.py

Added replacement tests for lpstanding and lphandler. Both the the modules
were needed updating to get the xmlrpc proxy using the proper method that
can also be tested. I deleted the standing/first post moderation section of
the posting.txt since it was not directly testing our standing code.
    lib/lp/services/mailman/monkeypatches/lphandler.py
    lib/lp/services/mailman/monkeypatches/lpstanding.py
    lib/lp/services/mailman/tests/test_lphandler.py
    lib/lp/services/mailman/tests/test_lpstanding.py
-- 
https://code.launchpad.net/~sinzui/launchpad/mailing-list-email-0/+merge/43656
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/mailing-list-email-0 into lp:launchpad.
=== modified file 'lib/lp/services/mailman/doc/basic-integration.txt'
--- lib/lp/services/mailman/doc/basic-integration.txt	2010-09-17 20:46:58 +0000
+++ lib/lp/services/mailman/doc/basic-integration.txt	2010-12-14 15:21:18 +0000
@@ -113,8 +113,4 @@
     X-RcptTo: anne.person@xxxxxxxxxxx
     <BLANKLINE>
     This is a test message.
-    _______________________________________________
-    Mailing list: http://launchpad.dev/~alpha
-    Post to     : alpha@xxxxxxxxxxxxxxxxxxx
-    Unsubscribe : http://launchpad.dev/~alpha
-    More help   : http://help.launchpad.dev/ListHelp
+    ...

=== modified file 'lib/lp/services/mailman/doc/contact-address.txt'
--- lib/lp/services/mailman/doc/contact-address.txt	2010-10-19 01:42:48 +0000
+++ lib/lp/services/mailman/doc/contact-address.txt	2010-12-14 15:21:18 +0000
@@ -135,11 +135,7 @@
     --
     You received this question notification because you are a member of
     Itest One, which is an answer contact for Mozilla Firefox.
-    _______________________________________________
-    Mailing list: http://launchpad.dev/~itest-one
-    Post to     : itest-one@xxxxxxxxxxxxxxxxxxx
-    Unsubscribe : http://launchpad.dev/~itest-one
-    More help   : http://help.launchpad.dev/ListHelp
+    ...
 
 Here is the message to the archive submission address.
 
@@ -199,11 +195,7 @@
     <BLANKLINE>
     --
       http://blueprints.launchpad.dev:.../firefox/+spec/canvas
-    _______________________________________________
-    Mailing list: http://launchpad.dev/~itest-one
-    Post to     : itest-one@xxxxxxxxxxxxxxxxxxx
-    Unsubscribe : http://launchpad.dev/~itest-one
-    More help   : http://help.launchpad.dev/ListHelp
+    ...
 
 ...and this one gets sent to the archives.
 
@@ -226,11 +218,7 @@
     <BLANKLINE>
     --
       http://blueprints.launchpad.dev:.../firefox/+spec/canvas
-    _______________________________________________
-    Mailing list: http://launchpad.dev/~itest-one
-    Post to     : itest-one@xxxxxxxxxxxxxxxxxxx
-    Unsubscribe : http://launchpad.dev/~itest-one
-    More help   : http://help.launchpad.dev/ListHelp
+    ...
 
 The team's contact address is set to each team member individually.
 Notifications will no longer be sent to the mailing list.
@@ -352,11 +340,7 @@
     X-RcptTo: anne.person@xxxxxxxxxxx
     <BLANKLINE>
     Hi, I am a member of Launchpad.
-    _______________________________________________
-    Mailing list: http://launchpad.dev/~itest-one
-    Post to     : itest-one@xxxxxxxxxxxxxxxxxxx
-    Unsubscribe : http://launchpad.dev/~itest-one
-    More help   : http://help.launchpad.dev/ListHelp
+    ...
 
     >>> print messages[1].as_string()
     From: anne.person@xxxxxxxxxxx
@@ -372,8 +356,4 @@
     X-RcptTo: archive@xxxxxxxxxxxxxxxx
     <BLANKLINE>
     Hi, I am a member of Launchpad.
-    _______________________________________________
-    Mailing list: http://launchpad.dev/~itest-one
-    Post to     : itest-one@xxxxxxxxxxxxxxxxxxx
-    Unsubscribe : http://launchpad.dev/~itest-one
-    More help   : http://help.launchpad.dev/ListHelp
+    ...

=== removed file 'lib/lp/services/mailman/doc/decorations.txt'
--- lib/lp/services/mailman/doc/decorations.txt	2010-10-19 01:42:48 +0000
+++ lib/lp/services/mailman/doc/decorations.txt	1970-01-01 00:00:00 +0000
@@ -1,169 +0,0 @@
-===========
-Decorations
-===========
-
-Messages sent by Mailman to team mailing list recipients have a number of
-decorations that help users interact better with the list and its archive.
-These decorations live in the message headers, and in the text footer that
-Mailman adds to the bottom of very message.
-
-A mailing list is created, and Anne subscribes to it.
-
-    >>> from lp.services.mailman.testing import helpers
-    >>> list_one = helpers.create_list('itest-one')
-    >>> helpers.subscribe('Anne', 'itest-one')
-
-Anne sends a message to the list.
-
-    >>> smtpd.reset()
-    >>> from Mailman.Post import inject
-    >>> inject('itest-one', """\
-    ... From: anne.person@xxxxxxxxxxx
-    ... To: itest-one@xxxxxxxxxxxxxxxxxxx
-    ... Subject: A member post
-    ... Message-ID: <first-injection>
-    ...
-    ... Hi, I am a member of this team's list.
-    ... """)
-
-    # Wait for two deliveries, one to Anne and the other to the archiver.
-    >>> smtpd_watcher.wait_for_mbox_delivery('first-injection')
-    >>> smtpd_watcher.wait_for_mbox_delivery('first-injection')
-
-
-VERP headers
-============
-
-VERP stands for Variable Envelope Return Path and it is a technique used to
-track users for bounce processing.  The idea is that you encode the actual
-subscribed address in the Sender and Errors-To headers so that they can be
-extracted if a bounce is ever received.  This works because remote MTAs must
-bounce to this address, regardless of how many intervening hops, forwards, or
-rewrites there are.
-
-(Technically, Mailman doesn't do VERP because that's defined as happening in
-the MTA, but it's close enough in intent and exactly the same in
-implementation so we use the same terminology in Mailman.)
-
-    >>> from operator import itemgetter
-    >>> messages = sorted(smtpd, key=itemgetter('sender'))
-    >>> for message in messages:
-    ...     print message['sender']
-    ...     print message['errors-to']
-    ...     print '---'
-    itest-one-bounces+anne.person=example.com@xxxxxxxxxxxxxxxxxxx
-    itest-one-bounces+anne.person=example.com@xxxxxxxxxxxxxxxxxxx
-    ---
-    itest-one-bounces+archive=mail-archive.dev@xxxxxxxxxxxxxxxxxxx
-    itest-one-bounces+archive=mail-archive.dev@xxxxxxxxxxxxxxxxxxx
-    ---
-
-For the following discussion, it doesn't matter which message we look at in
-detail, so the last one in the sorted sequence is just fine.
-
-
-RFC 2369 headers
-================
-
-RFC 2369 defines a set of headers for mailing lists, often called the List-*
-headers due to the common prefix of these headers.
-
-
-List-Help
----------
-
-The List-Help header points to "an instructive website" to provide help to
-users.
-
-    >>> print message['list-help']
-    <http://help.launchpad.dev/ListHelp>
-
-
-List-Id
--------
-
-The List-Id header uniquely identifies the mailing list.  Mailman crafts the
-List-Id header using the team name and the host name on which the lists
-reside.
-
-    >>> print message['list-id']
-    <itest-one.lists.launchpad.dev>
-
-
-List-Subscribe and List-Unsubscribe
------------------------------------
-
-The List-Unsubscribe header points to the contact address of the mechanism the
-user can access to unsubscribe from the mailing list.  Because we have not yet
-enabled the email command robot for Launchpad, the only way to unsubscribe is
-for a user to hit her email settings page.  The List-Subscribe header is
-similar.
-
-    >>> print message['list-subscribe']
-    <http://launchpad.dev/~itest-one>
-    >>> print message['list-unsubscribe']
-    <http://launchpad.dev/~itest-one>
-
-
-List-Archive
-------------
-
-The List-Archive header describes how to access the archives for this mailing
-list.  It is a list-wide url, not a url-specific to the individual message
-(see draft RFC 5064 for that).
-
-This url should exactly match the link given on the team's overview page.
-
-    >>> from canonical.testing.layers import BaseLayer
-    >>> root_url = BaseLayer.appserver_root_url()
-    >>> browser = Browser('no-priv@xxxxxxxxxxxxx:test')
-    >>> browser.open('%s/~itest-one' % root_url)
-    >>> browser.getLink(id='mailing-list-archive')
-    <Link text='View archive' url='http://lists.launchpad.dev/itest-one'>
-    >>> print message['list-archive']
-    <http://lists.launchpad.dev/itest-one>
-
-
-List-Post
----------
-
-The List-Post header is a mailto url that tells people where they can post new
-messages for this mailing list.
-
-    >>> print message['list-post']
-    <mailto:itest-one@xxxxxxxxxxxxxxxxxxx>
-
-
-List-Owner
-----------
-
-The List-Owner contains a url pointing to the human contact for the mailing
-list.  This header may be omitted if it is the same as the postmaster, but in
-our case, we point this at the team owning the mailing list.
-
-    >>> print message['list-owner']
-    <http://launchpad.dev/~itest-one>
-
-
-Message footer
-==============
-
-Mailman appends a footer of helpful text on each message.  This footer
-contains a link to this message's archive (which should be the same as the RFC
-5064 link), the message's posting address, the unsubscribe address, etc.
-The easiest way to see the footer is to just scan the message body for the
-separator.
-
-    >>> body_lines = message.get_payload().splitlines(True)
-    >>> for line_number, line in enumerate(body_lines):
-    ...     if line.startswith('_____'):
-    ...         break
-    ... else:
-    ...     raise AssertionError('No footer found')
-    >>> print ''.join(body_lines[line_number + 1:])
-    Mailing list: http://launchpad.dev/~itest-one
-    Post to     : itest-one@xxxxxxxxxxxxxxxxxxx
-    Unsubscribe : http://launchpad.dev/~itest-one
-    More help   : http://help.launchpad.dev/ListHelp
-    <BLANKLINE>
-    <BLANKLINE>

=== modified file 'lib/lp/services/mailman/doc/postings.txt'
--- lib/lp/services/mailman/doc/postings.txt	2010-12-13 19:58:54 +0000
+++ lib/lp/services/mailman/doc/postings.txt	2010-12-14 15:21:18 +0000
@@ -633,160 +633,6 @@
     <iguana>
 
 
-First post moderation
-=====================
-
-Normally, Launchpad members who are not subscribed to the mailing list will
-have their posts held for moderation.
-
-    >>> login('admin@xxxxxxxxxxxxx')
-    >>> bart = factory.makePersonByName('Bart')
-    >>> transaction.commit()
-    >>> bart.personal_standing
-    <DBItem PersonalStanding.UNKNOWN...
-
-    >>> def print_message_summaries():
-    ...     messages = sorted(smtpd, key=itemgetter('sender'))
-    ...     print 'Number of messages:', len(messages)
-    ...     for message in messages:
-    ...         print message['sender']
-    ...         print '   ', message['message-id']
-    ...         print '   ', message['from']
-    ...         print '   ', message['subject']
-
-    >>> inject('itest-one', """\
-    ... From: bperson@xxxxxxxxxxx
-    ... To: itest-one@xxxxxxxxxxxxxxxxxxx
-    ... Subject: A non-member post
-    ... Message-ID: <jackal>
-    ...
-    ... Hi, I am not a member of the mailing list.
-    ... """)
-    >>> vette_watcher.wait_for_hold('itest-one', 'jackal')
-    >>> print_message_summaries()
-    Number of messages: 1
-    bounces@xxxxxxxxxxxxx
-        ...
-        Itest One <noreply@xxxxxxxxxxxxx>
-        New mailing list message requiring approval for Itest One
-
-However, a Launchpad member in good standing is allowed to post to any mailing
-list.
-
-    >>> from lp.registry.interfaces.person import PersonalStanding
-    >>> login('admin@xxxxxxxxxxxxx')
-    >>> bart.personal_standing = PersonalStanding.GOOD
-    >>> transaction.commit()
-
-    >>> inject('itest-one', """\
-    ... From: bart.person@xxxxxxxxxxx
-    ... To: itest-one@xxxxxxxxxxxxxxxxxxx
-    ... Subject: A member in good standing
-    ... Message-ID: <kestrel>
-    ...
-    ... Hi, I am a Launchpad member in good standing!
-    ... """)
-
-    # Wait for mailing list deliveries to both recipients.
-    >>> smtpd_watcher.wait_for_mbox_delivery('kestrel')
-    >>> smtpd_watcher.wait_for_mbox_delivery('kestrel')
-    >>> print_message_summaries()
-    Number of messages: 2
-    itest-one-bounces+anne.person=example.com@xxxxxxxxxxxxxxxxxxx
-        <kestrel>
-        bart.person@xxxxxxxxxxx
-        [Itest-one] A member in good standing
-    itest-one-bounces+archive=mail-archive.dev@xxxxxxxxxxxxxxxxxxx
-        <kestrel>
-        bart.person@xxxxxxxxxxx
-        [Itest-one] A member in good standing
-
-A member in excellent standing can of course also post to the list.
-
-    >>> bart.personal_standing = PersonalStanding.EXCELLENT
-    >>> transaction.commit()
-
-    >>> inject('itest-one', """\
-    ... From: bart.person@xxxxxxxxxxx
-    ... To: itest-one@xxxxxxxxxxxxxxxxxxx
-    ... Subject: A member in excellent standing
-    ... Message-ID: <llama>
-    ...
-    ... Hi, I am a Launchpad member in excellent standing!
-    ... """)
-
-    # Wait for mailing list deliveries to both recipients.
-    >>> smtpd_watcher.wait_for_mbox_delivery('llama')
-    >>> smtpd_watcher.wait_for_mbox_delivery('llama')
-    >>> print_message_summaries()
-    Number of messages: 2
-    itest-one-bounces+anne.person=example.com@xxxxxxxxxxxxxxxxxxx
-        <llama>
-        bart.person@xxxxxxxxxxx
-        [Itest-one] A member in excellent standing
-    itest-one-bounces+archive=mail-archive.dev@xxxxxxxxxxxxxxxxxxx
-        <llama>
-        bart.person@xxxxxxxxxxx
-        [Itest-one] A member in excellent standing
-
-But a person in poor standing will have their messages held for approval.
-
-    >>> bart.personal_standing = PersonalStanding.POOR
-    >>> transaction.commit()
-
-    >>> inject('itest-one', """\
-    ... From: bperson@xxxxxxxxxxx
-    ... To: itest-one@xxxxxxxxxxxxxxxxxxx
-    ... Subject: A non-member post
-    ... Message-ID: <mongoose>
-    ...
-    ... Hi, I am not a member of the mailing list.
-    ... """)
-    >>> vette_watcher.wait_for_hold('itest-one', 'mongoose')
-    >>> print_message_summaries()
-    Number of messages: 1
-    bounces@xxxxxxxxxxxxx
-        ...
-        Itest One <noreply@xxxxxxxxxxxxx>
-        New mailing list message requiring approval for Itest One
-
-Should a non-team member's held post be approved, they are then allowed to
-post to just that mailing list without further approval required.
-
-    >>> browser.open('%s/~itest-one/+mailinglist-moderate' % root_url)
-    >>> browser.getControl(name='field.%3Cmongoose%3E').value = ['approve']
-    >>> browser.getControl('Moderate').click()
-    >>> smtpd_watcher.wait_for_mbox_delivery('mongoose')
-    >>> smtpd_watcher.wait_for_mbox_delivery('mongoose')
-    >>> helpers.ensure_addresses_are_enabled(
-    ...     'itest-one', 'bperson@xxxxxxxxxxx')
-
-    >>> smtpd.reset()
-    >>> inject('itest-one', """\
-    ... From: bperson@xxxxxxxxxxx
-    ... To: itest-one@xxxxxxxxxxxxxxxxxxx
-    ... Subject: A non-member post
-    ... Message-ID: <otter>
-    ...
-    ... Hi, I am not a member of the mailing list.
-    ... """)
-    >>> smtpd_watcher.wait_for_mbox_delivery('otter')
-    >>> smtpd_watcher.wait_for_mbox_delivery('otter')
-    >>> messages = sorted(smtpd, key=itemgetter('sender'))
-    >>> len(messages)
-    2
-    >>> for message in messages:
-    ...     print message['sender']
-    ...     print message['subject']
-    ...     print message['message-id']
-    itest-one-bounces+anne.person=example.com@xxxxxxxxxxxxxxxxxxx
-    [Itest-one] A non-member post
-    <otter>
-    itest-one-bounces+archive=mail-archive.dev@xxxxxxxxxxxxxxxxxxx
-    [Itest-one] A non-member post
-    <otter>
-
-
 Preventing archiver forgeries
 =============================
 

=== modified file 'lib/lp/services/mailman/monkeypatches/lphandler.py'
--- lib/lp/services/mailman/monkeypatches/lphandler.py	2010-08-20 20:31:18 +0000
+++ lib/lp/services/mailman/monkeypatches/lphandler.py	2010-12-14 15:21:18 +0000
@@ -5,7 +5,7 @@
 
 
 import hashlib
-import xmlrpclib
+from Mailman.Queue import XMLRPCRunner
 
 from Mailman import (
     Errors,
@@ -42,7 +42,7 @@
     # can't talk to Launchpad, I believe it's better to let the message get
     # posted to the list than to discard or hold it.
     is_member = True
-    proxy = xmlrpclib.ServerProxy(mm_cfg.XMLRPC_URL)
+    proxy = proxy = XMLRPCRunner.get_mailing_list_api_proxy()
     # This will fail if we can't talk to Launchpad.  That's okay though
     # because Mailman's IncomingRunner will re-queue the message and re-start
     # processing at this handler.

=== modified file 'lib/lp/services/mailman/monkeypatches/lpstanding.py'
--- lib/lp/services/mailman/monkeypatches/lpstanding.py	2009-06-25 05:30:52 +0000
+++ lib/lp/services/mailman/monkeypatches/lpstanding.py	2010-12-14 15:21:18 +0000
@@ -7,9 +7,7 @@
 whether list non-members are allowed to post to a mailing list.
 """
 
-import xmlrpclib
-
-from Mailman import mm_cfg
+from Mailman.Queue import XMLRPCRunner
 
 
 def process(mlist, msg, msgdata):
@@ -26,7 +24,7 @@
     sender = msg.get_sender()
     # Ask Launchpad about the standing of this member.
     in_good_standing = False
-    proxy = xmlrpclib.ServerProxy(mm_cfg.XMLRPC_URL)
+    proxy = XMLRPCRunner.get_mailing_list_api_proxy()
     # This will fail if we can't talk to Launchpad.  That's okay though
     # because Mailman's IncomingRunner will re-queue the message and re-start
     # processing at this handler.

=== modified file 'lib/lp/services/mailman/monkeypatches/mm_cfg.py.in'
--- lib/lp/services/mailman/monkeypatches/mm_cfg.py.in	2010-09-17 20:46:58 +0000
+++ lib/lp/services/mailman/monkeypatches/mm_cfg.py.in	2010-12-14 15:21:18 +0000
@@ -44,7 +44,7 @@
 
 SITE_LIST_OWNER = '%(site_list_owner)s'
 
-DEFAULT_MSG_FOOTER = '''_______________________________________________
+DEFAULT_MSG_FOOTER = '''-- 
 %(footer)s'''
 
 # Set up MHonArc archiving.

=== modified file 'lib/lp/services/mailman/testing/__init__.py'
--- lib/lp/services/mailman/testing/__init__.py	2010-12-13 22:58:24 +0000
+++ lib/lp/services/mailman/testing/__init__.py	2010-12-14 15:21:18 +0000
@@ -62,6 +62,7 @@
         mlist.Create(team.name, owner_email, 'password')
         mlist.host_name = 'lists.launchpad.dev'
         mlist.web_page_url = 'http://lists.launchpad.dev/mailman/'
+        mlist.use_dollar_strings = 1
         mlist.Save()
         mlist.addNewMember(owner_email)
         return mlist

=== added file 'lib/lp/services/mailman/tests/test_lphandler.py'
--- lib/lp/services/mailman/tests/test_lphandler.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/mailman/tests/test_lphandler.py	2010-12-14 15:21:18 +0000
@@ -0,0 +1,83 @@
+# Copyright 20010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+"""Test the LaunchpadMember monekypatches"""
+
+__metaclass__ = type
+__all__ = []
+
+import hashlib
+
+from Mailman import (
+    Errors,
+    mm_cfg,
+    )
+from Mailman.Handlers import LaunchpadMember
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.services.mailman.testing import MailmanTestCase
+
+
+class TestLaunchpadMemberTestCase(MailmanTestCase):
+    """Test lphandler.
+
+    Mailman process() methods quietly return. They may set msg_data key-values
+    or raise an error to end processing. This group of tests tests often check
+    for errors, but that does not mean there is an error condition, it only
+    means message processing has reached a final decision. Messages that do
+    not cause a final decision pass-through and the process() methods ends
+    without a return.
+    """
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestLaunchpadMemberTestCase, self).setUp()
+        self.team, self.mailing_list = self.factory.makeTeamAndMailingList(
+            'team-1', 'team-1-owner')
+        self.mm_list = self.makeMailmanList(self.mailing_list)
+
+    def tearDown(self):
+        super(TestLaunchpadMemberTestCase, self).tearDown()
+        self.cleanMailmanList(self.mm_list)
+
+    def test_messages_from_unknown_senders_are_discarded(self):
+        # A massage from an unknown email address is discarded.
+        message = self.makeMailmanMessage(
+            self.mm_list, 'gerbil@xxxxxxxxxxx', 'subject', 'any content.')
+        msg_data = {}
+        args = (self.mm_list, message, msg_data)
+        self.assertRaises(
+            Errors.DiscardMessage, LaunchpadMember.process, *args)
+
+    def test_preapproved_messages_are_always_accepted(self):
+        # An approved message is accepted even if the email address is
+        # unknown.
+        message = self.makeMailmanMessage(
+            self.mm_list, 'gerbil@xxxxxxxxxxx', 'subject', 'any content.')
+        msg_data = dict(approved=True)
+        silence = LaunchpadMember.process(self.mm_list, message, msg_data)
+        self.assertEqual(None, silence)
+
+    def test_messages_from_launchpad_users_are_accepted(self):
+        # A message from a launchpad user is accepted.
+        lp_user_email = 'chinchila@xxxxxx'
+        lp_user = self.factory.makePerson(email=lp_user_email)
+        message = self.makeMailmanMessage(
+            self.mm_list, lp_user_email, 'subject', 'any content.')
+        msg_data = {}
+        silence = LaunchpadMember.process(self.mm_list, message, msg_data)
+        self.assertEqual(None, silence)
+
+    def test_messages_from_launchpad_itself_are_accepted(self):
+        # A message from launchpad itseld is accepted. Launchpad will sent
+        # a secret.
+        message = self.makeMailmanMessage(
+            self.mm_list, 'guinea-pig@xxxxxxxxxxx', 'subject', 'any content.')
+        message['message-id'] = 'hamster.hamster'
+        hash = hashlib.sha1(mm_cfg.LAUNCHPAD_SHARED_SECRET)
+        hash.update(message['message-id'])
+        message['x-launchpad-hash'] = hash.hexdigest()
+        msg_data = {}
+        silence = LaunchpadMember.process(self.mm_list, message, msg_data)
+        self.assertEqual(None, silence)
+        self.assertEqual(True, msg_data['approved'])

=== added file 'lib/lp/services/mailman/tests/test_lpheaders.py'
--- lib/lp/services/mailman/tests/test_lpheaders.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/mailman/tests/test_lpheaders.py	2010-12-14 15:21:18 +0000
@@ -0,0 +1,103 @@
+# Copyright 20010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+"""Test the lpheaders monekypatches"""
+
+__metaclass__ = type
+__all__ = []
+
+from Mailman.Handlers import (
+    Decorate,
+    LaunchpadHeaders,
+    )
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.services.mailman.testing import MailmanTestCase
+
+
+class TestLaunchpadHeadersTestCase(MailmanTestCase):
+    """Test lpheaders.
+
+    Mailman process() methods quietly return. They may set msg_data key-values
+    or raise an error to end processing. This group of tests tests often check
+    for errors, but that does not mean there is an error condition, it only
+    means message processing has reached a final decision. Messages that do
+    not cause a final decision pass-through and the process() methods ends
+    without a return.
+    """
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestLaunchpadHeadersTestCase, self).setUp()
+        self.team, self.mailing_list = self.factory.makeTeamAndMailingList(
+            'team-1', 'team-1-owner')
+        self.mm_list = self.makeMailmanList(self.mailing_list)
+        self.lp_user_email = 'albatros@xxxxxx'
+        self.lp_user = self.factory.makePerson(
+            name='albatros', email=self.lp_user_email)
+
+    def tearDown(self):
+        super(TestLaunchpadHeadersTestCase, self).tearDown()
+        self.cleanMailmanList(self.mm_list)
+
+    def test_message_launchpad_headers(self):
+        # All messages get updated headers.
+        message = self.makeMailmanMessage(
+            self.mm_list, self.lp_user_email, 'subject', 'any content.')
+        msg_data = {}
+        silence = LaunchpadHeaders.process(self.mm_list, message, msg_data)
+        self.assertEqual(None, silence)
+        self.assertEqual(
+            '<team-1.lists.launchpad.dev>', message['List-Id'])
+        self.assertEqual(
+            '<http://help.launchpad.dev/ListHelp>', message['List-Help'])
+        self.assertEqual(
+            '<http://launchpad.dev/~team-1>', message['List-Subscribe'])
+        self.assertEqual(
+            '<http://launchpad.dev/~team-1>', message['List-Unsubscribe'])
+        self.assertEqual(
+            '<mailto:team-1@xxxxxxxxxxxxxxxxxxx>', message['List-Post'])
+        self.assertEqual(
+            '<http://lists.launchpad.dev/team-1>', message['List-Archive'])
+        self.assertEqual(
+            '<http://launchpad.dev/~team-1>', message['List-Owner'])
+
+    def test_message_decoration_data(self):
+        # The lpheaders process method provides decoration-data.
+        message = self.makeMailmanMessage(
+            self.mm_list, self.lp_user_email, 'subject', 'any content.')
+        msg_data = {}
+        silence = LaunchpadHeaders.process(self.mm_list, message, msg_data)
+        self.assertEqual(None, silence)
+        self.assertTrue('decoration-data' in msg_data)
+        decoration_data = msg_data['decoration-data']
+        self.assertEqual(
+            'http://launchpad.dev/~team-1',
+            decoration_data['list_owner'])
+        self.assertEqual(
+            'team-1@xxxxxxxxxxxxxxxxxxx',
+            decoration_data['list_post'])
+        self.assertEqual(
+            'http://launchpad.dev/~team-1',
+            decoration_data['list_unsubscribe'])
+        self.assertEqual(
+            'http://help.launchpad.dev/ListHelp',
+            decoration_data['list_help'])
+
+    def test_message_decorate_footer(self):
+        # The Decorate handler uses the lpheaders decoration-data.
+        message = self.makeMailmanMessage(
+            self.mm_list, self.lp_user_email, 'subject', 'any content.')
+        msg_data = {}
+        LaunchpadHeaders.process(self.mm_list, message, msg_data)
+        self.assertTrue('decoration-data' in msg_data)
+        silence = Decorate.process(self.mm_list, message, msg_data)
+        self.assertEqual(None, silence)
+        body, footer = message.get_payload()[1].get_payload().rsplit('-- ', 1)
+        expected = (
+            "\n"
+            "Mailing list: http://launchpad.dev/~team-1\n";
+            "Post to     : team-1@xxxxxxxxxxxxxxxxxxx\n"
+            "Unsubscribe : http://launchpad.dev/~team-1\n";
+            "More help   : http://help.launchpad.dev/ListHelp\n";)
+        self.assertEqual(expected, footer)

=== added file 'lib/lp/services/mailman/tests/test_lpstanding.py'
--- lib/lp/services/mailman/tests/test_lpstanding.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/mailman/tests/test_lpstanding.py	2010-12-14 15:21:18 +0000
@@ -0,0 +1,59 @@
+# Copyright 20010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+"""Test the lpstanding monekypatches"""
+
+__metaclass__ = type
+__all__ = []
+
+from Mailman.Handlers import LPStanding
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.registry.interfaces.person import PersonalStanding
+from lp.services.mailman.testing import MailmanTestCase
+from lp.testing import celebrity_logged_in
+
+
+class TestLPStandingTestCase(MailmanTestCase):
+    """Test lpstanding.
+
+    Mailman process() methods quietly return. They may set msg_data key-values
+    or raise an error to end processing. This group of tests tests often check
+    for errors, but that does not mean there is an error condition, it only
+    means message processing has reached a final decision. Messages that do
+    not cause a final decision pass-through and the process() methods ends
+    without a return.
+    """
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestLPStandingTestCase, self).setUp()
+        self.team, self.mailing_list = self.factory.makeTeamAndMailingList(
+            'team-1', 'team-1-owner')
+        self.mm_list = self.makeMailmanList(self.mailing_list)
+        self.lp_user_email = 'beaver@xxxxxx'
+        self.lp_user = self.factory.makePerson(email=self.lp_user_email)
+
+    def tearDown(self):
+        super(TestLPStandingTestCase, self).tearDown()
+        self.cleanMailmanList(self.mm_list)
+
+    def test_non_subscriber_without_good_standing_is_not_approved(self):
+        # Non-subscribers without good standing are not approved to post.
+        message = self.makeMailmanMessage(
+            self.mm_list, self.lp_user_email, 'subject', 'any content.')
+        msg_data = {}
+        silence = LPStanding.process(self.mm_list, message, msg_data)
+        self.assertEqual(None, silence)
+        self.assertFalse('approved' in msg_data)
+
+    def test_non_subscriber_with_good_standing_is_approved(self):
+        # Non-subscribers with good standing are approved to post.
+        with celebrity_logged_in('admin'):
+            self.lp_user.personal_standing = PersonalStanding.GOOD
+        message = self.makeMailmanMessage(
+            self.mm_list, self.lp_user_email, 'subject', 'any content.')
+        msg_data = {}
+        silence = LPStanding.process(self.mm_list, message, msg_data)
+        self.assertEqual(None, silence)
+        self.assertTrue(msg_data['approved'])