← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:remove-mailman into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:remove-mailman into launchpad:master.

Commit message:
Remove Mailman, which is now in a separate tree

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/379464

This now lives in https://git.launchpad.net/lp-mailman.

The SMTP server that ran as part of AppServerLayer was only needed for Mailman integration tests, so drop it too.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:remove-mailman into launchpad:master.
diff --git a/.gitignore b/.gitignore
index 40890fc..25bc29d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,8 +11,6 @@ database/replication/preamble.sk
 database/schema/diagrams/*.ps
 database/schema/diagrams/*.dot
 thread*.request
-lib/Mailman
-lib/mailman
 TAGS
 cover.txt
 lint.txt
diff --git a/Makefile b/Makefile
index aacda12..9fc3323 100644
--- a/Makefile
+++ b/Makefile
@@ -126,12 +126,6 @@ check: clean build $(JS_BUILD_DIR)/.development
 	${PY} -t ./test_on_merge.py $(VERBOSITY) $(TESTOPTS)
 	bzr status --no-pending
 
-check_mailman: build $(JS_BUILD_DIR)/.development
-	# Run all tests, including the Mailman integration
-	# tests. test_on_merge.py takes care of setting up the database.
-	${PY} -t ./test_on_merge.py $(VERBOSITY) $(TESTOPTS) \
-		lp.services.mailman.tests
-
 lint: ${PY} $(JS_BUILD_DIR)/.development
 	@bash ./utilities/lint
 
@@ -266,7 +260,6 @@ compile: $(PY)
 	${SHHH} $(MAKE) -C sourcecode build PYTHON=${PYTHON} \
 	    LPCONFIG=${LPCONFIG}
 	${SHHH} bin/build-twisted-plugin-cache
-	${SHHH} LPCONFIG=${LPCONFIG} ${PY} -t buildmailman.py
 	scripts/update-version-info.sh
 
 test_build: build
@@ -300,7 +293,7 @@ start-gdb: build inplace stop support_files run.gdb
 
 run_all: build inplace stop
 	bin/run \
-	 -r librarian,sftp,forker,mailman,codebrowse,bing-webservice,\
+	 -r librarian,sftp,forker,codebrowse,bing-webservice,\
 	memcached,rabbitmq -i $(LPCONFIG)
 
 run_codebrowse: compile
@@ -346,7 +339,7 @@ stop: build initscript-stop
 # servers, where we know we don't need the extra steps in a full
 # "make stop" because of how the code is deployed/built.
 initscript-stop:
-	bin/killservice librarian launchpad mailman
+	bin/killservice librarian launchpad
 
 shutdown: scheduleoutage stop
 	$(RM) +maintenancetime.txt
@@ -384,16 +377,7 @@ clean_buildout: clean_pip
 clean_logs:
 	$(RM) logs/thread*.request
 
-clean_mailman:
-	$(RM) -r /var/tmp/mailman /var/tmp/mailman-xmlrpc.test
-ifdef LP_MAKE_KEEP_MAILMAN
-	@echo "Keeping previously built mailman."
-else
-	$(RM) lib/Mailman
-	$(RM) -r lib/mailman
-endif
-
-lxc-clean: clean_js clean_mailman clean_pip clean_logs
+lxc-clean: clean_js clean_pip clean_logs
 	# XXX: BradCrittenden 2012-05-25 bug=1004514:
 	# It is important for parallel tests inside LXC that the
 	# $(CODEHOSTING_ROOT) directory not be completely removed.
@@ -404,9 +388,6 @@ lxc-clean: clean_js clean_mailman clean_pip clean_logs
 	if test -f sourcecode/pygettextpo/Makefile; then \
 		$(MAKE) -C sourcecode/pygettextpo clean; \
 	fi
-	if test -f sourcecode/mailman/Makefile; then \
-		$(MAKE) -C sourcecode/mailman clean; \
-	fi
 	$(RM) -r env
 	$(RM) -r $(LP_BUILT_JS_ROOT)/*
 	$(RM) -r $(CODEHOSTING_ROOT)/*
@@ -422,8 +403,6 @@ lxc-clean: clean_js clean_mailman clean_pip clean_logs
 			  /var/tmp/fatsam.test \
 			  /var/tmp/lperr \
 			  /var/tmp/lperr.test \
-			  /var/tmp/mailman \
-			  /var/tmp/mailman-xmlrpc.test \
 			  /var/tmp/ppa \
 			  /var/tmp/ppa.test \
 			  /var/tmp/testkeyserver
@@ -505,7 +484,7 @@ pydoctor:
 		--docformat restructuredtext --verbose-about epytext-summary \
 		$(PYDOCTOR_OPTIONS)
 
-.PHONY: apidoc build_eggs build_wheels check check_config check_mailman	\
+.PHONY: apidoc build_eggs build_wheels check check_config		\
 	clean clean_buildout clean_js clean_logs clean_pip compile	\
 	css_combine debug default doc ftest_build ftest_inplace		\
 	hosted_branches jsbuild jsbuild_widget_css launchpad.pot	\
diff --git a/buildmailman.py b/buildmailman.py
deleted file mode 100644
index 68beb86..0000000
--- a/buildmailman.py
+++ /dev/null
@@ -1,241 +0,0 @@
-#! /usr/bin/python
-#
-# Copyright 2009, 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-from __future__ import print_function
-
-import errno
-import grp
-import os
-import pwd
-import socket
-import subprocess
-import sys
-import tempfile
-
-from lazr.config import as_username_groupname
-
-from lp.services.config import config
-from lp.services.mailman.config import (
-    configure_prefix,
-    configure_siteowner,
-    )
-from lp.services.mailman.monkeypatches import monkey_patch
-
-
-basepath = [part for part in sys.path if part]
-
-
-def build_mailman():
-    # Build and install Mailman if it is enabled and not yet built.
-    if not config.mailman.build:
-        # There's nothing to do.
-        return 0
-    mailman_path = configure_prefix(config.mailman.build_prefix)
-    mailman_bin = os.path.join(mailman_path, 'bin')
-    var_dir = os.path.abspath(config.mailman.build_var_dir)
-    executable = os.path.abspath('bin/py')
-
-    # If we can import the package, we assume Mailman is properly built at
-    # the least.  This does not catch re-installs that might be necessary
-    # should our copy in sourcecode be updated.  Do that manually.
-    try:
-        import Mailman
-    except ImportError:
-        need_build = need_install = True
-    else:
-        need_build = need_install = False
-        # Make sure that the configure prefix is correct, in case this tree
-        # was moved after building Mailman.
-        try:
-            from Mailman import Defaults
-        except ImportError:
-            need_build = need_install = True
-        else:
-            if Defaults.PYTHON != executable:
-                need_build = need_install = True
-                # We'll need to remove this; "make install" won't overwrite
-                # the existing file, and then installation will fail due to
-                # it having the wrong sys.path.
-                try:
-                    os.unlink(
-                        os.path.join(mailman_path, 'Mailman', 'mm_cfg.py'))
-                except OSError as e:
-                    if e.errno != errno.ENOENT:
-                        raise
-    if not need_install:
-        # Also check for Launchpad-specific bits stuck into the source tree by
-        # monkey_patch(), in case this is half-installed.  See
-        # <https://bugs.launchpad.net/launchpad-registry/+bug/683486>.
-        try:
-            from Mailman.Queue import XMLRPCRunner
-            from Mailman.Handlers import LPModerate
-        except ImportError:
-            # Monkey patches not present, redo install and patch steps.
-            need_install = True
-
-    # Make sure the target directories exist and have the correct
-    # permissions, otherwise configure will complain.
-    user, group = as_username_groupname(config.mailman.build_user_group)
-    # Now work backwards to get the uid and gid
-    try:
-        uid = pwd.getpwnam(user).pw_uid
-    except KeyError:
-        print('No user found:', user, file=sys.stderr)
-        sys.exit(1)
-    try:
-        gid = grp.getgrnam(group).gr_gid
-    except KeyError:
-        print('No group found:', group, file=sys.stderr)
-        sys.exit(1)
-
-    # Ensure that the var_dir exists, is owned by the user:group, and has
-    # the necessary permissions.  Set the mode separately after the
-    # makedirs() call because some platforms ignore mkdir()'s mode (though
-    # I think Linux does not ignore it -- better safe than sorry).
-    try:
-        os.makedirs(var_dir)
-    except OSError as e:
-        if e.errno != errno.EEXIST:
-            raise
-    else:
-        # Just created the var directory, will need to install mailmain bits.
-        need_install = True
-    os.chown(var_dir, uid, gid)
-    os.chmod(var_dir, 0o2775)
-
-    # Skip mailman setup if nothing so far has shown a reinstall needed.
-    if not need_install:
-        return 0
-
-    mailman_source = os.path.join('sourcecode', 'mailman')
-    if config.mailman.build_host_name:
-        build_host_name = config.mailman.build_host_name
-    else:
-        build_host_name = socket.getfqdn()
-
-    # Build and install the Mailman software.  Note that we don't care about
-    # --with-cgi-gid because we're not going to use that Mailman subsystem.
-    configure_args = (
-        './configure',
-        '--prefix', mailman_path,
-        '--with-var-prefix=' + var_dir,
-        '--with-python=' + executable,
-        '--with-username=' + user,
-        '--with-groupname=' + group,
-        '--with-mail-gid=' + group,
-        '--with-mailhost=' + build_host_name,
-        '--with-urlhost=' + build_host_name,
-        )
-    if need_build:
-        # Configure.
-        retcode = subprocess.call(configure_args, cwd=mailman_source)
-        if retcode:
-            print('Could not configure Mailman:', file=sys.stderr)
-            sys.exit(retcode)
-        # Make.
-        retcode = subprocess.call(('make', ), cwd=mailman_source)
-        if retcode:
-            print('Could not make Mailman.', file=sys.stderr)
-            sys.exit(retcode)
-    retcode = subprocess.call(('make', 'install'), cwd=mailman_source)
-    if retcode:
-        print('Could not install Mailman.', file=sys.stderr)
-        sys.exit(retcode)
-    # Symlink Mailman's Python modules into the import path.
-    try:
-        os.unlink(os.path.join('lib', 'Mailman'))
-    except OSError as e:
-        if e.errno != errno.ENOENT:
-            raise
-    os.symlink(
-        os.path.join('mailman', 'Mailman'), os.path.join('lib', 'Mailman'))
-    # Try again to import the package.
-    try:
-        import Mailman
-    except ImportError:
-        print('Could not import the Mailman package', file=sys.stderr)
-        return 1
-
-    # Check to see if the site list exists.  The output can go to /dev/null
-    # because we don't really care about it.  The site list exists if
-    # config_list returns a zero exit status, otherwise it doesn't
-    # (probably).  Before we can do this however, we must monkey patch
-    # Mailman, otherwise mm_cfg.py won't be set up correctly.
-    monkey_patch(mailman_path, config)
-
-    import Mailman.mm_cfg
-    retcode = subprocess.call(
-        ('./config_list', '-o', '/dev/null',
-         Mailman.mm_cfg.MAILMAN_SITE_LIST),
-        cwd=mailman_bin,
-        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-
-    if retcode:
-        addr, password = configure_siteowner(
-            config.mailman.build_site_list_owner)
-
-        # The site list does not yet exist, so create it now.
-        retcode = subprocess.call(
-            ('./newlist', '--quiet',
-             '--emailhost=' + build_host_name,
-             Mailman.mm_cfg.MAILMAN_SITE_LIST,
-             addr, password),
-            cwd=mailman_bin)
-        if retcode:
-            print('Could not create site list', file=sys.stderr)
-            return retcode
-
-    retcode = configure_site_list(
-        mailman_bin, Mailman.mm_cfg.MAILMAN_SITE_LIST)
-    if retcode:
-        print('Could not configure site list', file=sys.stderr)
-        return retcode
-
-    # Create a directory to hold the gzip'd tarballs for the directories of
-    # deactivated lists.
-    try:
-        os.mkdir(os.path.join(Mailman.mm_cfg.VAR_PREFIX, 'backups'))
-    except OSError as e:
-        if e.errno != errno.EEXIST:
-            raise
-
-    return 0
-
-
-def configure_site_list(mailman_bin, site_list_name):
-    """Configure the site list.
-
-    Currently, the only thing we want to set is to not advertise the
-    site list.
-    """
-    fd, config_file_name = tempfile.mkstemp()
-    try:
-        os.close(fd)
-        config_file = open(config_file_name, 'w')
-        try:
-            print('advertised = False', file=config_file)
-        finally:
-            config_file.close()
-        return subprocess.call(
-            ('./config_list', '-i', config_file_name, site_list_name),
-            cwd=mailman_bin)
-    finally:
-        os.remove(config_file_name)
-
-
-def main():
-    # setting python paths
-    program = sys.argv[0]
-
-    src = 'lib'
-    here = os.path.dirname(os.path.abspath(program))
-    srcdir = os.path.join(here, src)
-    sys.path = [srcdir, here] + basepath
-    return build_mailman()
-
-
-if __name__ == '__main__':
-    return_code = main()
-    sys.exit(return_code)
diff --git a/configs/README.txt b/configs/README.txt
index a7dda7a..050b0f7 100644
--- a/configs/README.txt
+++ b/configs/README.txt
@@ -294,10 +294,8 @@ Launchpad schema. This is a general outline of inheritance:
         |    + bazaar-staging/launchpad-lazr.conf
         |    |
         |    + staging/launchpad-lazr.conf
-        |    |    |
-        |    |    + authserver-lazr.conf
-        |    |
-        |    + staging-mailman/launchpad-lazr.conf
+        |         |
+        |         + authserver-lazr.conf
         |
         + lpnet-lazr.conf
         |    |
@@ -311,8 +309,6 @@ Launchpad schema. This is a general outline of inheritance:
         |    |
         |    + production/launchpad-lazr.conf
         |    |
-        |    + production-mailman/launchpad-lazr.conf
-        |    |
         |    + production-xmlrpc-private/launchpad-lazr.conf
         |
         + demo-lazr.conf
diff --git a/configs/development/launchpad-lazr.conf b/configs/development/launchpad-lazr.conf
index 1a53919..a0b8f0e 100644
--- a/configs/development/launchpad-lazr.conf
+++ b/configs/development/launchpad-lazr.conf
@@ -137,20 +137,9 @@ comments_list_truncate_newest_to: 6
 ubuntu_disable_filebug: false
 
 [mailman]
-launch: True
-build: True
-build_var_dir: /var/tmp/mailman
-xmlrpc_runner_sleep: 5
-smtp: localhost:9025
-list_help_header: http://help.launchpad.test/ListHelp
 archive_address: archive@xxxxxxxxxxxxxxxxx
-list_owner_header_template: http://launchpad.test/~$team_name
 archive_url_template: http://lists.launchpad.test/$team_name
-list_subscription_headers: http://launchpad.test/~$team_name
 build_host_name: lists.launchpad.test
-# Crank these way down so they're easier to test.
-soft_max_size: 40000
-hard_max_size: 1000000
 
 [memcache]
 servers: (127.0.0.1:11217,1)
diff --git a/configs/testrunner-appserver/launchpad-lazr.conf b/configs/testrunner-appserver/launchpad-lazr.conf
index b4619fe..80faefb 100644
--- a/configs/testrunner-appserver/launchpad-lazr.conf
+++ b/configs/testrunner-appserver/launchpad-lazr.conf
@@ -18,11 +18,6 @@ internal_macaroon_secret_key: internal-dev-macaroon-secret
 [librarian_server]
 launch: False
 
-[mailman]
-launch: False
-xmlrpc_runner_sleep: 1
-register_bounces_every: 1
-
 [vhost.mainsite]
 rooturl: http://launchpad.test:8085/
 
@@ -55,11 +50,3 @@ rooturl: http://xmlrpc-private.launchpad.test:8085/
 
 [vhost.feeds]
 rooturl: http://feeds.launchpad.test:8085/
-
-[immediate_mail]
-# BarryWarsaw 04-Dec-2008: AppServerLayer tests should send email to the fake
-# SMTP server that the layer starts up, so that they can be collected and
-# tested.
-smtp_port: 9025
-smtp_host: localhost
-send_email: true
diff --git a/configs/testrunner-appserver/mail-configure.zcml b/configs/testrunner-appserver/mail-configure.zcml
deleted file mode 100644
index 09c72c8..0000000
--- a/configs/testrunner-appserver/mail-configure.zcml
+++ /dev/null
@@ -1,28 +0,0 @@
-<!-- Copyright 2009 Canonical Ltd.  This software is licensed under the
-     GNU Affero General Public License version 3 (see the file LICENSE).
--->
-
-<!-- This file configures Launchpad to use direct delivery of mail to an SMTP
-     server listening on port 9025.  Generally, this will only work when the
-     Launchpad/Mailman integration tests are running.  Those tests spawn a
-     simple SMTP server which coordinates and delivers mail generated during
-     the tests.  So this configuration only works when those tests are
-     running.
--->
-
-<configure
-    xmlns="http://namespaces.zope.org/zope";
-    xmlns:mail="http://namespaces.zope.org/mail";
-    i18n_domain="zope">
-
-    <mail:smtpMailer
-        name="itests"
-        hostname="localhost"
-        port="9025"
-        />
-
-    <mail:directDelivery
-        permission="zope.SendMail"
-        mailer="itests" />
-
-</configure>
diff --git a/constraints.txt b/constraints.txt
index 34630e1..e794c78 100644
--- a/constraints.txt
+++ b/constraints.txt
@@ -223,7 +223,6 @@ lazr.jobrunner==0.16
 lazr.lifecycle==1.2
 lazr.restful==0.21.0
 lazr.restfulclient==0.14.3
-lazr.smtptest==1.3
 lazr.sshserver==0.1.10
 lazr.uri==1.0.3
 libnacl==1.3.6
diff --git a/lib/lp/app/browser/tests/test_views.py b/lib/lp/app/browser/tests/test_views.py
index 3b13827..5245ff8 100644
--- a/lib/lp/app/browser/tests/test_views.py
+++ b/lib/lp/app/browser/tests/test_views.py
@@ -38,8 +38,8 @@ def tearDown_bing(test):
 
 
 # The default layer of view tests is the DatabaseFunctionalLayer. Tests
-# that require something special like the librarian or mailman must run
-# on a layer that sets those services up.
+# that require something special like the librarian must run on a layer
+# that sets those services up.
 special = {
     'launchpad-search-pages.txt(Bing)': LayeredDocFileSuite(
         '../doc/launchpad-search-pages.txt',
diff --git a/lib/lp/registry/browser/tests/test_views.py b/lib/lp/registry/browser/tests/test_views.py
index 0a7964f..e5f35c4 100644
--- a/lib/lp/registry/browser/tests/test_views.py
+++ b/lib/lp/registry/browser/tests/test_views.py
@@ -23,8 +23,8 @@ from lp.testing.systemdocs import (
 here = os.path.dirname(os.path.realpath(__file__))
 
 # The default layer of view tests is the DatabaseFunctionalLayer. Tests
-# that require something special like the librarian, memcaches, or mailman
-# must run on a layer that sets those services up.
+# that require something special like the librarian or memcaches must
+# run on a layer that sets those services up.
 special_test_layer = {
     'distribution-views.txt': LaunchpadFunctionalLayer,
     'distributionsourcepackage-views.txt': LaunchpadFunctionalLayer,
diff --git a/lib/lp/registry/xmlrpc/mailinglist.py b/lib/lp/registry/xmlrpc/mailinglist.py
index 7277200..a8545db 100644
--- a/lib/lp/registry/xmlrpc/mailinglist.py
+++ b/lib/lp/registry/xmlrpc/mailinglist.py
@@ -38,15 +38,9 @@ from lp.services.messages.interfaces.message import IMessageSet
 from lp.services.webapp import LaunchpadXMLRPCView
 from lp.xmlrpc import faults
 
-# Not all developers will have built the Mailman instance (via
-# 'make mailman_instance').  In that case, this import will fail, but in that
-# case just use the constant value directly.
-try:
-    from Mailman.MemberAdaptor import ENABLED, BYUSER
-    ENABLED, BYUSER
-except ImportError:
-    ENABLED = 0
-    BYUSER = 2
+# These constants must match those defined in Mailman.MemberAdaptor.
+ENABLED = 0
+BYUSER = 2
 
 
 @implementer(IMailingListAPIView)
diff --git a/lib/lp/scripts/runlaunchpad.py b/lib/lp/scripts/runlaunchpad.py
index 11e06e4..c7019bd 100644
--- a/lib/lp/scripts/runlaunchpad.py
+++ b/lib/lp/scripts/runlaunchpad.py
@@ -23,7 +23,6 @@ from zope.app.server.main import main
 
 from lp.services.config import config
 from lp.services.daemons import tachandler
-from lp.services.mailman import runmailman
 from lp.services.osutils import ensure_directory_exists
 from lp.services.pidfile import (
     make_pidfile,
@@ -121,17 +120,6 @@ class TacFile(Service):
         process.stdin.close()
 
 
-class MailmanService(Service):
-
-    @property
-    def should_launch(self):
-        return config.mailman.launch
-
-    def launch(self):
-        runmailman.start_mailman()
-        self.addCleanup(runmailman.stop_mailman)
-
-
 class CodebrowseService(Service):
 
     @property
@@ -260,7 +248,6 @@ SERVICES = {
                          'librarian_server', prepare_for_librarian),
     'sftp': TacFile('sftp', 'daemons/sftp.tac', 'codehosting'),
     'forker': ForkingSessionService(),
-    'mailman': MailmanService(),
     'bing-webservice': BingWebService(),
     'codebrowse': CodebrowseService(),
     'memcached': MemcachedService(),
@@ -375,10 +362,6 @@ def start_testapp(argv=list(sys.argv)):
         fixture = ConfigUseFixture(BaseLayer.appserver_config_name)
         fixture.setUp()
         teardowns.append(fixture.cleanUp)
-        # Interactive tests always need this.  We let functional tests use
-        # a local one too because of simplicity.
-        LayerProcessController.startSMTPServer()
-        teardowns.append(LayerProcessController.stopSMTPServer)
         if interactive_tests:
             root_url = config.appserver_root_url()
             print('*' * 70)
diff --git a/lib/lp/scripts/tests/test_runlaunchpad.py b/lib/lp/scripts/tests/test_runlaunchpad.py
index 461a3e5..5dd6977 100644
--- a/lib/lp/scripts/tests/test_runlaunchpad.py
+++ b/lib/lp/scripts/tests/test_runlaunchpad.py
@@ -145,12 +145,7 @@ class ServersToStart(testtools.TestCase):
         services = sorted(get_services_to_run([]))
         expected = [SERVICES['librarian']]
 
-        # Mailman may or may not be asked to run.
-        if config.mailman.launch:
-            expected.append(SERVICES['mailman'])
-
-        # Likewise, the search test services may or may not be asked to
-        # run.
+        # The search test services may or may not be asked to run.
         if config.bing_test_service.launch:
             expected.append(SERVICES['bing-webservice'])
 
diff --git a/lib/lp/scripts/utilities/importpedant.py b/lib/lp/scripts/utilities/importpedant.py
index 83a62a8..ec41baf 100644
--- a/lib/lp/scripts/utilities/importpedant.py
+++ b/lib/lp/scripts/utilities/importpedant.py
@@ -186,10 +186,6 @@ def import_pedant(name, globals={}, locals={}, fromlist=[], level=-1):
     if name == 'sre':
         return module
 
-    # Mailman 2.1 code base is originally circa 1998, so yeah, no __all__'s.
-    if name.startswith('Mailman'):
-        return module
-
     # Some uses of __import__ pass None for globals, so handle that.
     import_into = None
     if globals is not None:
diff --git a/lib/lp/scripts/utilities/killservice.py b/lib/lp/scripts/utilities/killservice.py
index ef91f58..a922052 100755
--- a/lib/lp/scripts/utilities/killservice.py
+++ b/lib/lp/scripts/utilities/killservice.py
@@ -16,8 +16,6 @@ from signal import (
     )
 import time
 
-from lp.services.config import config
-from lp.services.mailman.runmailman import stop_mailman
 from lp.services.osutils import process_exists
 from lp.services.pidfile import (
     get_pid,
@@ -45,12 +43,6 @@ def main():
     pids = []  # List of pids we tried to kill.
     services = args[:]
 
-    # Mailman is special, but only stop it if it was launched.
-    if 'mailman' in services:
-        if config.mailman.launch:
-            stop_mailman()
-        services.remove('mailman')
-
     for service in services:
         log.debug("PID file is %s", pidfile_path(service))
         try:
diff --git a/lib/lp/services/config/schema-lazr.conf b/lib/lp/services/config/schema-lazr.conf
index ada0c9d..ad3d36a 100644
--- a/lib/lp/services/config/schema-lazr.conf
+++ b/lib/lp/services/config/schema-lazr.conf
@@ -1260,9 +1260,10 @@ os_password: none
 os_tenant_name: none
 
 
-# Mailman configuration.  This is only a shim to the real Mailman
-# configuration system and is primarily used to specify settings that
-# differ from the defaults, or are needed during the build.
+# Mailman configuration.  Most of this is configured in
+# https://git.launchpad.net/lp-mailman instead; the entries here are only
+# those that are needed for Launchpad's mailing list data model and for the
+# XML-RPC endpoints that Launchpad provides to lp-mailman.
 [mailman]
 
 # This string is used in the X-Launchpad-Hash header as salt in a hash that's
@@ -1273,105 +1274,11 @@ os_tenant_name: none
 shared_secret: sutsGNcUEpc
 
 # datatype: string
-list_help_header: https://help.launchpad.net/ListHelp
-
-# datatype: string
 archive_address: archive@xxxxxxxxxxxxxxxx
 
-# Whether Mailman should be started (i.e mailmanctl start).
-# datatype: boolean
-launch: False
-
-# datatype: integer
-xmlrpc_runner_sleep: 10
-
-# Socket timeout on XML-RPC connections made by Mailman.
-# This should at least be higher than the LP request timeout.
-# datatype: integer
-xmlrpc_timeout: 10
-
-# How often do we run the bounce processor?  For production, the default 15
-# minutes is fine, for testing we want to run it more often.
-# datatype: int
-register_bounces_every: 900
-
-# The hostname and port in the form of hostname:port.
-# datatype: string
-smtp: localhost:25
-
-# Ceiling on the number of recipients that can be specified in a single SMTP
-# transaction.  Set to 0 to submit the entire recipient list in one
-# transaction.
-# WARNING: Make sure that this matches the limits on the SMTP host!
-# datatype: integer
-smtp_max_rcpts: 50
-
-# Ceiling on the number of SMTP sessions to perform on a single socket
-# connection.
-# WARNING: Make sure that this matches the limits on the SMTP host!
-# datatype: integer
-smtp_max_sesions_per_connection: 200
-
-# The list owner header is a template URL that may contain
-# $team_name in it.
-# datatype: string
-list_owner_header_template: https://launchpad.net/~$team_name
-
-# datatype: string
-xmlrpc_url: http://xmlrpc-private.launchpad.test:8087/mailinglists
-
 # datatype: string
 archive_url_template: http://lists.launchpad.net/$team_name
 
-# The list subscription header is a template URL that may contain
-# $team_name in it.
-# datatype: string
-list_subscription_headers: https://launchpad.net/~$team_name
-
-# The XMLRPC qrunner batch size when requesting mailing list information.  The
-# qrunner will only ask for the info for these number of mailing lists at a
-# time, though all will be requested during any one time through the loop.
-# datatype: integer
-subscription_batch_size: 25
-
-# Hard and soft limits on the size of messages that will be accepted by
-# Mailman.  Messages below the soft limit in size are not moderated.  Between
-# the soft and hard limit, messages will be held for moderator approval.
-# Above the hard limit, messages will be logged and discarded with no
-# notification to the sender.  The Exim hard limit on incoming mail to
-# lists.launchpad.net is 50MB so the Mailman hard limit should never be hit in
-# practice.  These numbers are in bytes.
-soft_max_size: 2000000
-hard_max_size: 50000000
-
-# Whether Mailman should be built if it is not already.
-# datatype: boolean
-build: false
-
-# Specify Mailman's configure's --prefix argument.
-# If a value is given we assume it's a path and make it absolute.
-# datatype: string
-build_prefix:
-
-# The 'VAR_DIR' location.  This is where Mailman will put and look
-# for variable run time data, such as the list pickles and queue
-# directories.
-# datatype: string
-build_var_dir: /var/mailman
-
-# The user:group names that the Mailman process will run under.
-# You may need to invoke buildmailman.py or "make run" as root via
-# sudo to have the necessary permissions during the build or run
-# phase.  Leave this commented to use the current user and group.
-# datatype: string
-build_user_group:
-
-# Specify the site list's owner address and password
-# If not set, a fake email address and random password
-# will be used.
-# datatype: string
-build_site_list_owner:
-
 # Set this if you want a host_name other than the current
 # machine's `hostname -f`.  This is only used for the email domain
 # part.
diff --git a/lib/lp/services/mailman/__init__.py b/lib/lp/services/mailman/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/lib/lp/services/mailman/__init__.py
+++ /dev/null
diff --git a/lib/lp/services/mailman/config/__init__.py b/lib/lp/services/mailman/config/__init__.py
deleted file mode 100644
index 1d71ff8..0000000
--- a/lib/lp/services/mailman/config/__init__.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""ZConfig datatypes for <mailman> and <mailman-build> configuration keys."""
-
-
-import os
-import random
-from string import (
-    ascii_letters,
-    digits,
-    )
-
-
-__all__ = [
-    'configure_prefix',
-    'configure_siteowner',
-    ]
-
-
-EMPTY_STRING = ''
-
-
-def configure_prefix(value):
-    """Specify Mailman's configure's --prefix argument.
-
-    If a value is given we assume it's a path and make it absolute.  If it's
-    already absolute, it doesn't change.
-
-    >>> configure_prefix('/tmp/var/mailman')
-    '/tmp/var/mailman'
-
-    If it's relative, then it's relative to the current working directory.
-
-    >>> import os
-    >>> here = os.getcwd()
-    >>> configure_prefix('some/lib/mailman') == os.path.join(
-    ...     here, 'some/lib/mailman')
-    True
-
-    If the empty string is given (the default), then this returns lib/mailman
-    relative to the current working directory.
-
-    >>> configure_prefix('') == os.path.join(here, 'lib/mailman')
-    True
-    """
-    if value:
-        return os.path.abspath(value)
-    return os.path.abspath(os.path.join('lib', 'mailman'))
-
-
-def random_characters(length=10):
-    """Return a random string of characters."""
-    chars = digits + ascii_letters
-    return EMPTY_STRING.join(random.choice(chars) for c in range(length))
-
-
-def configure_siteowner(value):
-    """Accept a string of the form email:password.
-
-    Given a value, it must be an address and password separated by a colon.
-
-    >>> configure_siteowner('foo')
-    Traceback (most recent call last):
-    ...
-    ValueError: need more than 1 value to unpack
-    >>> configure_siteowner('me@xxxxxxxxxxx:password')
-    ('me@xxxxxxxxxxx', 'password')
-
-    However, the format (or validity) of the email address is not checked.
-
-    >>> configure_siteowner('email:password')
-    ('email', 'password')
-
-    If an empty string is given (the default), we use a random password and a
-    random local part, with the domain forced to example.com.
-
-    >>> address, password = configure_siteowner('')
-    >>> len(password) == 10
-    True
-    >>> localpart, domain = address.split('@', 1)
-    >>> len(localpart) == 10
-    True
-    >>> domain
-    'example.com'
-    """
-    if value:
-        addr, password = value.split(':', 1)
-    else:
-        localpart = random_characters()
-        password = random_characters()
-        addr = localpart + '@example.com'
-    return addr, password
diff --git a/lib/lp/services/mailman/config/tests/__init__.py b/lib/lp/services/mailman/config/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/lib/lp/services/mailman/config/tests/__init__.py
+++ /dev/null
diff --git a/lib/lp/services/mailman/config/tests/test_config.py b/lib/lp/services/mailman/config/tests/test_config.py
deleted file mode 100644
index 4c2a6ea..0000000
--- a/lib/lp/services/mailman/config/tests/test_config.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-from doctest import (
-    DocTestSuite,
-    ELLIPSIS,
-    NORMALIZE_WHITESPACE,
-    )
-
-
-def test_suite():
-    suite = DocTestSuite(
-            'lp.services.mailman.config',
-            optionflags=NORMALIZE_WHITESPACE | ELLIPSIS)
-    return suite
diff --git a/lib/lp/services/mailman/monkeypatches/__init__.py b/lib/lp/services/mailman/monkeypatches/__init__.py
deleted file mode 100644
index 0311737..0000000
--- a/lib/lp/services/mailman/monkeypatches/__init__.py
+++ /dev/null
@@ -1,128 +0,0 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Install Launchpad integration code into the Mailman module."""
-
-import os
-import shutil
-
-from lazr.config import as_host_port
-
-
-def monkey_patch(mailman_path, config):
-    """Monkey-patch an installed Mailman 2.1 tree.
-
-    Rather than maintain a forked tree of Mailman 2.1, we apply a set of
-    changes to an installed Mailman tree.  This tree can be found rooted at
-    mailman_path.
-
-    This should usually mean just copying a file from this directory into
-    mailman_path.  Rather than build a lot of process into the mix, just hard
-    code each transformation here.
-    """
-    # Hook Mailman to Launchpad by writing a custom mm_cfg.py file which adds
-    # the top of our Launchpad tree to Mailman's sys.path.  The mm_cfg.py file
-    # won't do much more than set up sys.path and do an from-import-* to get
-    # everything that doesn't need to be dynamically calculated at run-time.
-    # Things that can only be calculated at run-time are written to mm_cfg.py
-    # now.  It's okay to simply overwrite any existing mm_cfg.py, since we'll
-    # provide everything Mailman needs.
-    #
-    # Remember, don't rely on Launchpad's config object in the mm_cfg.py file
-    # or in the lp.services.mailman.monkeypatches.defaults module because
-    # Mailman will not be able to initialize Launchpad's configuration system.
-    # Instead, anything that's needed from config should be written to the
-    # mm_cfg.py file now.
-    #
-    # Calculate the parent directory of the lp module.  This directory
-    # will get appended to Mailman's sys.path.
-    import lp
-    from lp.services.mailman.config import configure_siteowner
-    launchpad_top = os.path.abspath(
-        os.path.join(os.path.dirname(lp.__file__), os.pardir, os.pardir))
-    # Read the email footer template for all Launchpad messages.
-    from lp.services.mail.helpers import get_email_template
-    footer = get_email_template(
-        'mailinglist-footer.txt', app='services/mailman/monkeypatches')
-    # Write the mm_cfg.py file, filling in the dynamic values now.
-    host, port = as_host_port(config.mailman.smtp)
-    owner_address, owner_password = configure_siteowner(
-        config.mailman.build_site_list_owner)
-    config_path_in = os.path.join(os.path.dirname(__file__), 'mm_cfg.py.in')
-    config_file_in = open(config_path_in)
-    try:
-        config_template = config_file_in.read()
-    finally:
-        config_file_in.close()
-    config_path_out = os.path.join(mailman_path, 'Mailman', 'mm_cfg.py')
-    config_file_out = open(config_path_out, 'w')
-    try:
-        print >> config_file_out, config_template % dict(
-            launchpad_top=launchpad_top,
-            smtp_host=host,
-            smtp_port=port,
-            smtp_max_rcpts=config.mailman.smtp_max_rcpts,
-            smtp_max_sesions_per_connection=(
-                config.mailman.smtp_max_sesions_per_connection),
-            xmlrpc_url=config.mailman.xmlrpc_url,
-            xmlrpc_sleeptime=config.mailman.xmlrpc_runner_sleep,
-            xmlrpc_timeout=config.mailman.xmlrpc_timeout,
-            xmlrpc_subscription_batch_size=(
-                config.mailman.subscription_batch_size),
-            site_list_owner=owner_address,
-            list_help_header=config.mailman.list_help_header,
-            list_subscription_headers=(
-                config.mailman.list_subscription_headers),
-            archive_url_template=config.mailman.archive_url_template,
-            list_owner_header_template=(
-                config.mailman.list_owner_header_template),
-            footer=footer,
-            var_dir=config.mailman.build_var_dir,
-            shared_secret=config.mailman.shared_secret,
-            soft_max_size=config.mailman.soft_max_size,
-            hard_max_size=config.mailman.hard_max_size,
-            register_bounces_every=config.mailman.register_bounces_every,
-            )
-    finally:
-        config_file_out.close()
-    # Mailman's qrunner system requires runner modules to live in the
-    # Mailman.Queue package.  Set things up so that there's a hook module in
-    # there for the XMLRPCRunner.
-    runner_path = os.path.join(mailman_path,
-                               'Mailman', 'Queue', 'XMLRPCRunner.py')
-    runner_file = open(runner_path, 'w')
-    try:
-        print >> runner_file, (
-            'from lp.services.mailman.monkeypatches.xmlrpcrunner '
-            'import *')
-    finally:
-        runner_file.close()
-    # Install our handler wrapper modules so that Mailman can find them.  Most
-    # of the actual code of the handler comes from our monkey patches modules.
-    for mm_name, lp_name in (('LaunchpadMember', 'lphandler'),
-                             ('LaunchpadHeaders', 'lpheaders'),
-                             ('LPStanding', 'lpstanding'),
-                             ('LPModerate', 'lpmoderate'),
-                             ('LPSize', 'lpsize'),
-                             ):
-        handler_path = os.path.join(
-            mailman_path, 'Mailman', 'Handlers', mm_name + '.py')
-        handler_file = open(handler_path, 'w')
-        try:
-            package = 'lp.services.mailman.monkeypatches'
-            module = package + '.' + lp_name
-            print >> handler_file, 'from', module, 'import *'
-        finally:
-            handler_file.close()
-
-    here = os.path.dirname(__file__)
-    # Install the MHonArc control file.
-    mhonarc_rc_file = os.path.join(here, 'lp-mhonarc-common.mrc')
-    runtime_data_dir = os.path.join(config.mailman.build_var_dir, 'data')
-    shutil.copy(mhonarc_rc_file, runtime_data_dir)
-    # Install the launchpad site templates.
-    launchpad_template_path = os.path.join(here, 'sitetemplates')
-    site_template_path = os.path.join(mailman_path, 'templates', 'site')
-    if os.path.isdir(site_template_path):
-        shutil.rmtree(site_template_path)
-    shutil.copytree(launchpad_template_path, site_template_path)
diff --git a/lib/lp/services/mailman/monkeypatches/defaults.py b/lib/lp/services/mailman/monkeypatches/defaults.py
deleted file mode 100644
index fd3b940..0000000
--- a/lib/lp/services/mailman/monkeypatches/defaults.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-# Pick up the standard Mailman defaults
-from Mailman.Defaults import *
-
-# Use a name for the site list that is very unlikely to conflict with any
-# possible Launchpad team name.  The default is "mailman" and that doesn't cut
-# it. :)  The site list is never used by Launchpad, but it's required by
-# Mailman 2.1.
-MAILMAN_SITE_LIST = 'unused_mailman_site_list'
-
-# We don't need to coordinate aliases with a mail server because we'll be
-# pulling incoming messages from a POP account.
-MTA = None
-
-# Disable runners for features we don't need.
-QRUNNERS = [
-    ('ArchRunner',     1),  # messages for the archiver
-    ('BounceRunner',   1),  # for processing the qfile/bounces directory
-##     ('CommandRunner',  1),  # commands and bounces from the outside world
-    ('IncomingRunner', 1),  # posts from the outside world
-##     ('NewsRunner',     1),  # outgoing messages to the nntpd
-    ('OutgoingRunner', 1),  # outgoing messages to the smtpd
-    ('VirginRunner',   1),  # internally crafted (virgin birth) messages
-    ('RetryRunner',    1),  # retry temporarily failed deliveries
-    # Non-standard runners we've added.
-    ('XMLRPCRunner',   1),  # Poll for XMLRPC requests
-    ]
-
-# Other list defaults.
-DEFAULT_GENERIC_NONMEMBER_ACTION = 3  # Discard
-DEFAULT_SEND_REMINDERS = No
-DEFAULT_SEND_WELCOME_MSG = Yes
-DEFAULT_SEND_GOODBYE_MSG = No
-DEFAULT_DIGESTABLE = No
-DEFAULT_BOUNCE_NOTIFY_OWNER_ON_DISABLE = No
-DEFAULT_BOUNCE_NOTIFY_OWNER_ON_REMOVAL = No
-VERP_PERSONALIZED_DELIVERIES = Yes
-DEFAULT_FORWARD_AUTO_DISCARDS = No
-DEFAULT_BOUNCE_PROCESSING = No
-
-# Modify the global pipeline to add some handlers for Launchpad specific
-# functionality.
-# - ensure posters are Launchpad members.
-GLOBAL_PIPELINE.insert(0, 'LaunchpadMember')
-# - insert our own RFC 2369 and RFC 5064 headers; this must appear after
-#   CookHeaders
-index = GLOBAL_PIPELINE.index('CookHeaders')
-GLOBAL_PIPELINE.insert(index + 1, 'LaunchpadHeaders')
-# - Insert our own moderation handlers instead of the standard Mailman
-#   Moderate and Hold handlers.  Hold always comes after Moderate in the
-#   default global pipeline.
-index = GLOBAL_PIPELINE.index('Moderate')
-GLOBAL_PIPELINE[index:index + 2] = ['LPStanding', 'LPModerate', 'LPSize']
diff --git a/lib/lp/services/mailman/monkeypatches/emailtemplates/mailinglist-footer.txt b/lib/lp/services/mailman/monkeypatches/emailtemplates/mailinglist-footer.txt
deleted file mode 100644
index 9cc971b..0000000
--- a/lib/lp/services/mailman/monkeypatches/emailtemplates/mailinglist-footer.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Mailing list: $list_owner
-Post to     : $list_post
-Unsubscribe : $list_unsubscribe
-More help   : $list_help
diff --git a/lib/lp/services/mailman/monkeypatches/lp-mhonarc-common.mrc b/lib/lp/services/mailman/monkeypatches/lp-mhonarc-common.mrc
deleted file mode 100644
index 1129182..0000000
--- a/lib/lp/services/mailman/monkeypatches/lp-mhonarc-common.mrc
+++ /dev/null
@@ -1,535 +0,0 @@
-<!-- Launchpad customizations common to all our MHonArc-generated
-     mailing list archives.
-
-     See http://www.mhonarc.org/MHonArc/doc/mhonarc.html and
-     http://www.mhonarc.org/MHonArc/doc/faq/, they are your friends.
-
-     http://www.mhonarc.org/MHonArc/doc/resources.html#index is
-     especially your friend, when all others have abandoned you.  -->
-
-<!-- Basic parameters. -->
-<SPAMMODE>
-<MAIN>
-<THREAD>
-<SORT>
-<REVERSE>
-<TREVERSE>
-<NODOC>
-<UMASK>
-022
-</UMASK>
-
-
-<!-- Encode all messages as utf-8 and set the page encoding to utf-8 -->
-<TextEncode>
-utf-8; MHonArc::UTF8::to_utf8; MHonArc/UTF8.pm
-</TextEncode>
-
-<!-- text/plain only. CVE-2010-4524 -->
-<MIMEExcs>
-text/html
-text/x-html
-</MIMEExcs>
-
-<IDXFNAME>
-date.html
-</IDXFNAME>
-
-<TIDXFNAME>
-maillist.html
-</TIDXFNAME>
-
-<!-- Use multi-page indexes.
-     See http://www.mhonarc.org/MHonArc/doc/resources/multipg.html -->
-<MULTIPG>
-<IDXSIZE>
-200
-</IDXSIZE>
-
-<!-- strip the first [list-name] from the subect line. -->
-<SUBJECTSTRIPCODE>
-s/\[[^ ]+\]//;
-</SUBJECTSTRIPCODE>
-
-
-<!-- Define a custom resource variable to represent this mailing list.
-     This depends on $ML-NAME$ having been set already, presumably on
-     the command line via '-definevar'.  See
-     http://www.mhonarc.org/MHonArc/doc/resources/definevar.html. -->
-
-<DefineVar>
-TEAM-LINK
-<a href="https://launchpad.net/~$ML-NAME$";>$ML-NAME$</a>
-</DefineVar>
-
-
-<DefineVar>
-PAGE-TOP-START
-<!DOCTYPE html>
-<html>
-<head>
-<title>
-</DefineVar>
-
-<DefineVar>
-PAGE-TOP-END
-</title>
-<style type="text/css">
-h1, ul, ol, dl, li, dt, dd {
-    margin: 0;
-    padding: 0;
-    }
-ul ul {
-    margin-left: 2em;
-    }
-.mail li {
-    margin-left: 20px;
-    padding-left: 0px;
-   }
-.upper-batch-nav {
-    margin: 12px 0;
-    border-bottom: 1px solid #d2d2d2;
-    }
-.lower-batch-nav {
-    margin: 12px 0;
-    border-top: 1px solid #d2d2d2;
-    }
-.lower-batch-nav.message-count-0 {
-    display: none;
-    }
-.batch-navigation-links {
-    text-align: right;
-    padding-left: 24px;
-    }
-.message-count-0 {
-    margin-bottom: 12px;
-    }
-.message-count-0:before {
-    content: "There are no messages in this mailing list archive.";
-    }
-.back-to {
-    margin: 0 0 12px 0;
-    padding: 0 0 6px 0;
-    border-bottom: 1px solid #d2d2d2;
-    }
-.facetmenu {
-    margin-top: 4px;
-    margin-left: .5em;
-    }
-</style>
-<link rel="stylesheet" href="https://launchpad.net/+icing/import.css"; />
-<link rel="shortcut icon" href="https://launchpad.net/@@/launchpad.png"; />
-</head>
-<body>
-  <div class="back-to">
-    <a href="https://launchpad.net/~$ML-NAME$";>&larr;
-      Back to team overview</a>
-  </div>
-  <h1>$ML-NAME$ team mailing list archive</h1>
-  <div id="watermark" class="watermark-apps-portlet">
-    <div class="wide">
-</DefineVar>
-
-<DefineVar>
-PAGE-BOTTOM
-  <div id="footer" class="footer">
-    <div class="lp-arcana">
-        <div class="lp-branding">
-          <a href="https://launchpad.net/";><img
-         src="https://launchpad.net/@@/launchpad-logo-and-name-hierarchy.png";
-         alt="Launchpad" /></a>
-          &nbsp;&bull;&nbsp;
-          <a href="https://launchpad.net/+tour";>Take the tour</a>
-          &nbsp;&bull;&nbsp;
-          <a href="https://help.launchpad.net/";>Read the guide</a>
-          &nbsp;&bull;&nbsp;
-          <a href="https://help.launchpad.net/Teams/MailingLists";
-          >Help for mailing lists</a>
-          &nbsp;
-          <form id="globalsearch" method="get"
-                accept-charset="UTF-8"
-                action="https://launchpad.net/+search";>
-            <input type="search" id="search-text" name="field.text" />
-            <input type="submit" value=""
-              class="sprite search-icon" />
-          </form>
-        </div>
-    </div>
-    <div class="colophon">
-      &copy; 2004-2020
-      <a href="http://canonical.com/";>Canonical&nbsp;Ltd.</a>
-      &nbsp;&bull;&nbsp;
-      <a href="https://launchpad.net/legal";>Terms of use</a>
-      &nbsp;&bull;&nbsp;
-      <a href="https://www.ubuntu.com/legal/dataprivacy";>Data privacy</a>
-      &nbsp;&bull;&nbsp;
-      <a href="https://launchpad.net/support";>Contact Launchpad Support</a>
-      &nbsp;&bull;&nbsp;
-      <a href="http://blog.launchpad.net/";>Blog</a>
-      &nbsp;&bull;&nbsp;
-      <a href="http://www.canonical.com/about-canonical/careers";>Careers</a>
-      &nbsp;&bull;&nbsp;
-      <a href="https://twitter.com/launchpadstatus";>System status</a>
-      </span>
-    </div>
-  </div>
-</body>
-</html>
-</DefineVar>
-
-
-<!-- What do the next/prev links look like? -->
-<IDXLABEL>
-Date index
-</IDXLABEL>
-
-<TIDXLABEL>
-Thread index
-</TIDXLABEL>
-
-<!-- The text is reversed because the messages are sorted in reverse. -->
-<PREVPGLINK>
-<a href="$PG(FIRST)$">Last</a> &bull; <a href="$PG(PREV)$">Next</a>
-</PREVPGLINK>
-
-<PREVPGLINKIA>
-<span class="inactive">Last &bull; Next</span>
-</PREVPGLINKIA>
-
-<TPREVPGLINK>
-<a href="$PG(TFIRST)$">Last</a> &bull; <a href="$PG(TPREV)$">Next</a>
-</TPREVPGLINK>
-
-<TPREVPGLINKIA>
-<span class="inactive">Last &bull; Next</span>
-</TPREVPGLINKIA>
-
-<NEXTPGLINK>
-<a class="next" href="$PG(NEXT)$">Previous</a>
-  &bull; <a href="$PG(LAST)$">First</a>
-</NEXTPGLINK>
-
-<NEXTPGLINKIA>
-<span class="next inactive">Previous</span>
-  &bull; <span class="inactive">First</span>
-</NEXTPGLINKIA>
-
-<TNEXTPGLINK>
-<a class="next" href="$PG(TNEXT)$">Previous</a>
-   &bull; <a href="$PG(TLAST)$">First</a>
-</TNEXTPGLINK>
-
-<TNEXTPGLINKIA>
-<span class="next inactive">Previous</span>
-  &bull;<span class="inactive">First</span>
-</TNEXTPGLINKIA>
-
-
-<!-- Thread index pages -->
-<TIDXPGBEGIN>
-$PAGE-TOP-START$
-Messages by thread : Mailing list archive : $ML-NAME$ team in Launchpad
-$PAGE-TOP-END$
-</TIDXPGBEGIN>
-
-<TIDXPGEND>
-$PAGE-BOTTOM$
-</TIDXPGEND>
-
-<!-- Formatting for the start of thread page.
-     See http://www.mhonarc.org/MHonArc/doc/resources/thead.html. -->
-<THEAD>
-      <ul class="facetmenu">
-          <li title="View messages by thread"
-            class="active"><span>Thread</span></li>
-          <li title="View messages by date"><a href="$IDXFNAME$">Date</a></li>
-      </ul>
-    </div>
-  </div>
-
-<ol class="breadcrumbs">
-  <li>
-    <img src="https://launchpad.net/@@/team"; alt=""/>
-    <a href="https://launchpad.net/~$ML-NAME$";>$ML-NAME$ team</a>
-  </li>
-  <li>
-    <a href="$TIDXFNAME$">Mailing list archive</a>
-  </li>
-  <li>
-    Messages by thread
-  </li>
-</ol>
-
-<h2>Messages by thread</h2>
-
-<p>
-Messages sent to the $ML-NAME$ mailing list, ordered by thread from the
-newest to oldest.
-</p>
-
-<table class="wide upper-batch-nav">
-  <tr>
-    <td>
-      $NUMOFIDXMSG$ of $NUMOFMSG$ messages, page $PGLINKLIST(T5;T5)$
-    </td>
-    <td class="batch-navigation-links">
-      $PGLINK(TPREV)$ &bull; $PGLINK(TNEXT)$
-    </td>
-  </tr>
-</table>
-
-<ul class="mail wide message-count-$NUMOFMSG$">
-</THEAD>
-
-<TFOOT>
-</ul>
-<table class="wide lower-batch-nav message-count-$NUMOFMSG$">
-  <tr>
-    <td>
-      $NUMOFIDXMSG$ of $NUMOFMSG$ messages, page $PGLINKLIST(T5;T5)$
-    </td>
-    <td class="batch-navigation-links">
-      $PGLINK(TPREV)$ &bull; $PGLINK(TNEXT)$
-    </td>
-  </tr>
-</table>
-</TFOOT>
-
-
-<!-- Date index pages -->
-<IDXPGBEGIN>
-$PAGE-TOP-START$
-Messages by date : Mailing list archive : $ML-NAME$ team in Launchpad
-$PAGE-TOP-END$
-</IDXPGBEGIN>
-
-<IDXPGEND>
-$PAGE-BOTTOM$
-</IDXPGEND>
-
-<!-- Formatting for the start of list page.
-     See http://www.mhonarc.org/MHonArc/doc/resources/listbegin.html. -->
-<LISTBEGIN>
-    <ul class="facetmenu">
-        <li title="View messages by thread">
-            <a href="$TIDXFNAME$">Thread</a>
-        </li>
-        <li title="View messages by date" class="active">
-          <span>Date</span>
-        </li>
-    </ul>
-  </div>
-</div>
-
-<ol class="breadcrumbs">
-  <li>
-    <img src="https://launchpad.net/@@/team"; alt=""/>
-    <a href="https://launchpad.net/~$ML-NAME$";>$ML-NAME$ team</a>
-  </li>
-  <li>
-    <a href="$TIDXFNAME$">Mailing list archive</a>
-  </li>
-  <li>
-    Messages by date
-  </li>
-</ol>
-
-<h2>Messages by date</h2>
-
-<p>
-Messages sent to the $ML-NAME$ mailing list, ordered by date from the
-newest to oldest.
-</p>
-
-<table class="wide upper-batch-nav">
-  <tr>
-    <td>
-      $NUMOFIDXMSG$ of $NUMOFMSG$ messages, page $PGLINKLIST(5;5)$
-    </td>
-    <td class="batch-navigation-links">
-      $PGLINK(PREV)$ &bull; $PGLINK(NEXT)$
-    </td>
-  </tr>
-</table>
-
-<ul class="mail wide message-count-$NUMOFMSG$">
-</LISTBEGIN>
-
-<LISTEND>
-</ul>
-<table class="wide lower-batch-nav message-count-$NUMOFMSG$">
-  <tr>
-    <td>
-      $NUMOFIDXMSG$ of $NUMOFMSG$ messages, page $PGLINKLIST(5;5)$
-    </td>
-    <td class="batch-navigation-links">
-      $PGLINK(PREV)$ &bull; $PGLINK(NEXT)$
-    </td>
-  </tr>
-</table>
-</LISTEND>
-
-
-<!-- Message item formatting for all lists. -->
-<DefineVar>
-MESSAGE-LIST-ITEM
-<li>
-  <strong>$SUBJECT$</strong>
-  <br />From: $FROMNAME$, $MSGGMTDATE(CUR;%Y-%m-%d)$
-</li>
-</DefineVar>
-
-<LITEMPLATE>
-$MESSAGE-LIST-ITEM$
-</LITEMPLATE>
-
-<TTOPBEGIN>
-$MESSAGE-LIST-ITEM$
-</TTOPBEGIN>
-
-<TLITXT>
-$MESSAGE-LIST-ITEM$
-</TLITXT>
-
-<TSINGLETXT>
-$MESSAGE-LIST-ITEM$
-</TSINGLETXT>
-
-
-<!-- Message pages -->
-<MSGPGBEGIN>
-$PAGE-TOP-START$
-$SUBJECTNA$ : Mailing list archive : $ML-NAME$ team in Launchpad
-$PAGE-TOP-END$
-</MSGPGBEGIN>
-
-<MSGPGEND>
-$PAGE-BOTTOM$
-</MSGPGEND>
-
-<SUBJECTHEADER>
-  <!-- Supress the header section since it was moved before the top links. -->
-</SUBJECTHEADER>
-
-<!-- Message navigation links -->
-<TopLinks>
-    <ul class="facetmenu">
-        <li title="View messages by thread"
-          class="active"><a href="$TIDXFNAME$">Thread</a></li>
-        <li title="View messages by date"><a href="$IDXFNAME$">Date</a></li>
-    </ul>
-  </div>
-</div>
-
-<ol class="breadcrumbs">
-  <li>
-    <img src="https://launchpad.net/@@/team"; alt=""/>
-    <a href="https://launchpad.net/~$ML-NAME$";>$ML-NAME$ team</a>
-  </li>
-  <li>
-    <a href="$TIDXFNAME$">Mailing list archive</a>
-  </li>
-  <li>
-    Message #$MSGNUM$
-  </li>
-</ol>
-
-<h2>$SUBJECTNA$</h2>
-
-<table class="wide upper-batch-nav">
-  <tr>
-    <td>
-      &nbsp;
-    </td>
-    <td class="batch-navigation-links">
-      <a href="$MSG(TPREV)$">Thread Previous</a> &bull;
-      <a href="$MSG(PREV)$">Date Previous</a> &bull;
-      <a class="next" href="$MSG(NEXT)$">Date Next</a> &bull;
-      <a href="$MSG(TNEXT)$">Thread Next</a>
-    </td>
-  </tr>
-</table>
-</TopLinks>
-
-<BotLinks>
-<table class="wide lower-batch-nav">
-  <tr>
-    <td>
-      &nbsp;
-    </td>
-    <td class="batch-navigation-links">
-      <a href="$MSG(TPREV)$">Thread Previous</a> &bull;
-      <a href="$MSG(PREV)$">Date Previous</a> &bull;
-      <a class="next" href="$MSG(NEXT)$">Date Next</a> &bull;
-      <a href="$MSG(TNEXT)$">Thread Next</a>
-    </td>
-  </tr>
-</table>
-</BotLinks>
-
-<!-- Exclude noisy message fields -->
-<EXCS>
-subject
-list-
-dkim-
-Domainkey-
-precendence
-References
-</EXCS>
-
-<!-- Format message header in a definition list. -->
-
-<Fieldsbeg>
-<ul class="iconed">
-</FieldsBeg>
-
-<LabelBeg>
-<li>
-  <strong>
-</LabelBeg>
-
-<LabelEnd>
-</strong>:
-</LabelEnd>
-
-<FldBeg>
-</FldBeg>
-
-<FldEnd>
-</li>
-</FldEnd>
-
-<FieldsEnd>
-</ul>
-</FieldsEnd>
-
-<LabelStyles>
--default-:strong
-</LabelStyles>
-
-<FOLUPBEGIN>
-<h3>Follow ups</h3>
-<ul class="mail wide">
-</FOLUPBEGIN>
-
-<FOLUPLITXT>
-$MESSAGE-LIST-ITEM$
-</FOLUPLITXT>
-
-<FOLUPEND>
-</ul>
-</FOLUPEND>
-
-
-<REFSBEGIN>
-<h3>References</h3>
-<ul class="mail wide">
-</REFSBEGIN>
-
-<REFSLITXT>
-$MESSAGE-LIST-ITEM$
-</REFSLITXT>
-
-<REFSEND>
-</ul>
-</REFSEND>
diff --git a/lib/lp/services/mailman/monkeypatches/lphandler.py b/lib/lp/services/mailman/monkeypatches/lphandler.py
deleted file mode 100644
index 16af5c3..0000000
--- a/lib/lp/services/mailman/monkeypatches/lphandler.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""A global pipeline handler for determining Launchpad membership."""
-
-
-import hashlib
-
-from Mailman import (
-    Errors,
-    mm_cfg,
-    )
-from Mailman.Logging.Syslog import syslog
-from Mailman.Queue import XMLRPCRunner
-
-
-def process(mlist, msg, msgdata):
-    """Discard the message if it doesn't come from a Launchpad member."""
-    if msgdata.get('approved'):
-        return
-    # Some automated processes will send messages to the mailing list. For
-    # example, if the list is a contact address for a team and that team is
-    # the contact address for a project's answer tracker, an automated message
-    # will be sent from Launchpad. Check for a header that indicates this was
-    # a Launchpad-generated message. See
-    # lp.services.mail.sendmail.sendmail for where this is set.
-    secret = msg['x-launchpad-hash']
-    message_id = msg['message-id']
-    if secret and message_id:
-        hash = hashlib.sha1(mm_cfg.LAUNCHPAD_SHARED_SECRET)
-        hash.update(message_id)
-        if secret == hash.hexdigest():
-            # Since this message is coming from Launchpad, pre-approve it.
-            # Yes, this could be spoofed, but there's really no other way
-            # (currently) to do it.
-            msgdata['approved'] = True
-            return
-    # Ask Launchpad whether the sender is a Launchpad member.  If not, discard
-    # the message with extreme prejudice, but log this.
-    sender = msg.get_sender()
-    # Check with Launchpad about whether the sender is a member or not.  If we
-    # 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 = 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.
-    try:
-        is_member = proxy.isRegisteredInLaunchpad(sender)
-    except Exception as error:
-        XMLRPCRunner.handle_proxy_error(error, msg, msgdata)
-    # This handler can just return if the sender is a member of Launchpad.
-    if is_member:
-        return
-    # IncomingRunner already posts the Message-ID to the logs/vette for
-    # discarded messages, so we only need to add a little more detail here.
-    syslog('vette', 'Sender is not a Launchpad member: %s', sender)
-    raise Errors.DiscardMessage
diff --git a/lib/lp/services/mailman/monkeypatches/lpheaders.py b/lib/lp/services/mailman/monkeypatches/lpheaders.py
deleted file mode 100644
index 7371d03..0000000
--- a/lib/lp/services/mailman/monkeypatches/lpheaders.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""A global pipeline handler for inserting Launchpad specific headers."""
-
-from string import Template
-
-from Mailman import mm_cfg
-
-
-def process(mlist, msg, msgdata):
-    """Add RFC 2369 and RFC 5064 headers."""
-    # Start by deleting any existing such headers in the message already.
-    for header in ('list-id', 'list-help', 'list-post', 'list-archive',
-                   'list-owner', 'list-subscribe', 'list-unsubscribe',
-                   'archived-at'):
-        del msg[header]
-    # Calculate values used both in the RFC 2369 and 5064 headers, and in the
-    # message footer decoration.
-    list_name = mlist.internal_name()
-    list_owner = Template(
-        mm_cfg.LIST_OWNER_HEADER_TEMPLATE).safe_substitute(
-        team_name=list_name)
-    list_archive = Template(
-        mm_cfg.LIST_ARCHIVE_HEADER_TEMPLATE).safe_substitute(
-        team_name=list_name)
-    list_post = mlist.GetListEmail()
-    list_unsubscribe = Template(
-        mm_cfg.LIST_SUBSCRIPTION_HEADERS).safe_substitute(
-        team_name=list_name)
-    list_help = mm_cfg.LIST_HELP_HEADER
-    # Add the RFC 2369 headers.
-    msg['List-Id'] = '<%s.%s>' % (list_name, mlist.host_name)
-    msg['List-Help'] = '<%s>' % list_help
-    # We really don't want to have to VERP these headers in, so we use a
-    # generic header that Launchpad will redirect to the user's actual page.
-    # The subscribe and unsubscribe pages are the same.
-    msg['List-Subscribe'] = '<%s>' % list_unsubscribe
-    msg['List-Unsubscribe'] = '<%s>' % list_unsubscribe
-    msg['List-Post'] = '<mailto:%s>' % list_post
-    msg['List-Archive'] = '<%s>' % list_archive
-    msg['List-Owner'] = '<%s>' % list_owner
-    # Set up message metadata for header/footer decoration interpolation in
-    # the Decorate handler.
-    msgdata['decoration-data'] = dict(
-        list_owner=list_owner,
-        list_post=list_post,
-        list_unsubscribe=list_unsubscribe,
-        list_help=list_help,
-        )
diff --git a/lib/lp/services/mailman/monkeypatches/lpmoderate.py b/lib/lp/services/mailman/monkeypatches/lpmoderate.py
deleted file mode 100644
index 7223c1e..0000000
--- a/lib/lp/services/mailman/monkeypatches/lpmoderate.py
+++ /dev/null
@@ -1,117 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""A pipeline handler for holding list non-members postings for approval.
-"""
-
-from email.iterators import typed_subpart_iterator
-from email.utils import (
-    formatdate,
-    make_msgid,
-    )
-import xmlrpclib
-
-from Mailman import Errors
-from Mailman.Logging.Syslog import syslog
-from Mailman.Queue import XMLRPCRunner
-
-
-def process(mlist, msg, msgdata):
-    """Handle all list non-member postings.
-
-    For Launchpad members who are not list-members, a previous handler will
-    check their personal standing to see if they are allowed to post.  This
-    handler takes care of all other cases and it overrides Mailman's standard
-    Moderate handler.  It also knows how to hold messages in Launchpad's
-    librarian.
-    """
-    # If the message is already approved, then this handler is done.
-    if msgdata.get('approved'):
-        return
-    # If the sender is a member of the mailing list, then this handler is
-    # done.  Note that we don't need to check the member's Moderate flag as
-    # the original Mailman handler does, because for Launchpad, we know it
-    # will always be unset.
-    for sender in msg.get_senders():
-        if mlist.isMember(sender):
-            return
-    # From here on out, we're dealing with senders who are not members of the
-    # mailing list.  They are also not Launchpad members in good standing or
-    # we'd have already approved the message.  So now the message must be held
-    # in Launchpad for approval via the LP u/i.
-    hold(mlist, msg, msgdata, 'Not subscribed')
-
-
-def is_message_empty(msg):
-    """Is the message missing a text/plain part with content?"""
-    for part in typed_subpart_iterator(msg, 'text'):
-        if part.get_content_subtype() == 'plain':
-            if len(part.get_payload().strip()) > 0:
-                return False
-    return True
-
-
-def hold(mlist, msg, msgdata, annotation):
-    """Hold the message in both Mailman and Launchpad.
-
-    `annotation` is an arbitrary string required by the API.
-    """
-    # Hold the message in Mailman and Launchpad so that it's easier to
-    # resubmit it after approval via the LP u/i.  If the team administrator
-    # ends up rejecting the message, it will also be easy to discard it on the
-    # Mailman side.  But this way, we don't have to reconstitute the message
-    # from the librarian if it gets approved.  However, unlike the standard
-    # Moderate handler, we don't craft all the notification messages about
-    # this hold.  We also need to keep track of the message-id (which better
-    # be unique) because that's how we communicate about the message's status.
-    request_id = mlist.HoldMessage(msg, annotation, msgdata)
-    assert mlist.Locked(), (
-        'Mailing list should be locked: %s' % mlist.internal_name())
-    # This is a hack because by default Mailman cannot look up held messages
-    # by message-id.  This works because Mailman's persistency layer simply
-    # pickles the MailList object, mostly without regard to a known schema.
-    #
-    # Mapping: message-id -> request-id
-    holds = getattr(mlist, 'held_message_ids', None)
-    if holds is None:
-        holds = mlist.held_message_ids = {}
-    message_id = msg.get('message-id')
-    if message_id is None:
-        msg['Message-ID'] = message_id = make_msgid()
-    if message_id in holds:
-        # No legitimate sender should ever give us a message with a duplicate
-        # message id, so treat this as spam.
-        syslog('vette',
-               'Discarding duplicate held message-id: %s', message_id)
-        raise Errors.DiscardMessage
-    # Discard messages that claim to be from the list itself because Mailman's
-    # internal handlers did not approve the message before it arrived at this
-    # step--these messages are forgeries.
-    list_address = mlist.getListAddress()
-    for sender in msg.get_senders():
-        if list_address == sender:
-            syslog('vette',
-                   'Discarding forged message-id: %s', message_id)
-            raise Errors.DiscardMessage
-    # Discard messages without text content since there will be nothing to
-    # moderate. Most of these messages are spam.
-    if is_message_empty(msg):
-        syslog('vette',
-               'Discarding text-less message-id: %s', message_id)
-        raise Errors.DiscardMessage
-    holds[message_id] = request_id
-    # In addition to Message-ID, the librarian requires a Date header.
-    if 'date' not in msg:
-        msg['Date'] = formatdate()
-    # Store the message in the librarian.
-    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.
-    proxy.holdMessage(mlist.internal_name(),
-                      xmlrpclib.Binary(msg.as_string()))
-    syslog('vette', 'Holding message for LP approval: %s', message_id)
-    # Raise this exception, signaling to the incoming queue runner that it is
-    # done processing this message, and should not send it through any further
-    # handlers.
-    raise Errors.HoldMessage
diff --git a/lib/lp/services/mailman/monkeypatches/lpsize.py b/lib/lp/services/mailman/monkeypatches/lpsize.py
deleted file mode 100644
index 067f2d0..0000000
--- a/lib/lp/services/mailman/monkeypatches/lpsize.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""A pipeline handler for checking message sizes."""
-
-import email
-
-from Mailman import (
-    Errors,
-    Message,
-    mm_cfg,
-    )
-from Mailman.Handlers.LPModerate import hold
-from Mailman.Logging.Syslog import syslog
-
-
-def truncated_message(original_message, limit=10000):
-    """Create a smaller version of the message for moderation."""
-    message = email.message_from_string(
-        original_message.as_string(), Message.Message)
-    for part in email.iterators.typed_subpart_iterator(message, 'multipart'):
-        subparts = part.get_payload()
-        removeable = []
-        for subpart in subparts:
-            if subpart.get_content_maintype() == 'multipart':
-                # This part is handled in the outer loop.
-                continue
-            elif subpart.get_content_type() == 'text/plain':
-                # Truncate the message at 10kb so that there is enough
-                # information for the moderator to make a decision.
-                content = subpart.get_payload().strip()
-                if len(content) > limit:
-                    subpart.set_payload(
-                        content[:limit] + '\n[truncated for moderation]',
-                        subpart.get_charset())
-            else:
-                removeable.append(subpart)
-        for subpart in removeable:
-            subparts.remove(subpart)
-    return message
-
-
-def process(mlist, msg, msgdata):
-    """Check the message size (approximately) against hard and soft limits.
-
-    If the message is below the soft limit, do nothing.  This allows the
-    message to be posted without moderation, assuming no other handler get
-    triggered of course.
-
-    Messages between the soft and hard limits get moderated in the Launchpad
-    web u/i, just as non-member posts would.  Personal standing does not
-    override the size checks.
-
-    Messages above the hard limit get logged and discarded.  In production, we
-    should never actually hit the hard limit.  The Exim in front of
-    lists.launchpad.net has its own hard limit of 50MB (which is the
-    production standard Mailman hard limit value), so messages larger than
-    this should never even show up.
-    """
-    # Calculate the message size by turning it back into a string.  In Mailman
-    # 3.0 this calculation is done on initial message parse so it will be
-    # quicker and not consume so much memory.  But since the hard limit is
-    # 50MB I don't think we can actually get into any real trouble here, as
-    # long as we can trust Python's reference counter.
-    message_size = len(msg.as_string())
-    # Hard and soft limits are specified in bytes.
-    if message_size < mm_cfg.LAUNCHPAD_SOFT_MAX_SIZE:
-        # Nothing to do.
-        return
-    if message_size < mm_cfg.LAUNCHPAD_HARD_MAX_SIZE:
-        # Hold the message in Mailman.  See lpmoderate.py for similar
-        # algorithm.
-        # There is a limit to the size that can be stored in Lp. Send
-        # a trucated copy of the message that has enough information for
-        # the moderator to make a decision.
-        hold(mlist, truncated_message(msg), msgdata, 'Too big')
-    # The message is larger than the hard limit, so log and discard.
-    syslog('vette', 'Discarding message w/size > hard limit: %s',
-           msg.get('message-id', 'n/a'))
-    raise Errors.DiscardMessage
diff --git a/lib/lp/services/mailman/monkeypatches/lpstanding.py b/lib/lp/services/mailman/monkeypatches/lpstanding.py
deleted file mode 100644
index da7337c..0000000
--- a/lib/lp/services/mailman/monkeypatches/lpstanding.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""A pipeline handler for moderating Launchpad users based on standing.
-
-This handler checks Launchpad member's personal standing in order to determine
-whether list non-members are allowed to post to a mailing list.
-"""
-
-from Mailman.Queue import XMLRPCRunner
-
-
-def process(mlist, msg, msgdata):
-    """Check the standing of a non-Launchpad member.
-
-    A message posted to a mailing list from a Launchpad member in good
-    standing is allowed onto the list even if they are not members of the
-    list.
-
-    Because this handler comes before the standard Moderate handler, if the
-    sender is not in good standing, we just defer to other decisions further
-    along the pipeline.  If the sender is in good standing, we approve it.
-    """
-    sender = msg.get_sender()
-    # Ask Launchpad about the standing of this member.
-    in_good_standing = False
-    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.
-    try:
-        in_good_standing = proxy.inGoodStanding(sender)
-    except Exception as error:
-        XMLRPCRunner.handle_proxy_error(error, msg, msgdata)
-    # If the sender is a member in good standing, that's all we need to know
-    # in order to let the message pass.
-    if in_good_standing:
-        msgdata['approved'] = True
diff --git a/lib/lp/services/mailman/monkeypatches/mm_cfg.py.in b/lib/lp/services/mailman/monkeypatches/mm_cfg.py.in
deleted file mode 100644
index 25e722e..0000000
--- a/lib/lp/services/mailman/monkeypatches/mm_cfg.py.in
+++ /dev/null
@@ -1,67 +0,0 @@
-# Automatically generated by runlaunchpad.py
-
-# Initialize sys.path so that the Mailman processes, which use the standard
-# system Python instead of bin/py, can find all the necessary packages and
-# modules.  Some of these are in sourcecode and some are in the virtualenv.
-#
-# This is a two-step process.  First, we hack sys.path in order to find a
-# directory containing _pythonpath.  Then the _pythonpath module does all the
-# subsequent sys.path hacking necessary.
-
-# Set up Mailman's sys.path to pick up the top of Launchpad's tree.  This only
-# gets us a sys.path
-import sys
-sys.path.insert(0, '%(launchpad_top)s')
-import _pythonpath
-
-# Pick up Launchpad static overrides.  This will also pick up the standard
-# Mailman.Defaults.* variables.
-from lp.services.mailman.monkeypatches.defaults import *
-
-# Our dynamic overrides of all the static defaults.
-SMTPHOST = '%(smtp_host)s'
-SMTPPORT = %(smtp_port)d
-SMTP_MAX_RCPTS = %(smtp_max_rcpts)d
-SMTP_MAX_SESSIONS_PER_CONNECTION = %(smtp_max_sesions_per_connection)d
-
-# Configuration options for the XMLRPCRunner.
-XMLRPC_URL = '%(xmlrpc_url)s'
-XMLRPC_SLEEPTIME = %(xmlrpc_sleeptime)s
-XMLRPC_TIMEOUT = %(xmlrpc_timeout)s
-XMLRPC_SUBSCRIPTION_BATCH_SIZE = %(xmlrpc_subscription_batch_size)s
-LAUNCHPAD_SHARED_SECRET = '%(shared_secret)s'
-
-# RFC 2369 header information.
-LIST_HELP_HEADER = '%(list_help_header)s'
-LIST_SUBSCRIPTION_HEADERS = '%(list_subscription_headers)s'
-LIST_ARCHIVE_HEADER_TEMPLATE = '%(archive_url_template)s'
-LIST_OWNER_HEADER_TEMPLATE = '%(list_owner_header_template)s'
-
-# Soft and hard maximum message size limits.  Anything below the soft limit is
-# allowed directly through.  Between the soft and hard limits, the message is
-# held for approval.  Above the hard limit, the message is logged and
-# discarded.  Note that the normal Mailman size thresholds are ignored.
-LAUNCHPAD_SOFT_MAX_SIZE = %(soft_max_size)d
-LAUNCHPAD_HARD_MAX_SIZE = %(hard_max_size)d
-
-SITE_LIST_OWNER = '%(site_list_owner)s'
-
-DEFAULT_MSG_FOOTER = '''-- 
-%(footer)s'''
-
-# Set up MHonArc archiving.
-PUBLIC_EXTERNAL_ARCHIVER = '/usr/bin/mhonarc \
--add \
--dbfile %(var_dir)s/archives/private/%%(listname)s.mbox/mhonarc.db \
--outdir %(var_dir)s/mhonarc/%%(listname)s \
--definevar ML-NAME=%%(listname)s \
--rcfile %(var_dir)s/data/lp-mhonarc-common.mrc \
--stderr %(var_dir)s/logs/mhonarc \
--stdout %(var_dir)s/logs/mhonarc \
--spammode \
--umask 022'
-PRIVATE_EXTERNAL_ARCHIVER = PUBLIC_EXTERNAL_ARCHIVER
-
-# How often do we run the bounce processor?  For production, the default 15
-# minutes is fine, for testing we want to run it more often.
-REGISTER_BOUNCES_EVERY = %(register_bounces_every)d
diff --git a/lib/lp/services/mailman/monkeypatches/sitetemplates/en/postheld.txt b/lib/lp/services/mailman/monkeypatches/sitetemplates/en/postheld.txt
deleted file mode 100644
index de975c6..0000000
--- a/lib/lp/services/mailman/monkeypatches/sitetemplates/en/postheld.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-Your mail to '%(listname)s' with the subject
-
-    %(subject)s
-
-Is being held until the list moderator can review it for approval.
-
-The reason it is being held:
-
-    %(reason)s
-
-Either the message will get posted to the list, or you will receive
-notification of the moderator's decision.
diff --git a/lib/lp/services/mailman/monkeypatches/xmlrpcrunner.py b/lib/lp/services/mailman/monkeypatches/xmlrpcrunner.py
deleted file mode 100644
index 66bdaa2..0000000
--- a/lib/lp/services/mailman/monkeypatches/xmlrpcrunner.py
+++ /dev/null
@@ -1,704 +0,0 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""XMLRPC runner for querying Launchpad."""
-
-__all__ = [
-    'get_mailing_list_api_proxy',
-    'handle_proxy_error',
-    'XMLRPCRunner',
-    ]
-
-from cStringIO import StringIO
-from datetime import (
-    datetime,
-    timedelta,
-    )
-import errno
-import os
-from random import shuffle
-import shutil
-import socket
-import sys
-import tarfile
-import traceback
-import xmlrpclib
-
-from Mailman import (
-    Errors,
-    mm_cfg,
-    Utils,
-    )
-from Mailman.LockFile import TimeOutError
-from Mailman.Logging.Syslog import syslog
-from Mailman.MailList import MailList
-from Mailman.Queue.Runner import Runner
-from Mailman.Queue.sbcache import get_switchboard
-
-from lp.services.webapp.errorlog import ErrorReportingUtility
-from lp.services.xmlrpc import Transport
-
-
-COMMASPACE = ', '
-
-
-# Mapping from modifiable attributes as they are named by the xmlrpc
-# interface, to the attribute names on the MailList instances.
-attrmap = {
-    'welcome_message': 'welcome_msg',
-    }
-
-
-def get_mailing_list_api_proxy():
-    return xmlrpclib.ServerProxy(
-        mm_cfg.XMLRPC_URL, transport=Transport(timeout=mm_cfg.XMLRPC_TIMEOUT))
-
-
-def log_exception(message, *args):
-    """Write the current exception traceback into the Mailman log file.
-
-    This is really just a convenience function for a refactored chunk of
-    common code.
-
-    :param message: The message to appear in the xmlrpc and error logs. It
-        may be a format string.
-    :param args: Optional arguments to be interpolated into a format string.
-    """
-    error_utility = ErrorReportingUtility()
-    error_utility.configure(section_name='mailman')
-    error_utility.raising(sys.exc_info())
-    out_file = StringIO()
-    traceback.print_exc(file=out_file)
-    traceback_text = out_file.getvalue()
-    syslog('xmlrpc', message, *args)
-    syslog('error', message, *args)
-    syslog('error', traceback_text)
-
-
-def handle_proxy_error(error, msg=None, msgdata=None):
-    """Log the error and enqueue the message if needed.
-
-    :param error: The error to log.
-    :param msg: An optional Mailman.Message to re-enqueue.
-    :param msgdata: The message data to enque with the message.
-    :raise DiscardMessage: When a message is enqueued.
-    """
-    if isinstance(error, (xmlrpclib.ProtocolError, socket.error)):
-        log_exception('Cannot talk to Launchpad:\n%s', error)
-    else:
-        log_exception('Launchpad exception: %s', error)
-    if msg is not None:
-        queue = get_switchboard(mm_cfg.INQUEUE_DIR)
-        queue.enqueue(msg, msgdata)
-        raise Errors.DiscardMessage
-
-
-class XMLRPCRunner(Runner):
-    """A Mailman 'queue runner' for talking to the Launchpad XMLRPC service.
-    """
-
-    # We increment this every time we see an actual change in the data coming
-    # from Launchpad.  We write this to the xmlrpc log file for better
-    # synchronization with the integration tests.  It's not used for any other
-    # purpose.
-    def __init__(self, slice=None, numslices=None):
-        """Create a faux runner which checks into Launchpad occasionally.
-
-        Every XMLRPC_SLEEPTIME number of seconds, this runner wakes up and
-        connects to a Launchpad XMLRPC service to see if there's anything for
-        it to do.  slice and numslices are ignored, but required by the
-        Mailman queue runner framework.
-        """
-        self.SLEEPTIME = mm_cfg.XMLRPC_SLEEPTIME
-        # Instead of calling the superclass's __init__() method, just
-        # initialize the two attributes that are actually used.  The reason
-        # for this is that the XMLRPCRunner doesn't have a queue so it
-        # shouldn't be trying to create a Switchboard instance.  Still, it
-        # needs a dummy _kids and _stop attributes for the rest of the runner
-        # to work.  We're using runners in a more general sense than Mailman 2
-        # is designed for.
-        self._kids = {}
-        self._stop = False
-        self._proxy = get_mailing_list_api_proxy()
-        # Ensure that the log file exists, mostly for the test suite.
-        syslog('xmlrpc', 'XMLRPC runner starting')
-        self.heartbeat_frequency = timedelta(minutes=5)
-        self.last_heartbeat = None
-        self._heartbeat()
-
-    def _oneloop(self):
-        """Check to see if there's anything for Mailman to do.
-
-        Mailman makes an XMLRPC connection to Launchpad to see if there are
-        any mailing lists to create, modify or deactivate.  It also requests
-        updates to list subscriptions.  This method is called periodically by
-        the base class's main loop.
-
-        This method always returns 0 to indicate to the base class's main loop
-        that it should sleep for a while after calling this method.
-        """
-        methods = (
-            self._check_list_actions, self._get_subscriptions,
-            self._check_held_messages)
-        try:
-            for method in methods:
-                if self._shortcircuit():
-                    # The runner was  shutdown during the long network call.
-                    break
-                method()
-        except:
-            # Log the exception and report an OOPS.
-            log_exception('Unexpected XMLRPCRunner exception')
-        else:
-            self._heartbeat()
-        # Snooze for a while.
-        return 0
-
-    def _heartbeat(self):
-        """Add a heartbeat to the log for a monitor to watch."""
-        now = datetime.now()
-        last_heartbeat = self.last_heartbeat
-        if (last_heartbeat is None
-            or now - last_heartbeat >= self.heartbeat_frequency):
-            syslog('xmlrpc', '--MARK--')
-            self.last_heartbeat = now
-
-    def _log(self, exc):
-        """Log the exception in a log file and as an OOPS."""
-        Runner._log(self, exc)
-        error_utility = ErrorReportingUtility()
-        error_utility.configure(section_name='mailman')
-        error_utility.raising(sys.exc_info())
-
-    def _check_list_actions(self):
-        """See if there are any list actions to perform."""
-        try:
-            actions = self._proxy.getPendingActions()
-        except (xmlrpclib.ProtocolError, socket.error) as error:
-            log_exception('Cannot talk to Launchpad:\n%s', error)
-            return
-        except xmlrpclib.Fault as error:
-            log_exception('Launchpad exception: %s', error)
-            return
-        if actions:
-            syslog('xmlrpc', 'Received these actions: %s',
-                   COMMASPACE.join(actions))
-        else:
-            return
-        # There are three actions that can currently be taken.  A create
-        # action creates a mailing list, possibly with some defaults, a modify
-        # changes the settings on some existing mailing list, and a deactivate
-        # means that the list should be deactivated.  This latter doesn't have
-        # a directly corresponding semantic at the Mailman layer -- if a
-        # mailing list exists, it's activated.  We'll take it to mean that the
-        # list should be deleted, but its archives should remain.
-        statuses = {}
-        if 'create' in actions:
-            self._create_or_reactivate(actions['create'], statuses)
-            del actions['create']
-        if 'modify' in actions:
-            self._modify(actions['modify'], statuses)
-            del actions['modify']
-        if 'deactivate' in actions:
-            self._deactivate(actions['deactivate'], statuses)
-            del actions['deactivate']
-        if 'unsynchronized' in actions:
-            self._resynchronize(actions['unsynchronized'], statuses)
-            del actions['unsynchronized']
-        # Any other keys should be ignored because they specify actions that
-        # we know nothing about.  We'll log them to Mailman's log files
-        # though.
-        if actions:
-            syslog('xmlrpc', 'Invalid xmlrpc action keys: %s',
-                   COMMASPACE.join(actions))
-        # Report the statuses to Launchpad.  Do this individually so as to
-        # reduce the possibility that a bug in Launchpad causes the reporting
-        # all subsequent mailing lists statuses to fail.  The reporting of
-        # status triggers synchronous operations in Launchpad, such as
-        # notifying team admins that their mailing list is ready, and those
-        # operations could fail for spurious reasons.  That shouldn't affect
-        # the status reporting for any other list.  This is a little more
-        # costly, but it's not that bad.
-        for team_name, (action, status) in statuses.items():
-            this_status = {team_name: status}
-            try:
-                self._proxy.reportStatus(this_status)
-                syslog('xmlrpc', '[%s] %s: %s' % (team_name, action, status))
-            except (xmlrpclib.ProtocolError, socket.error) as error:
-                log_exception('Cannot talk to Launchpad:\n%s', error)
-            except xmlrpclib.Fault as error:
-                log_exception('Launchpad exception: %s', error)
-
-    def _update_list_subscriptions(self, list_name, subscription_info):
-        """Update the subscription information for a single mailing list.
-
-        :param list_name: The name of the mailing list to update.
-        :type list_name: string
-        :param subscription_info: The mailing list's new subscription
-            information.
-        :type subscription_info: a list of 4-tuples containing the address,
-            real name, flags, and status of each person in the list's
-            subscribers.
-        """
-        ## syslog('xmlrpc', '%s subinfo: %s', list_name, subscription_info)
-        # Start with an unlocked list.
-        mlist = MailList(list_name, lock=False)
-        # Create a mapping of email address to the member's real name,
-        # flags, and status.  Note that flags is currently unused.
-        member_map = dict((address, (realname, flags, status))
-                          for address, realname, flags, status
-                          in subscription_info)
-        # In Mailman parlance, the 'member key' is the lower-cased
-        # address.  We need a mapping from the member key to
-        # case-preserved address for all future members of the list.
-        key_to_case_preserved = dict((address.lower(), address)
-                                     for address in member_map)
-        # The following modifications to membership may be made:
-        # - Current members whose address is changing case
-        # - New members who need to be added to the mailing list
-        # - Old members who need to be removed from the mailing list
-        # - Current members whose settings are being changed
-        #
-        # Start by getting the case-folded membership sets of all current
-        # and future members.
-        current_members = set(mlist.getMembers())
-        future_members = set(key_to_case_preserved)
-        # Additions are all those addresses in future_members who are not
-        # in current_members.
-        additions = future_members - current_members
-        # Deletions are all those addresses in current_members who are not
-        # in future_members.
-        deletions = current_members - future_members
-        # Any address in both current and future members is either
-        # changing case, updating settings, or both.
-        updates = current_members & future_members
-        # If there's nothing to do, just skip this list.
-        if not additions and not deletions and not updates:
-            # Why did we get here?  This list should not have shown up in
-            # getMembershipInformation().
-            syslog('xmlrpc',
-                   'Strange subscription information for list: %s',
-                   list_name)
-            return
-        # Lock the list and make the modifications.  Don't worry if the list
-        # couldn't be locked, we'll just log that and try again the next time
-        # through the loop.  Eventually any existing lock will expire anyway.
-        try:
-            mlist.Lock(2)
-        except TimeOutError:
-            syslog('xmlrpc',
-                   'Could not lock the list to update subscriptions: %s',
-                   list_name)
-            return
-        try:
-            # Handle additions first.
-            if len(additions) > 0:
-                syslog('xmlrpc', 'Adding to %s: %s', list_name, additions)
-            for address in additions:
-                # When adding the new member, be sure to use the
-                # case-preserved email address.
-                original_address = key_to_case_preserved[address]
-                realname, flags, status = member_map[original_address]
-                mlist.addNewMember(original_address, realname=realname)
-                mlist.setDeliveryStatus(original_address, status)
-            # Handle deletions next.
-            if len(deletions) > 0:
-                syslog('xmlrpc', 'Removing from %s: %s', list_name, deletions)
-            for address in deletions:
-                mlist.removeMember(address)
-            # Updates can be either a settings update, a change in the
-            # case of the subscribed address, or both.
-            found_updates = []
-            for address in updates:
-                # See if the case is changing.
-                current_address = mlist.getMemberCPAddress(address)
-                future_address = key_to_case_preserved[address]
-                if current_address != future_address:
-                    mlist.changeMemberAddress(address, future_address)
-                    found_updates.append('%s -> %s' %
-                                         (address, future_address))
-                # flags are ignored for now.
-                realname, flags, status = member_map[future_address]
-                if realname != mlist.getMemberName(address):
-                    mlist.setMemberName(address, realname)
-                    found_updates.append('%s new name: %s' %
-                                         (address, realname))
-                if status != mlist.getDeliveryStatus(address):
-                    mlist.setDeliveryStatus(address, status)
-                    found_updates.append('%s new status: %s' %
-                                         (address, status))
-            if len(found_updates) > 0:
-                syslog('xmlrpc', 'Membership updates for %s: %s',
-                       list_name, found_updates)
-            # We're done, so flush the changes for this mailing list.
-            mlist.Save()
-        finally:
-            mlist.Unlock()
-
-    def _get_subscriptions(self):
-        """Get the latest subscription information."""
-        # First, calculate the names of the active mailing lists.
-        lists = sorted(list_name
-                       for list_name in Utils.list_names()
-                       if list_name != mm_cfg.MAILMAN_SITE_LIST)
-        # Batch the subscription requests in order to reduce the possibility
-        # of timeouts in the XMLRPC server.  Note that we cannot eliminate
-        # timeouts, which will cause an entire batch to fail.  To reduce the
-        # possibility that the same batch of teams will always fail, we
-        # shuffle the list of team names so the batches will always be
-        # different.
-        shuffle(lists)
-        while lists:
-            if self._shortcircuit():
-                # Stop was called during a long network call.
-                return
-            batch = lists[:mm_cfg.XMLRPC_SUBSCRIPTION_BATCH_SIZE]
-            lists = lists[mm_cfg.XMLRPC_SUBSCRIPTION_BATCH_SIZE:]
-            ## syslog('xmlrpc', 'batch: %s', batch)
-            ## syslog('xmlrpc', 'lists: %s', lists)
-            # Get the information for this batch of mailing lists.
-            try:
-                info = self._proxy.getMembershipInformation(batch)
-            except (xmlrpclib.ProtocolError, socket.error) as error:
-                log_exception('Cannot talk to Launchpad: %s', error)
-                syslog('xmlrpc', 'batch: %s', batch)
-                continue
-            except xmlrpclib.Fault as error:
-                log_exception('Launchpad exception: %s', error)
-                syslog('xmlrpc', 'batch: %s', batch)
-                continue
-            for list_name in info:
-                subscription_info = info[list_name]
-                # The subscription info for a mailing list can be None,
-                # meaning that there are no subscribers or allowed posters.
-                # The latter can only happen if there are no active team
-                # members, and that can only happen when the owner has been
-                # specifically deactivate for some reason.  This is not an
-                # error condition.
-                if subscription_info is not None:
-                    self._update_list_subscriptions(
-                        list_name, subscription_info)
-
-    def _create_or_reactivate(self, actions, statuses):
-        """Process mailing list creation and reactivation actions.
-
-        actions is a sequence of (team_name, initializer) tuples where the
-        team_name is the name of the mailing list to create and initializer is
-        a dictionary of initial custom values to set on the new mailing list.
-
-        statuses is a dictionary mapping team names to one of the strings
-        'success' or 'failure'.
-        """
-        for team_name, initializer in actions:
-            # This is a set of attributes defining the defaults for lists
-            # created under Launchpad's control.  We need to map attribute
-            # names as Launchpad sees them to attribute names on the MailList
-            # object.
-            list_defaults = {}
-            # Verify that the initializer variables are what we expect.
-            for key in attrmap:
-                if key in initializer:
-                    list_defaults[attrmap[key]] = initializer[key]
-                    del initializer[key]
-            if initializer:
-                # Reject this list creation request.
-                statuses[team_name] = ('create', 'failure')
-                syslog('xmlrpc', 'Unexpected create settings: %s',
-                       COMMASPACE.join(initializer))
-                continue
-            # Either the mailing list was deactivated at one point, or it
-            # never existed in the first place.  Look for a backup tarfile; if
-            # it exists, this is a reactivation, otherwise it is the initial
-            # creation.
-            tgz_file_name = os.path.join(
-                mm_cfg.VAR_PREFIX, 'backups', team_name + '.tgz')
-            try:
-                tgz_file = tarfile.open(tgz_file_name, 'r:gz')
-            except IOError as error:
-                if error.errno != errno.ENOENT:
-                    raise
-                # The archive tarfile does not exist, meaning this is the
-                # initial creation request.
-                action = 'created'
-                status = self._create(team_name)
-            else:
-                # This is a reactivation request.  Unpack the archived tarball
-                # into the lists directory.
-                action = 'reactivated'
-                status = self._reactivate(team_name, tgz_file)
-                tgz_file.close()
-                if status:
-                    os.remove(tgz_file_name)
-            # If the list was successfully created or reactivated, apply
-            # defaults.  Otherwise, set the failure status and return.
-            if not status:
-                syslog('xmlrpc', 'An error occurred; the list was not %s: %s',
-                       action, team_name)
-                statuses[team_name] = ('reactivate', 'failure')
-                return
-            self._apply_list_defaults(team_name, list_defaults)
-            statuses[team_name] = ('create/reactivate', 'success')
-
-    def _apply_list_defaults(self, team_name, list_defaults):
-        """Apply mailing list defaults and tie the new list into the MTA."""
-        mlist = MailList(team_name)
-        try:
-            for key, value in list_defaults.items():
-                setattr(mlist, key, value)
-            # Do MTA specific creation steps.
-            if mm_cfg.MTA:
-                modname = 'Mailman.MTA.' + mm_cfg.MTA
-                __import__(modname)
-                sys.modules[modname].create(mlist, quiet=True)
-            mlist.Save()
-        finally:
-            mlist.Unlock()
-
-    def _reactivate(self, team_name, tgz_file):
-        """Reactivate an archived mailing list from backup file."""
-        lists_dir = os.path.join(mm_cfg.VAR_PREFIX, 'lists')
-        # Temporarily change to the top level `lists` directory, since all the
-        # tar files have paths relative to that.
-        old_cwd = os.getcwd()
-        try:
-            os.chdir(lists_dir)
-            tgz_file.extractall()
-        finally:
-            os.chdir(old_cwd)
-        syslog('xmlrpc', '%s: %s', lists_dir, os.listdir(lists_dir))
-        return True
-
-    def _create(self, team_name):
-        """Create a new mailing list."""
-        # Create the mailing list and set the defaults.
-        mlist = MailList()
-        try:
-            # Use a fake list admin password; Mailman will never be
-            # administered from its web u/i.  Nor will the mailing list
-            # require an owner that's different from the site owner.  Also by
-            # default, only English is supported.
-            try:
-                mlist.Create(team_name,
-                             mm_cfg.SITE_LIST_OWNER,
-                             ' no password ')
-                # Additional hard coded list defaults.
-                # - Personalize regular delivery so that we can VERP these.
-                # - Turn off RFC 2369 headers; we'll do them differently
-                # - enable $-string substitutions in headers/footers
-                mlist.personalize = 1
-                mlist.include_rfc2369_headers = False
-                mlist.use_dollar_strings = True
-                mlist.held_message_ids = {}
-                mlist.Save()
-                # Now create the archive directory for MHonArc.
-                path = os.path.join(mm_cfg.VAR_PREFIX, 'mhonarc', team_name)
-                os.makedirs(path)
-            # We have to use a bare except here because of the legacy string
-            # exceptions that Mailman can raise.
-            except:
-                log_exception('List creation error for team: %s', team_name)
-                return False
-            else:
-                return True
-        finally:
-            mlist.Unlock()
-
-    def _modify(self, actions, statuses):
-        """Process mailing list modification actions.
-
-        actions is a sequence of (team_name, modifications) tuples where the
-        team_name is the name of the mailing list to create and modifications
-        is a dictionary of values to set on the mailing list.
-
-        statuses is a dictionary mapping team names to one of the strings
-        'success' or 'failure'.
-        """
-        for team_name, modifications in actions:
-            # First, validate the modification keywords.
-            list_settings = {}
-            for key in attrmap:
-                if key in modifications:
-                    list_settings[attrmap[key]] = modifications[key]
-                    del modifications[key]
-            if modifications:
-                statuses[team_name] = ('modify', 'failure')
-                syslog('xmlrpc', 'Unexpected modify settings: %s',
-                       COMMASPACE.join(modifications))
-                continue
-            try:
-                mlist = MailList(team_name)
-                try:
-                    for key, value in list_settings.items():
-                        setattr(mlist, key, value)
-                    mlist.Save()
-                finally:
-                    mlist.Unlock()
-            # We have to use a bare except here because of the legacy string
-            # exceptions that Mailman can raise.
-            except:
-                log_exception(
-                    'List modification error for team: %s', team_name)
-                statuses[team_name] = ('modify', 'failure')
-            else:
-                statuses[team_name] = ('modify', 'success')
-
-    def _deactivate(self, actions, statuses):
-        """Process mailing list deactivation actions.
-
-        actions is a sequence of team names for the mailing lists to
-        deactivate.
-
-        statuses is a dictionary mapping team names to one of the strings
-        'success' or 'failure'.
-        """
-        for team_name in actions:
-            try:
-                mlist = MailList(team_name, lock=False)
-                if mm_cfg.MTA:
-                    modname = 'Mailman.MTA.' + mm_cfg.MTA
-                    __import__(modname)
-                    sys.modules[modname].remove(mlist, quiet=True)
-                # The archives are always persistent, so all we need to do to
-                # deactivate a list is to delete the 'lists/team_name'
-                # directory.  However, in order to support easy reactivation,
-                # and to provide a backup in case of error, we create a gzip'd
-                # tarball of the list directory.
-                lists_dir = os.path.join(mm_cfg.VAR_PREFIX, 'lists')
-                # To make reactivation easier, we temporarily cd to the
-                # $var/lists directory and make the tarball from there.
-                old_cwd = os.getcwd()
-                # XXX BarryWarsaw 2007-08-02: Should we watch out for
-                # collisions on the tar file name?  This can only happen if
-                # the team is resurrected but the old archived tarball backup
-                # wasn't removed.
-                tgz_file_name = os.path.join(
-                    mm_cfg.VAR_PREFIX, 'backups', team_name + '.tgz')
-                tgz_file = tarfile.open(tgz_file_name, 'w:gz')
-                try:
-                    os.chdir(lists_dir)
-                    # .add() works recursively by default.
-                    tgz_file.add(team_name)
-                    # Now delete the list's directory.
-                    shutil.rmtree(team_name)
-                finally:
-                    tgz_file.close()
-                    os.chdir(old_cwd)
-            # We have to use a bare except here because of the legacy string
-            # exceptions that Mailman can raise.
-            except:
-                log_exception('List deletion error for team: %s', team_name)
-                statuses[team_name] = ('deactivate', 'failure')
-            else:
-                statuses[team_name] = ('deactivate', 'success')
-
-    def _check_held_messages(self):
-        """See if any held messages have been accepted or rejected."""
-        try:
-            dispositions = self._proxy.getMessageDispositions()
-        except (xmlrpclib.ProtocolError, socket.error) as error:
-            log_exception('Cannot talk to Launchpad:\n%s', error)
-            return
-        except xmlrpclib.Fault as error:
-            log_exception('Launchpad exception: %s', error)
-            return
-        if dispositions:
-            syslog('xmlrpc',
-                   'Received dispositions for these message-ids: %s',
-                   COMMASPACE.join(dispositions))
-        else:
-            return
-        # For each message that has been acted upon in Launchpad, handle the
-        # message in here in Mailman.  We need to resort the dispositions so
-        # that we can handle all of them for a particular mailing list at the
-        # same time.
-        by_list = {}
-        for message_id, (team_name, action) in dispositions.items():
-            accepts, declines, discards = by_list.setdefault(
-                team_name, ([], [], []))
-            if action == 'accept':
-                accepts.append(message_id)
-            elif action == 'decline':
-                declines.append(message_id)
-            elif action == 'discard':
-                discards.append(message_id)
-            else:
-                syslog('xmlrpc',
-                       'Skipping invalid disposition "%s" for message-id: %s',
-                       action, message_id)
-        # Now cycle through the dispositions for every mailing list.
-        for team_name in by_list:
-            try:
-                mlist = MailList(team_name)
-            except Errors.MMUnknownListError:
-                log_exception(
-                    'Skipping dispositions for unknown list: %s', team_name)
-                continue
-            try:
-                accepts, declines, discards = by_list[team_name]
-                for message_id in accepts:
-                    request_id = mlist.held_message_ids.pop(message_id, None)
-                    if request_id is None:
-                        syslog('xmlrpc', 'Missing accepted message-id: %s',
-                               message_id)
-                    else:
-                        mlist.HandleRequest(request_id, mm_cfg.APPROVE)
-                        syslog('xmlrpc', 'Approved: %s', message_id)
-                for message_id in declines:
-                    request_id = mlist.held_message_ids.pop(message_id, None)
-                    if request_id is None:
-                        syslog('xmlrpc', 'Missing declined message-id: %s',
-                               message_id)
-                    else:
-                        mlist.HandleRequest(request_id, mm_cfg.REJECT)
-                        syslog('xmlrpc', 'Rejected: %s', message_id)
-                for message_id in discards:
-                    request_id = mlist.held_message_ids.pop(message_id, None)
-                    if request_id is None:
-                        syslog('xmlrpc', 'Missing declined message-id: %s',
-                               message_id)
-                    else:
-                        mlist.HandleRequest(request_id, mm_cfg.DISCARD)
-                        syslog('xmlrpc', 'Discarded: %s', message_id)
-                mlist.Save()
-            finally:
-                mlist.Unlock()
-
-    def _resynchronize(self, actions, statuses):
-        """Process resynchronization actions.
-
-        actions is a sequence of 2-tuples specifying what needs to be
-        resynchronized.  The tuple is of the form (listname, current-status).
-
-        statuses is a dictionary mapping team names to one of the strings
-        'success' or 'failure'.
-        """
-        syslog('xmlrpc', 'resynchronizing: %s',
-               COMMASPACE.join(sorted(name for (name, status) in actions)))
-        for name, status in actions:
-            # There's no way to really know whether the original action
-            # succeeded or not, however, it's unlikely that an action would
-            # fail leaving the mailing list in a usable state.  Therefore, if
-            # the list is loadable and lockable, we'll say it succeeded.
-            try:
-                mlist = MailList(name)
-            except Errors.MMUnknownListError:
-                # The list doesn't exist on the Mailman side, so if its status
-                # is CONSTRUCTING, we can create it now.
-                if status == 'constructing':
-                    if self._create(name):
-                        statuses[name] = ('resynchronize', 'success')
-                    else:
-                        statuses[name] = ('resynchronize', 'failure')
-                else:
-                    # Any other condition leading to an unknown list is a
-                    # failure state.
-                    statuses[name] = ('resynchronize', 'failure')
-            except:
-                # Any other exception is also a failure.
-                statuses[name] = ('resynchronize', 'failure')
-                log_exception('Mailing list does not load: %s', name)
-            else:
-                # The list loaded just fine, so it successfully
-                # resynchronized.  Be sure to unlock it!
-                mlist.Unlock()
-                statuses[name] = ('resynchronize', 'success')
diff --git a/lib/lp/services/mailman/runmailman.py b/lib/lp/services/mailman/runmailman.py
deleted file mode 100644
index fba83fc..0000000
--- a/lib/lp/services/mailman/runmailman.py
+++ /dev/null
@@ -1,128 +0,0 @@
-# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Start and stop the Mailman processes."""
-
-__metaclass__ = type
-__all__ = [
-    'start_mailman',
-    'stop_mailman',
-    ]
-
-
-import errno
-import os
-import signal
-import subprocess
-import sys
-
-import scandir
-
-import lp.services.config
-from lp.services.mailman.config import configure_prefix
-from lp.services.mailman.monkeypatches import monkey_patch
-
-
-def mailmanctl(command, quiet=False, config=None, *additional_arguments):
-    """Run mailmanctl command.
-
-    :param command: the command to use.
-    :param quiet: when this is true, no output will happen unless, an error
-        happens.
-    :param config: The LaunchpadConfig object to take configuration from.
-        Defaults to the global one.
-    :param additional_arguments: additional command arguments to pass to the
-        mailmanctl program.
-    :raises RuntimeError: when quiet is True and the command failed.
-    """
-    if config is None:
-        config = lp.services.config.config
-    mailman_path = configure_prefix(config.mailman.build_prefix)
-    mailman_bin = os.path.join(mailman_path, 'bin')
-    args = ['./mailmanctl']
-    args.extend(additional_arguments)
-    args.append(command)
-    if quiet:
-        stdout = subprocess.PIPE
-        stderr = subprocess.STDOUT
-    else:
-        stdout = None
-        stderr = None
-    env = dict(os.environ)
-    env['LPCONFIG'] = config.instance_name
-    process = subprocess.Popen(
-        args, cwd=mailman_bin, stdout=stdout, stderr=stderr, env=env)
-    code = process.wait()
-    if code:
-        if quiet:
-            raise RuntimeError(
-                'mailmanctl %s failed: %d\n%s' % (
-                    command, code, process.stdout.read()))
-        else:
-            print >> sys.stderr, 'mailmanctl %s failed: %d' % (command, code)
-
-
-def stop_mailman(quiet=False, config=None):
-    """Alias for mailmanctl('stop')."""
-    mailmanctl('stop', quiet, config)
-    # Further, if the Mailman master pid file was not removed, then the
-    # master watcher, and probably one of its queue runners, did not die.
-    # Kill it hard and clean up after it.
-    if config is None:
-        config = lp.services.config.config
-    mailman_path = configure_prefix(config.mailman.build_prefix)
-    master_pid_path = os.path.join(mailman_path, 'data', 'master-qrunner.pid')
-    try:
-        master_pid_file = open(master_pid_path)
-    except IOError as error:
-        if error.errno == errno.ENOENT:
-            # It doesn't exist, so we're all done.
-            return
-        raise
-    try:
-        master_pid = int(master_pid_file.read().strip())
-    finally:
-        master_pid_file.close()
-    try:
-        # Kill the entire process group.
-        os.kill(master_pid, -signal.SIGKILL)
-    except OSError as error:
-        if error.errno == errno.ESRCH:
-            # The process does not exist.  It could be a zombie that has yet
-            # to be waited on, but let's not worry about that.
-            return
-        raise
-    try:
-        os.remove(master_pid_path)
-    except OSError as error:
-        if error.errno != errno.ENOENT:
-            raise
-    lock_dir = os.path.join(mailman_path, 'locks')
-    for entry in scandir.scandir(lock_dir):
-        os.remove(entry.path)
-
-
-def start_mailman(quiet=False, config=None):
-    """Start the Mailman master qrunner.
-
-    The client of start_mailman() is responsible for ensuring that
-    stop_mailman() is called at the appropriate time.
-
-    :param quiet: when this is true, no output will happen unless, an error
-        happens.
-    :param config: The LaunchpadConfig object to take configuration from.
-        Defaults to the global one.
-    :raises RuntimeException: when Mailman fails to start successfully.
-    """
-    if config is None:
-        config = lp.services.config.config
-    # We need the Mailman bin directory so we can run some of Mailman's
-    # command line scripts.
-    mailman_path = configure_prefix(config.mailman.build_prefix)
-
-    # Monkey-patch the installed Mailman 2.1 tree.
-    monkey_patch(mailman_path, config)
-    # Start Mailman.  Pass in the -s flag so that any stale master pid files
-    # will get deleted.  "Stale" means the process that owned the pid no
-    # longer exists, so this can't hurt anything.
-    mailmanctl('start', quiet, config, '-s')
diff --git a/lib/lp/services/mailman/scripts/__init__.py b/lib/lp/services/mailman/scripts/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/lib/lp/services/mailman/scripts/__init__.py
+++ /dev/null
diff --git a/lib/lp/services/mailman/scripts/mlist_sync.py b/lib/lp/services/mailman/scripts/mlist_sync.py
deleted file mode 100644
index 67f8032..0000000
--- a/lib/lp/services/mailman/scripts/mlist_sync.py
+++ /dev/null
@@ -1,211 +0,0 @@
-#!/usr/bin/python -S
-
-# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Sync Mailman data from one Launchpad to another."""
-
-# XXX BarryWarsaw 2008-02-12:
-# Things this script does NOT do correctly.
-#
-# - Fix up the deactivated lists.  This isn't done because that data lives in
-#   the backed up tar file, so handling this would mean untar'ing, tricking
-#   Mailman into loading the pickle (or manually loading and patching), then
-#   re-tar'ing.  I don't think it's worth it because the only thing that will
-#   be broken is if a list that's deactivated on production is re-activated on
-#   staging.
-#
-# - Backpatch all the message footers and RFC 2369 headers of the messages in
-#   the archive.  To do this, we'd have to iterate through all messages,
-#   tweaking the List-* headers (easy) and ripping apart the footers,
-#   recalculating them and reattaching them (difficult).  Doing the iteration
-#   and update is quite painful in Python 2.4, but would be easier with Python
-#   2.5's new mailbox module.  /Then/ we'd have to regenerate the archives.
-#   Not doing this means that some of the links in staging's MHonArc archive
-#   will point to production archives.
-
-import logging
-import os
-import subprocess
-import sys
-import textwrap
-import time
-
-from six.moves.xmlrpc_client import Fault
-
-from lp.services.config import config
-from lp.services.mailman.config import configure_prefix
-from lp.services.scripts.base import LaunchpadScript
-from lp.xmlrpc import faults
-
-
-RSYNC_OPTIONS = ('-avz', '--delete')
-RSYNC_COMMAND = '/usr/bin/rsync'
-RSYNC_SUBDIRECTORIES = ('archives', 'backups', 'lists', 'mhonarc')
-SPACE = ' '
-
-
-class MailingListSyncScript(LaunchpadScript):
-    """
-    %prog [options] source_url
-
-    Sync the Mailman data structures between production and staging.  This
-    takes the most efficient route by rsync'ing over the list pickles, raw
-    archive mboxes, and mhonarc files, then it fixes up anything that needs
-    fixing.  This does /not/ sync over any qfiles because staging doesn't send
-    emails anyway.
-
-    source_url is required and it is the rsync source url which contains
-    mailman's var directory.  The destination is taken from the launchpad.conf
-    file.
-    """
-
-    loglevel = logging.INFO
-    description = 'Sync the Mailman data structures with the database.'
-
-    def __init__(self, *args, **kwargs):
-        self.usage = textwrap.dedent(self.__doc__)
-        super(MailingListSyncScript, self).__init__(*args, **kwargs)
-
-    def add_my_options(self):
-        """See `LaunchpadScript`."""
-        # Add optional override of production mailing list host name.  In real
-        # use, it's always going to be lists.launchpad.net so that makes a
-        # reasonable default.  Some testing environments may override this.
-        self.parser.add_option('--hostname', default='lists.launchpad.net',
-                               help=('The hostname for the production '
-                                     'mailing list system.  This is used to '
-                                     'resolve teams which have multiple '
-                                     'email addresses.'))
-
-    def syncMailmanDirectories(self, source_url):
-        """Synchronize the Mailman directories.
-
-        :param source_url: the base url of the source
-        """
-        # This can't be done at module global scope.
-        from Mailman import mm_cfg
-        # Start by rsync'ing over the entire $vardir/lists, $vardir/archives,
-        # $vardir/backups, and $vardir/mhonarc directories.  We specifically
-        # do not rsync the data, locks, logs, qfiles, or spam directories.
-        destination_url = config.mailman.build_var_dir
-        # Do one rsync for all subdirectories.
-        rsync_command = [RSYNC_COMMAND]
-        rsync_command.extend(RSYNC_OPTIONS)
-        rsync_command.append('--exclude=%s' % mm_cfg.MAILMAN_SITE_LIST)
-        rsync_command.append('--exclude=%s.mbox' % mm_cfg.MAILMAN_SITE_LIST)
-        rsync_command.extend(os.path.join(source_url, subdirectory)
-                             for subdirectory in RSYNC_SUBDIRECTORIES)
-        rsync_command.append(destination_url)
-        self.logger.info('executing: %s', SPACE.join(rsync_command))
-        process = subprocess.Popen(rsync_command,
-                                   stdout=subprocess.PIPE,
-                                   stderr=subprocess.PIPE)
-        stdout, stderr = process.communicate()
-        if process.returncode == 0:
-            self.logger.debug('%s', stdout)
-        else:
-            self.logger.error('rsync command failed with exit status: %s',
-                              process.returncode)
-            self.logger.error('STDOUT:\n%s\nSTDERR:\n%s', stdout, stderr)
-        return process.returncode
-
-    def fixHostnames(self):
-        """Fix up the host names in Mailman and the LP database."""
-        # These can't be done at module global scope.
-        from Mailman import Utils
-        from Mailman import mm_cfg
-        from Mailman.MailList import MailList
-        from Mailman.Queue import XMLRPCRunner
-
-        # Ask Launchpad to update all the team email addresses.  This can be
-        # a bit timeout-prone if the relevant tables are cold, but should
-        # warm up reasonably quickly, so allow for a few retries.
-        proxy = XMLRPCRunner.get_mailing_list_api_proxy()
-        max_retries = 3
-        for i in range(max_retries + 1):
-            try:
-                proxy.updateTeamAddresses(self.options.hostname)
-            except Fault as fault:
-                if (fault.faultCode == faults.OopsOccurred.error_code and
-                        i < max_retries):
-                    self.logger.warning(
-                        "updateTeamAddresses OOPSed.  Retrying ...")
-                    time.sleep(2 ** i)
-                else:
-                    raise
-
-        # Clean things up per mailing list.
-        for list_name in Utils.list_names():
-            # Skip the site list.
-            if list_name == mm_cfg.MAILMAN_SITE_LIST:
-                continue
-
-            # The first thing to clean up is the mailing list pickles.  There
-            # are things like host names in some attributes that need to be
-            # converted.  The following opens a locked list.
-            mailing_list = MailList(list_name)
-            try:
-                mailing_list.host_name = mm_cfg.DEFAULT_EMAIL_HOST
-                mailing_list.web_page_url = (
-                    mm_cfg.DEFAULT_URL_PATTERN % mm_cfg.DEFAULT_URL_HOST)
-                mailing_list.Save()
-            finally:
-                mailing_list.Unlock()
-
-            try:
-                proxy.isTeamPublic(list_name)
-            except Fault as fault:
-                if fault.faultCode == faults.NoSuchPersonWithName.error_code:
-                    # We found a mailing list in Mailman that does not exist
-                    # in the Launchpad database.  This can happen if we
-                    # rsynced the Mailman directories after the lists were
-                    # created, but we copied the LP database /before/ the
-                    # lists were created.  If we don't delete the Mailman
-                    # lists, we won't be able to create the mailing lists on
-                    # staging.
-                    self.logger.error('No LP mailing list for: %s', list_name)
-                    self.deleteMailmanList(list_name)
-                    continue
-
-    def deleteMailmanList(self, list_name):
-        """Delete all Mailman data structures for `list_name`."""
-        mailman_bindir = os.path.normpath(os.path.join(
-            configure_prefix(config.mailman.build_prefix), 'bin'))
-        process = subprocess.Popen(('./rmlist', '-a', list_name),
-                                   stdout=subprocess.PIPE,
-                                   stderr=subprocess.PIPE,
-                                   cwd=mailman_bindir)
-        stdout, stderr = process.communicate()
-        if process.returncode == 0:
-            self.logger.info('%s', stdout)
-        else:
-            self.logger.error('rmlist command failed with exit status: %s',
-                              process.returncode)
-            self.logger.error('STDOUT:\n%s\nSTDERR:\n%s', stdout, stderr)
-            # Keep going.
-
-    def main(self):
-        """See `LaunchpadScript`."""
-        source_url = None
-        if len(self.args) == 0:
-            self.parser.error('Missing source_url')
-        elif len(self.args) > 1:
-            self.parser.error('Too many arguments')
-        else:
-            source_url = self.args[0]
-
-        # We need to get to the Mailman API.  Set up the paths so that Mailman
-        # can be imported.  This can't be done at module global scope.
-        mailman_path = configure_prefix(config.mailman.build_prefix)
-        sys.path.append(mailman_path)
-
-        retcode = self.syncMailmanDirectories(source_url)
-        if retcode != 0:
-            return retcode
-
-        self.fixHostnames()
-
-        # All done; commit the database changes.
-        self.txn.commit()
-        return 0
diff --git a/lib/lp/services/mailman/tests/__init__.py b/lib/lp/services/mailman/tests/__init__.py
deleted file mode 100644
index 569cf00..0000000
--- a/lib/lp/services/mailman/tests/__init__.py
+++ /dev/null
@@ -1,159 +0,0 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Test helpers for mailman integration."""
-
-__metaclass__ = type
-__all__ = []
-
-from contextlib import contextmanager
-import email
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
-import os
-import shutil
-
-from Mailman import (
-    MailList,
-    Message,
-    mm_cfg,
-    )
-from Mailman.Logging.Syslog import syslog
-from Mailman.Queue import XMLRPCRunner
-from Mailman.Queue.sbcache import get_switchboard
-from zope.security.proxy import removeSecurityProxy
-
-from lp.registry.tests.mailinglists_helper import MailingListXMLRPCTestProxy
-from lp.testing import TestCaseWithFactory
-from lp.testing.layers import DatabaseFunctionalLayer
-
-
-def get_mailing_list_api_test_proxy():
-    return MailingListXMLRPCTestProxy(context=None, request=None)
-
-
-@contextmanager
-def fake_mailinglist_api_proxy():
-    original_get_proxy = XMLRPCRunner.get_mailing_list_api_proxy
-    XMLRPCRunner.get_mailing_list_api_proxy = get_mailing_list_api_test_proxy
-    try:
-        yield
-    finally:
-        XMLRPCRunner.get_mailing_list_api_proxy = original_get_proxy
-
-
-class MailmanTestCase(TestCaseWithFactory):
-    """TestCase with factory and mailman support."""
-
-    layer = DatabaseFunctionalLayer
-
-    def setUp(self):
-        super(MailmanTestCase, self).setUp()
-        # Replace the xmlrpc proxy with a fast wrapper of the real view.
-        self.useContext(fake_mailinglist_api_proxy())
-
-    def tearDown(self):
-        super(MailmanTestCase, self).tearDown()
-        self.cleanMailmanList(self.mm_list)
-
-    def makeMailmanList(self, lp_mailing_list):
-        team = lp_mailing_list.team
-        owner_email = removeSecurityProxy(team.teamowner).preferredemail.email
-        return self.makeMailmanListWithoutTeam(team.name, owner_email)
-
-    def makeMailmanListWithoutTeam(self, list_name, owner_email):
-        # This utility is based on mailman/tests/TestBase.py.
-        self.cleanMailmanList(None, list_name)
-        mlist = MailList.MailList()
-        mlist.Create(list_name, owner_email, 'password')
-        mlist.host_name = mm_cfg.DEFAULT_URL_HOST
-        mlist.web_page_url = 'http://%s/mailman/' % mm_cfg.DEFAULT_URL_HOST
-        mlist.personalize = 1
-        mlist.include_rfc2369_headers = False
-        mlist.use_dollar_strings = True
-        mlist.Save()
-        mlist.addNewMember(owner_email)
-        return mlist
-
-    def cleanMailmanList(self, mlist, list_name=None):
-        # This utility is based on mailman/tests/TestBase.py.
-        if mlist is not None:
-            mlist.Unlock()
-            list_name = mlist.internal_name()
-        paths = [
-            'lists/%s',
-            'archives/private/%s',
-            'archives/private/%s.mbox',
-            'archives/public/%s',
-            'archives/public/%s.mbox',
-            'mhonarc/%s',
-            ]
-        for dirtmpl in paths:
-            list_dir = os.path.join(mm_cfg.VAR_PREFIX, dirtmpl % list_name)
-            if os.path.islink(list_dir):
-                os.unlink(list_dir)
-            elif os.path.isdir(list_dir):
-                shutil.rmtree(list_dir, ignore_errors=True)
-
-    def makeMailmanMessage(self, mm_list, sender, subject, content,
-                           mime_type='plain', attachment=None):
-        # Make a Mailman Message.Message.
-        if isinstance(sender, (list, tuple)):
-            sender = ', '.join(sender)
-        message = MIMEMultipart()
-        message['from'] = sender
-        message['to'] = mm_list.getListAddress()
-        message['subject'] = subject
-        message['message-id'] = self.getUniqueString()
-        message.attach(MIMEText(content, mime_type))
-        if attachment is not None:
-            # Rewrap the text message in a multipart message and add the
-            # attachment.
-            message.attach(attachment)
-        mm_message = email.message_from_string(
-            message.as_string(), Message.Message)
-        return mm_message
-
-    def get_log_entry(self, match_text):
-        """Return the first matched text line found in the log."""
-        log_path = syslog._logfiles['xmlrpc']._Logger__filename
-        mark = None
-        with open(log_path, 'r') as log_file:
-            for line in log_file.readlines():
-                if match_text in line:
-                    mark = line
-                    break
-        return mark
-
-    def get_mark(self):
-        """Return the --MARK-- entry from the log or None."""
-        return self.get_log_entry('--MARK--')
-
-    def reset_log(self):
-        """Truncate the log."""
-        log_path = syslog._logfiles['xmlrpc']._Logger__filename
-        syslog._logfiles['xmlrpc'].close()
-        with open(log_path, 'w') as log_file:
-            log_file.truncate()
-        syslog.write_ex('xmlrpc', 'Reset by test.')
-
-    def assertIsEnqueued(self, msg):
-        """Assert the message was appended to the incoming queue."""
-        switchboard = get_switchboard(mm_cfg.INQUEUE_DIR)
-        file_path = switchboard.files()[-1]
-        queued_msg, queued_msg_data = switchboard.dequeue(file_path)
-        self.assertEqual(msg['message-id'], queued_msg['message-id'])
-
-    @contextmanager
-    def raise_proxy_exception(self, method_name):
-        """Raise an exception when calling the passed proxy method name."""
-
-        def raise_exception(*args):
-            raise Exception('Test exception handling.')
-
-        proxy = XMLRPCRunner.get_mailing_list_api_proxy()
-        original_method = getattr(proxy.__class__, method_name)
-        setattr(proxy.__class__, method_name, raise_exception)
-        try:
-            yield
-        finally:
-            setattr(proxy.__class__, method_name, original_method)
diff --git a/lib/lp/services/mailman/tests/test_integration.py b/lib/lp/services/mailman/tests/test_integration.py
deleted file mode 100644
index 184b283..0000000
--- a/lib/lp/services/mailman/tests/test_integration.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# Copyright 2012 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Test the compliation and configuration of the Lp mailman instance."""
-
-__metaclass__ = type
-__all__ = []
-
-import os
-import subprocess
-import sys
-
-from Mailman.mm_cfg import MAILMAN_SITE_LIST
-
-from lp.services.config import config
-from lp.services.mailman.config import configure_prefix
-from lp.services.mailman.tests import MailmanTestCase
-from lp.testing import person_logged_in
-from lp.testing.layers import DatabaseFunctionalLayer
-
-
-def site_list_callable(mlist):
-    if mlist.internal_name() == MAILMAN_SITE_LIST:
-        sys.exit(99)
-    sys.exit(1)
-
-
-def can_import_callable(mlist):
-    try:
-        import lp.services.mailman
-        lp
-    except ImportError:
-        sys.exit(1)
-    else:
-        sys.exit(99)
-
-
-class CommandsTestCase(MailmanTestCase):
-    """Test mailman binary commands use the Lp compiled Mailman."""
-
-    layer = DatabaseFunctionalLayer
-
-    def setUp(self):
-        super(CommandsTestCase, 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(CommandsTestCase, self).tearDown()
-        self.cleanMailmanList(self.mm_list)
-
-    @staticmethod
-    def withlist(callable):
-        callable_path = '%s.%s' % (__name__, callable)
-        site_list = 'unused_mailman_site_list'
-        prefix_path = configure_prefix(config.mailman.build_prefix)
-        mailman_bin = os.path.join(prefix_path, 'bin')
-        command = './withlist -q -r %s %s' % (callable_path, site_list)
-        return subprocess.call(command.split(), cwd=mailman_bin)
-
-    def test_withlist_sitelist(self):
-        # Mailman's site list must be the Lp configured one.
-        self.assertEqual(99, self.withlist('site_list_callable'))
-
-    def test_withlist_import_lp_mailman(self):
-        # Lp's mailman can be imported.
-        self.assertEqual(99, self.withlist('can_import_callable'))
-
-    def test_lib_mailman(self):
-        # lib/mailman uses the Lp configured data directories.
-        lp_user_email = 'albatros@xxxxxx'
-        lp_user = self.factory.makePerson(name='albatros', email=lp_user_email)
-        with person_logged_in(lp_user):
-            lp_user.join(self.team)
-        message = self.makeMailmanMessage(
-            self.mm_list, lp_user_email, 'subject', 'any content.')
-        binary = os.path.join(config.root, 'lib/mailman/mail/mailman')
-        mailman = subprocess.Popen(
-            (binary, 'post', 'team-1'),
-            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE)
-        stdout, stderr = mailman.communicate(message.as_string())
-        self.assertEqual(0, mailman.returncode)
-        self.assertEqual('', stdout)
-        self.assertEqual('', stderr)
diff --git a/lib/lp/services/mailman/tests/test_lphandler.py b/lib/lp/services/mailman/tests/test_lphandler.py
deleted file mode 100644
index 4cec4d1..0000000
--- a/lib/lp/services/mailman/tests/test_lphandler.py
+++ /dev/null
@@ -1,97 +0,0 @@
-# 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 lp.services.mailman.tests import MailmanTestCase
-from lp.testing.layers import DatabaseFunctionalLayer
-
-
-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'
-        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 itself 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'])
-
-    def test_proxy_error_retries_message(self):
-        # When the Launchpad xmlrpc proxy raises an error, the message
-        # is re-enqueed.
-        lp_user_email = 'groundhog@xxxxxx'
-        self.factory.makePerson(email=lp_user_email)
-        message = self.makeMailmanMessage(
-            self.mm_list, lp_user_email, 'subject', 'any content.')
-        msg_data = {}
-        with self.raise_proxy_exception('isRegisteredInLaunchpad'):
-            args = (self.mm_list, message, msg_data)
-            self.assertRaises(
-                Errors.DiscardMessage, LaunchpadMember.process, *args)
-            self.assertIsEnqueued(message)
diff --git a/lib/lp/services/mailman/tests/test_lpheaders.py b/lib/lp/services/mailman/tests/test_lpheaders.py
deleted file mode 100644
index 59c0152..0000000
--- a/lib/lp/services/mailman/tests/test_lpheaders.py
+++ /dev/null
@@ -1,103 +0,0 @@
-# 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 lp.services.mailman.tests import MailmanTestCase
-from lp.testing.layers import DatabaseFunctionalLayer
-
-
-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.test>', message['List-Id'])
-        self.assertEqual(
-            '<http://help.launchpad.test/ListHelp>', message['List-Help'])
-        self.assertEqual(
-            '<http://launchpad.test/~team-1>', message['List-Subscribe'])
-        self.assertEqual(
-            '<http://launchpad.test/~team-1>', message['List-Unsubscribe'])
-        self.assertEqual(
-            '<mailto:team-1@xxxxxxxxxxxxxxxxxxxx>', message['List-Post'])
-        self.assertEqual(
-            '<http://lists.launchpad.test/team-1>', message['List-Archive'])
-        self.assertEqual(
-            '<http://launchpad.test/~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.test/~team-1',
-            decoration_data['list_owner'])
-        self.assertEqual(
-            'team-1@xxxxxxxxxxxxxxxxxxxx',
-            decoration_data['list_post'])
-        self.assertEqual(
-            'http://launchpad.test/~team-1',
-            decoration_data['list_unsubscribe'])
-        self.assertEqual(
-            'http://help.launchpad.test/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.test/~team-1\n";
-            "Post to     : team-1@xxxxxxxxxxxxxxxxxxxx\n"
-            "Unsubscribe : http://launchpad.test/~team-1\n";
-            "More help   : http://help.launchpad.test/ListHelp\n";)
-        self.assertEqual(expected, footer)
diff --git a/lib/lp/services/mailman/tests/test_lpmoderate.py b/lib/lp/services/mailman/tests/test_lpmoderate.py
deleted file mode 100644
index e3e5e7c..0000000
--- a/lib/lp/services/mailman/tests/test_lpmoderate.py
+++ /dev/null
@@ -1,113 +0,0 @@
-# Copyright 20010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Test the lpmoderate monekypatches"""
-
-__metaclass__ = type
-__all__ = []
-
-from Mailman import Errors
-from Mailman.Handlers import LPModerate
-from zope.security.proxy import removeSecurityProxy
-
-from lp.services.mailman.tests import MailmanTestCase
-from lp.testing.layers import LaunchpadFunctionalLayer
-
-
-class TestLPModerateTestCase(MailmanTestCase):
-    """Test lpmoderate.
-
-    Mailman process() methods quietly return. They may set msg_data key-values
-    or raise an error to end processing. These 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 = LaunchpadFunctionalLayer
-
-    def setUp(self):
-        super(TestLPModerateTestCase, 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 = 'capybara@xxxxxx'
-        self.lp_user = self.factory.makePerson(email=self.lp_user_email)
-
-    def tearDown(self):
-        super(TestLPModerateTestCase, self).tearDown()
-        self.cleanMailmanList(self.mm_list)
-
-    def test_process_message_from_preapproved(self):
-        # Any message mark with approval will silently complete the process.
-        message = self.makeMailmanMessage(
-            self.mm_list, self.lp_user_email, 'subject', 'any content.')
-        msg_data = dict(approved=True)
-        silence = LPModerate.process(self.mm_list, message, msg_data)
-        self.assertEqual(None, silence)
-
-    def test_process_message_from_subscriber(self):
-        # Messages from subscribers silently complete the process.
-        subscriber_email = removeSecurityProxy(
-            self.team.teamowner).preferredemail.email
-        message = self.makeMailmanMessage(
-            self.mm_list, subscriber_email, 'subject', 'any content.')
-        msg_data = {}
-        silence = LPModerate.process(self.mm_list, message, msg_data)
-        self.assertEqual(None, silence)
-
-    def test_process_message_from_lp_user_held_for_moderation(self):
-        # Messages from Launchpad users are held for moderation.
-        message = self.makeMailmanMessage(
-            self.mm_list, self.lp_user_email, 'subject', 'content')
-        msg_data = {}
-        args = (self.mm_list, message, msg_data)
-        self.assertRaises(
-            Errors.HoldMessage, LPModerate.process, *args)
-        self.assertEqual(1, self.mailing_list.getReviewableMessages().count())
-
-    def test_process_message_with_non_ascii_from_lp_user_held(self):
-        # Non-ascii messages can be held for moderation.
-        non_ascii_email = 'I \xa9 M <%s>' % self.lp_user_email.encode('ascii')
-        message = self.makeMailmanMessage(
-            self.mm_list, non_ascii_email, 'subject \xa9', 'content \xa9')
-        msg_data = {}
-        args = (self.mm_list, message, msg_data)
-        self.assertRaises(
-            Errors.HoldMessage, LPModerate.process, *args)
-        self.assertEqual(1, self.mailing_list.getReviewableMessages().count())
-
-    def test_process_duplicate_message_discarded(self):
-        # Messages are discarded is they are already held for moderation.
-        message = self.makeMailmanMessage(
-            self.mm_list, self.lp_user_email, 'subject', 'content')
-        self.mm_list.held_message_ids = {message['message-id']: message}
-        msg_data = {}
-        args = (self.mm_list, message, msg_data)
-        self.assertRaises(
-            Errors.DiscardMessage, LPModerate.process, *args)
-        self.assertEqual(0, self.mailing_list.getReviewableMessages().count())
-
-    def test_process_empty_mesage_from_nonsubcriber_discarded(self):
-        # Messages from Launchpad users without text content are discarded.
-        spam_message = self.makeMailmanMessage(
-            self.mm_list, self.lp_user_email,
-            'get drugs', '<a><img /></a>.', mime_type='html')
-        msg_data = dict(approved=False)
-        args = (self.mm_list, spam_message, msg_data)
-        self.assertRaises(
-            Errors.DiscardMessage, LPModerate.process, *args)
-        self.assertEqual(0, self.mailing_list.getReviewableMessages().count())
-
-    def test_process_message_from_list_discarded(self):
-        # Messages that claim to be from the list itself (not a subcriber) are
-        # discarded because Mailman's internal handlers did not set 'approve'
-        # in msg_data.
-        list_email = 'spammer <%s>' % self.mailing_list.address
-        message = self.makeMailmanMessage(
-            self.mm_list, list_email, 'subject', 'any content.')
-        msg_data = {}
-        args = (self.mm_list, message, msg_data)
-        self.assertRaises(
-            Errors.DiscardMessage, LPModerate.process, *args)
-        self.assertEqual(0, self.mailing_list.getReviewableMessages().count())
diff --git a/lib/lp/services/mailman/tests/test_lpsize.py b/lib/lp/services/mailman/tests/test_lpsize.py
deleted file mode 100644
index 38bd4aa..0000000
--- a/lib/lp/services/mailman/tests/test_lpsize.py
+++ /dev/null
@@ -1,133 +0,0 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Test the lpsize monekypatches"""
-
-from __future__ import with_statement
-
-__metaclass__ = type
-__all__ = []
-
-
-from email.mime.application import MIMEApplication
-
-from Mailman import Errors
-from Mailman.Handlers import LPSize
-from zope.security.proxy import removeSecurityProxy
-
-from lp.services.config import config
-from lp.services.mailman.tests import MailmanTestCase
-from lp.testing.layers import (
-    DatabaseFunctionalLayer,
-    LaunchpadFunctionalLayer,
-    )
-
-
-class TestLPSizeTestCase(MailmanTestCase):
-    """Test LPSize.
-
-    Mailman process() methods quietly return. They may set msg_data key-values
-    or raise an error to end processing. These 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 = LaunchpadFunctionalLayer
-
-    def setUp(self):
-        super(TestLPSizeTestCase, self).setUp()
-        self.team, self.mailing_list = self.factory.makeTeamAndMailingList(
-            'team-1', 'team-1-owner')
-        self.mm_list = self.makeMailmanList(self.mailing_list)
-        self.subscriber_email = removeSecurityProxy(
-            self.team.teamowner.preferredemail).email
-
-    def tearDown(self):
-        super(TestLPSizeTestCase, self).tearDown()
-        self.cleanMailmanList(self.mm_list)
-
-    def test_process_size_under_soft_limit(self):
-        # Any message under 40kb is sent to the list.
-        attachment = MIMEApplication(
-            '\n'.join(['x' * 20] * 1000), 'octet-stream')
-        message = self.makeMailmanMessage(
-            self.mm_list, self.subscriber_email, 'subject', 'content',
-            attachment=attachment)
-        msg_data = {}
-        silence = LPSize.process(self.mm_list, message, msg_data)
-        self.assertEqual(None, silence)
-
-    def test_process_size_over_soft_limit_held(self):
-        # Messages over 40kb held for moderation.
-        self.assertEqual(40000, config.mailman.soft_max_size)
-        attachment = MIMEApplication(
-            '\n'.join(['x' * 40] * 1000), 'octet-stream')
-        message = self.makeMailmanMessage(
-            self.mm_list, self.subscriber_email, 'subject', 'content',
-            attachment=attachment)
-        msg_data = {}
-        args = (self.mm_list, message, msg_data)
-        self.assertRaises(
-            Errors.HoldMessage, LPSize.process, *args)
-        self.assertEqual(1, self.mailing_list.getReviewableMessages().count())
-
-    def test_process_size_over_hard_limit_discarded(self):
-        # Messages over 1MB are discarded.
-        self.assertEqual(1000000, config.mailman.hard_max_size)
-        attachment = MIMEApplication(
-            '\n'.join(['x' * 1000] * 1000), 'octet-stream')
-        message = self.makeMailmanMessage(
-            self.mm_list, self.subscriber_email, 'subject', 'content',
-            attachment=attachment)
-        msg_data = {}
-        args = (self.mm_list, message, msg_data)
-        self.assertRaises(
-            Errors.DiscardMessage, LPSize.process, *args)
-        self.assertEqual(0, self.mailing_list.getReviewableMessages().count())
-
-
-class TestTruncatedMessage(MailmanTestCase):
-    """Test truncated_message helper."""
-
-    layer = DatabaseFunctionalLayer
-
-    def setUp(self):
-        super(TestTruncatedMessage, self).setUp()
-        self.team, self.mailing_list = self.factory.makeTeamAndMailingList(
-            'team-1', 'team-1-owner')
-        self.mm_list = self.makeMailmanList(self.mailing_list)
-        self.subscriber_email = removeSecurityProxy(
-            self.team.teamowner.preferredemail).email
-
-    def test_attchments_are_removed(self):
-        # Plain-text and multipart are preserved, everything else is removed.
-        attachment = MIMEApplication('binary gibberish', 'octet-stream')
-        message = self.makeMailmanMessage(
-            self.mm_list, self.subscriber_email, 'subject', 'content',
-            attachment=attachment)
-        moderated_message = LPSize.truncated_message(message)
-        parts = [part for part in moderated_message.walk()]
-        types = [part.get_content_type() for part in parts]
-        self.assertEqual(['multipart/mixed', 'text/plain'], types)
-
-    def test_small_text_is_preserved(self):
-        # Text parts below the limit are unchanged.
-        message = self.makeMailmanMessage(
-            self.mm_list, self.subscriber_email, 'subject', 'content')
-        moderated_message = LPSize.truncated_message(message, limit=1000)
-        parts = [part for part in moderated_message.walk()]
-        types = [part.get_content_type() for part in parts]
-        self.assertEqual(['multipart/mixed', 'text/plain'], types)
-        self.assertEqual('content', parts[1].get_payload())
-
-    def test_large_text_is_truncated(self):
-        # Text parts above the limit are truncated.
-        message = self.makeMailmanMessage(
-            self.mm_list, self.subscriber_email, 'subject', 'content excess')
-        moderated_message = LPSize.truncated_message(message, limit=7)
-        parts = [part for part in moderated_message.walk()]
-        types = [part.get_content_type() for part in parts]
-        self.assertEqual(['multipart/mixed', 'text/plain'], types)
-        self.assertEqual(
-            'content\n[truncated for moderation]', parts[1].get_payload())
diff --git a/lib/lp/services/mailman/tests/test_lpstanding.py b/lib/lp/services/mailman/tests/test_lpstanding.py
deleted file mode 100644
index d84db74..0000000
--- a/lib/lp/services/mailman/tests/test_lpstanding.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# 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 import Errors
-from Mailman.Handlers import LPStanding
-
-from lp.registry.interfaces.person import PersonalStanding
-from lp.services.mailman.tests import MailmanTestCase
-from lp.testing import celebrity_logged_in
-from lp.testing.layers import DatabaseFunctionalLayer
-
-
-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'])
-
-    def test_proxy_error_retries_message(self):
-        # When the Launchpad xmlrpc proxy raises an error, the message
-        # is re-enqueed.
-        message = self.makeMailmanMessage(
-            self.mm_list, self.lp_user_email, 'subject', 'any content.')
-        msg_data = {}
-        with self.raise_proxy_exception('inGoodStanding'):
-            args = (self.mm_list, message, msg_data)
-            self.assertRaises(
-                Errors.DiscardMessage, LPStanding.process, *args)
-            self.assertIsEnqueued(message)
diff --git a/lib/lp/services/mailman/tests/test_mlist_sync.py b/lib/lp/services/mailman/tests/test_mlist_sync.py
deleted file mode 100644
index adf1123..0000000
--- a/lib/lp/services/mailman/tests/test_mlist_sync.py
+++ /dev/null
@@ -1,166 +0,0 @@
-# Copyright 2011-2019 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Test the  mlist-sync script."""
-
-__metaclass__ = type
-__all__ = []
-
-from contextlib import contextmanager
-import os
-import shutil
-import tempfile
-
-from Mailman import mm_cfg
-from Mailman.MailList import MailList
-from Mailman.Utils import list_names
-import transaction
-
-from lp.services.config import config
-from lp.services.database.interfaces import IStore
-from lp.services.identity.model.emailaddress import EmailAddressSet
-from lp.services.log.logger import BufferLogger
-from lp.services.mailman.scripts.mlist_sync import MailingListSyncScript
-from lp.services.mailman.tests import MailmanTestCase
-from lp.testing import person_logged_in
-from lp.testing.dbuser import dbuser
-from lp.testing.layers import ZopelessDatabaseLayer
-
-
-@contextmanager
-def production_config(host_name):
-    """Simulate a production Launchpad and mailman config."""
-    config.push('production', """\
-        [mailman]
-        build_host_name: %s
-        """ % host_name)
-    default_email_host = mm_cfg.DEFAULT_EMAIL_HOST
-    mm_cfg.DEFAULT_EMAIL_HOST = host_name
-    default_url_host = mm_cfg.DEFAULT_URL_HOST
-    mm_cfg.DEFAULT_URL_HOST = host_name
-    try:
-        yield
-    finally:
-        mm_cfg.DEFAULT_URL_HOST = default_url_host
-        mm_cfg.DEFAULT_EMAIL_HOST = default_email_host
-        config.pop('production')
-
-
-@contextmanager
-def staging_config():
-    """Simulate a staging Launchpad config."""
-    config.push('staging', """\
-        [launchpad]
-        is_demo: True
-        """)
-    try:
-        yield
-    finally:
-        config.pop('staging')
-
-
-class TestMListSync(MailmanTestCase):
-    """Test mlist-sync script."""
-
-    layer = ZopelessDatabaseLayer
-
-    def setUp(self):
-        super(TestMListSync, self).setUp()
-        self.host_name = 'lists.production.launchpad.test'
-        with production_config(self.host_name):
-            self.team = self.factory.makeTeam(name='team-1')
-            self.mailing_list = self.factory.makeMailingList(
-                self.team, self.team.teamowner)
-            self.mm_list = self.makeMailmanList(self.mailing_list)
-            self.mm_list.Unlock()
-        self.addCleanup(self.cleanMailmanList, None, self.mm_list)
-        archive_dir = os.path.join(mm_cfg.VAR_PREFIX, 'mhonarc')
-        os.makedirs(os.path.join(archive_dir, self.team.name))
-        self.addCleanup(shutil.rmtree, archive_dir, ignore_errors=True)
-        self.naked_email_address_set = EmailAddressSet()
-
-    def setupProductionFiles(self):
-        "Setup a production file structure to sync."
-        tempdir = tempfile.mkdtemp()
-        source_dir = os.path.join(tempdir, 'production')
-        shutil.copytree(
-            config.mailman.build_var_dir, source_dir, symlinks=True)
-        self.addCleanup(shutil.rmtree, source_dir, ignore_errors=True)
-        return source_dir
-
-    def runMListSync(self, source_dir):
-        """Run mlist-sync.py."""
-        store = IStore(self.team)
-        store.flush()
-        transaction.commit()
-        store.invalidate()
-        script = MailingListSyncScript(
-            test_args=['--hostname', self.host_name, source_dir],
-            logger=BufferLogger())
-        script.txn = transaction
-        try:
-            with dbuser('mlist-sync'), staging_config():
-                return script.main()
-        finally:
-            self.addDetail('log', script.logger.content)
-
-    def getListInfo(self):
-        """Return a list of 4-tuples of Mailman mailing list info."""
-        list_info = []
-        for list_name in sorted(list_names()):
-            if list_name == mm_cfg.MAILMAN_SITE_LIST:
-                continue
-            mailing_list = MailList(list_name, lock=False)
-            list_address = mailing_list.getListAddress()
-            if self.naked_email_address_set.getByEmail(list_address) is None:
-                email = '%s not found' % list_address
-            else:
-                email = list_address
-            list_info.append(
-                (mailing_list.internal_name(), mailing_list.host_name,
-                 mailing_list.web_page_url, email))
-        return list_info
-
-    def test_staging_sync(self):
-        # List is synced with updated URLs and email addresses.
-        source_dir = self.setupProductionFiles()
-        self.assertEqual(0, self.runMListSync(source_dir))
-        list_summary = [(
-            'team-1',
-            'lists.launchpad.test',
-            'http://lists.launchpad.test/mailman/',
-            'team-1@xxxxxxxxxxxxxxxxxxxx')]
-        self.assertEqual(list_summary, self.getListInfo())
-
-    def test_staging_sync_list_without_team(self):
-        # Lists without a team are not synced. This happens when a team
-        # is deleted, but the list and archive remain.
-        with production_config(self.host_name):
-            mlist = self.makeMailmanListWithoutTeam('no-team', 'ex@xxxxxx')
-            mlist.Unlock()
-            os.makedirs(os.path.join(
-                mm_cfg.VAR_PREFIX, 'mhonarc', 'no-team'))
-        self.addCleanup(self.cleanMailmanList, None, 'no-team')
-        source_dir = self.setupProductionFiles()
-        self.assertEqual(0, self.runMListSync(source_dir))
-        list_summary = [(
-            'team-1',
-            'lists.launchpad.test',
-            'http://lists.launchpad.test/mailman/',
-            'team-1@xxxxxxxxxxxxxxxxxxxx')]
-        self.assertEqual(list_summary, self.getListInfo())
-
-    def test_staging_sync_with_team_address(self):
-        # The team's other address is not updated by the sync process.
-        email = self.factory.makeEmail('team-1@xxxxxx', self.team)
-        with production_config(self.host_name):
-            self.team.setContactAddress(email)
-        source_dir = self.setupProductionFiles()
-        self.assertEqual(0, self.runMListSync(source_dir))
-        list_summary = [(
-            'team-1',
-            'lists.launchpad.test',
-            'http://lists.launchpad.test/mailman/',
-            'team-1@xxxxxxxxxxxxxxxxxxxx')]
-        self.assertEqual(list_summary, self.getListInfo())
-        with person_logged_in(self.team.teamowner):
-            self.assertEqual('team-1@xxxxxx', self.team.preferredemail.email)
diff --git a/lib/lp/services/mailman/tests/test_mm_cfg.py b/lib/lp/services/mailman/tests/test_mm_cfg.py
deleted file mode 100644
index 1f3faf0..0000000
--- a/lib/lp/services/mailman/tests/test_mm_cfg.py
+++ /dev/null
@@ -1,220 +0,0 @@
-# Copyright 20010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Test the Launchpad defaults monekypatch and mm_cfg."""
-
-__metaclass__ = type
-__all__ = []
-
-import os
-
-from Mailman import (
-    mm_cfg,
-    Utils,
-    )
-
-from lp.services.config import config
-from lp.services.mailman.config import configure_prefix
-from lp.services.mailman.monkeypatches import monkey_patch
-from lp.testing import TestCase
-from lp.testing.layers import FunctionalLayer
-
-
-class TestMMCfgDefaultsTestCase(TestCase):
-    """Test launchapd default overrides."""
-
-    layer = FunctionalLayer
-
-    def test_common_values(self):
-        # Launchpad's boolean and string parameters.
-        self.assertEqual('unused_mailman_site_list', mm_cfg.MAILMAN_SITE_LIST)
-        self.assertEqual(None, mm_cfg.MTA)
-        self.assertEqual(3, mm_cfg.DEFAULT_GENERIC_NONMEMBER_ACTION)
-        self.assertEqual(False, mm_cfg.DEFAULT_SEND_REMINDERS)
-        self.assertEqual(True, mm_cfg.DEFAULT_SEND_WELCOME_MSG)
-        self.assertEqual(False, mm_cfg.DEFAULT_SEND_GOODBYE_MSG)
-        self.assertEqual(False, mm_cfg.DEFAULT_DIGESTABLE)
-        self.assertEqual(False, mm_cfg.DEFAULT_BOUNCE_NOTIFY_OWNER_ON_DISABLE)
-        self.assertEqual(False, mm_cfg.DEFAULT_BOUNCE_NOTIFY_OWNER_ON_REMOVAL)
-        self.assertEqual(True, mm_cfg.VERP_PERSONALIZED_DELIVERIES)
-        self.assertEqual(False, mm_cfg.DEFAULT_FORWARD_AUTO_DISCARDS)
-        self.assertEqual(False, mm_cfg.DEFAULT_BOUNCE_PROCESSING)
-
-    def test_qrunners(self):
-        # The queue runners used by Launchpad.
-        runners = [pair[0] for pair in mm_cfg.QRUNNERS if pair[1] == 1]
-        expected = [
-            'ArchRunner', 'BounceRunner', 'IncomingRunner', 'OutgoingRunner',
-            'VirginRunner', 'RetryRunner', 'XMLRPCRunner']
-        self.assertEqual(expected, runners)
-
-    def test_global_pipeline(self):
-        # The ordered list of handlers used by Launchpad.
-        # NB. This is a very important list when debuggin were a message
-        # has been touched.
-        expected = [
-            'LaunchpadMember', 'SpamDetect', 'Approve', 'Replybot',
-            'LPStanding', 'LPModerate', 'LPSize',
-            'MimeDel', 'Scrubber', 'Emergency', 'Tagger', 'CalcRecips',
-            'AvoidDuplicates', 'Cleanse', 'CleanseDKIM', 'CookHeaders',
-            'LaunchpadHeaders', 'ToDigest', 'ToArchive', 'ToUsenet',
-            'AfterDelivery', 'Acknowledge', 'ToOutgoing']
-        self.assertEqual(expected, mm_cfg.GLOBAL_PIPELINE)
-
-
-class TestMMCfgLaunchpadConfigTestCase(TestCase):
-    """Test launchapd default overrides.
-
-    The mailman config is generated from the selected launchpad config.
-    The config will either be the test runner or the app server depending
-    on the what was previously used when mailman was run. The config must
-    be created in setup to ensure predicable values.
-    """
-
-    layer = FunctionalLayer
-
-    def setUp(self):
-        super(TestMMCfgLaunchpadConfigTestCase, self).setUp()
-        # Generate a mailman config using this environment's config.
-        mailman_path = configure_prefix(config.mailman.build_prefix)
-        monkey_patch(mailman_path, config)
-        reload(mm_cfg)
-
-    def test_mail_server(self):
-        # Launchpad's smtp config values.
-        host, port = config.mailman.smtp.split(':')
-        self.assertEqual(host, mm_cfg.SMTPHOST)
-        self.assertEqual(int(port), mm_cfg.SMTPPORT)
-
-    def test_smtp_max_config(self):
-        # Mailman SMTP max limits are configured from the LP config.
-        self.assertEqual(
-            config.mailman.smtp_max_rcpts,
-            mm_cfg.SMTP_MAX_RCPTS)
-        self.assertEqual(
-            config.mailman.smtp_max_sesions_per_connection,
-            mm_cfg.SMTP_MAX_SESSIONS_PER_CONNECTION)
-
-    def test_xmlrpc_server(self):
-        # Launchpad's smtp config values.
-        self.assertEqual(
-            config.mailman.xmlrpc_url,
-            mm_cfg.XMLRPC_URL)
-        self.assertEqual(
-            config.mailman.xmlrpc_runner_sleep,
-            mm_cfg.XMLRPC_SLEEPTIME)
-        self.assertEqual(
-            config.mailman.subscription_batch_size,
-            mm_cfg.XMLRPC_SUBSCRIPTION_BATCH_SIZE)
-        self.assertEqual(
-            config.mailman.shared_secret,
-            mm_cfg.LAUNCHPAD_SHARED_SECRET)
-
-    def test_messge_footer(self):
-        # Launchpad's email footer.
-        self.assertEqual(
-            config.mailman.list_help_header,
-            mm_cfg.LIST_HELP_HEADER)
-        self.assertEqual(
-            config.mailman.list_owner_header_template,
-            mm_cfg.LIST_SUBSCRIPTION_HEADERS)
-        self.assertEqual(
-            config.mailman.archive_url_template,
-            mm_cfg.LIST_ARCHIVE_HEADER_TEMPLATE)
-        self.assertEqual(
-            config.mailman.list_owner_header_template,
-            mm_cfg.LIST_OWNER_HEADER_TEMPLATE)
-        self.assertEqual(
-            "-- \n"
-            "Mailing list: $list_owner\n"
-            "Post to     : $list_post\n"
-            "Unsubscribe : $list_unsubscribe\n"
-            "More help   : $list_help\n",
-            mm_cfg.DEFAULT_MSG_FOOTER)
-
-    def test_message_rules(self):
-        # Launchpad's rules for handling messages.
-        self.assertEqual(
-            config.mailman.soft_max_size,
-            mm_cfg.LAUNCHPAD_SOFT_MAX_SIZE)
-        self.assertEqual(
-            config.mailman.hard_max_size,
-            mm_cfg.LAUNCHPAD_HARD_MAX_SIZE)
-        self.assertEqual(
-            config.mailman.register_bounces_every,
-            mm_cfg.REGISTER_BOUNCES_EVERY)
-
-    def test_archive_setup(self):
-        # Launchpad's rules for setting up list archives.
-        self.assertTrue('-add' in mm_cfg.PUBLIC_EXTERNAL_ARCHIVER)
-        self.assertTrue('-spammode' in mm_cfg.PUBLIC_EXTERNAL_ARCHIVER)
-        self.assertTrue('-umask 022'in mm_cfg.PUBLIC_EXTERNAL_ARCHIVER)
-        self.assertTextMatchesExpressionIgnoreWhitespace(
-            '-dbfile /var/tmp'
-            '/mailman/archives/private/%\(listname\)s.mbox/mhonarc.db',
-            mm_cfg.PUBLIC_EXTERNAL_ARCHIVER)
-        self.assertTextMatchesExpressionIgnoreWhitespace(
-            '-outdir /var/tmp/mailman/mhonarc/%\(listname\)s',
-            mm_cfg.PUBLIC_EXTERNAL_ARCHIVER)
-        self.assertTextMatchesExpressionIgnoreWhitespace(
-            '-definevar ML-NAME=%\(listname\)s',
-            mm_cfg.PUBLIC_EXTERNAL_ARCHIVER)
-        self.assertTextMatchesExpressionIgnoreWhitespace(
-            '-rcfile /var/tmp/mailman/data/lp-mhonarc-common.mrc',
-            mm_cfg.PUBLIC_EXTERNAL_ARCHIVER)
-        self.assertTextMatchesExpressionIgnoreWhitespace(
-            '-stderr /var/tmp/mailman/logs/mhonarc',
-            mm_cfg.PUBLIC_EXTERNAL_ARCHIVER)
-        self.assertTextMatchesExpressionIgnoreWhitespace(
-            '-stdout /var/tmp/mailman/logs/mhonarc',
-            mm_cfg.PUBLIC_EXTERNAL_ARCHIVER)
-        self.assertEqual(
-            mm_cfg.PRIVATE_EXTERNAL_ARCHIVER, mm_cfg.PUBLIC_EXTERNAL_ARCHIVER)
-
-
-class TestMHonArchMRC(TestCase):
-    """Test the archive configuration."""
-
-    layer = FunctionalLayer
-
-    def test_html_disabled(self):
-        # HTML messages are ignored because of CVE-2010-4524.
-        mrc_path = os.path.join(
-            config.root, 'lib', 'lp', 'services', 'mailman', 'monkeypatches',
-            'lp-mhonarc-common.mrc')
-        with open(mrc_path) as mrc_file:
-            self.mrc = mrc_file.read()
-        mime_excs = (
-            '<MIMEExcs> '
-            'text/html '
-            'text/x-html '
-            '</MIMEExcs> ')
-        self.assertTextMatchesExpressionIgnoreWhitespace(
-            mime_excs, self.mrc)
-
-
-class TestSiteTemplates(TestCase):
-    """Test launchapd site templates."""
-
-    layer = FunctionalLayer
-
-    def test_postheld(self):
-        postheld_dict = {
-            'listname': 'fake-list',
-            'hostname': 'lists.launchpad.net',
-            'reason': 'XXX',
-            'sender': 'test@xxxxxxxxxxxxx',
-            'subject': "YYY",
-            'admindb_url': 'http://lists.launchpad.net/fake/admin',
-            }
-        text, file_name = Utils.findtext('postheld.txt', dict=postheld_dict)
-        self.assertTrue(
-            file_name.endswith('/lib/mailman/templates/site/en/postheld.txt'))
-        self.assertEqual(
-            "Your mail to 'fake-list' with the subject\n\n"
-            "    YYY\n\n"
-            "Is being held until the list moderator can review it for "
-            "approval.\n\nThe reason it is being held:\n\n"
-            "    XXX\n\n"
-            "Either the message will get posted to the list, or you will "
-            "receive\nnotification of the moderator's decision.\n",
-            text)
diff --git a/lib/lp/services/mailman/tests/test_xmlrpcrunner.py b/lib/lp/services/mailman/tests/test_xmlrpcrunner.py
deleted file mode 100644
index fa9158e..0000000
--- a/lib/lp/services/mailman/tests/test_xmlrpcrunner.py
+++ /dev/null
@@ -1,457 +0,0 @@
-# Copyright 20011 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Test the Launchpad XMLRPC runner."""
-
-__metaclass__ = type
-__all__ = []
-
-from contextlib import contextmanager
-from datetime import datetime
-import os
-import socket
-import tarfile
-
-from Mailman import (
-    Errors,
-    MailList,
-    mm_cfg,
-    )
-from Mailman.Logging.Syslog import syslog
-from Mailman.Queue.XMLRPCRunner import (
-    handle_proxy_error,
-    XMLRPCRunner,
-    )
-from Mailman.Utils import list_names
-from zope.component import getUtility
-from zope.security.proxy import removeSecurityProxy
-
-from lp.registry.interfaces.mailinglist import (
-    IMailingListSet,
-    MailingListStatus,
-    )
-from lp.services.config import config
-from lp.services.mailman.monkeypatches.xmlrpcrunner import (
-    get_mailing_list_api_proxy,
-    )
-from lp.services.mailman.tests import (
-    get_mailing_list_api_test_proxy,
-    MailmanTestCase,
-    )
-from lp.services.xmlrpc import Transport
-from lp.testing import (
-    monkey_patch,
-    person_logged_in,
-    TestCase,
-    )
-from lp.testing.fixture import CaptureOops
-from lp.testing.layers import (
-    BaseLayer,
-    DatabaseFunctionalLayer,
-    )
-
-
-@contextmanager
-def one_loop_exception(runner):
-    """Raise an error during th execution of _oneloop.
-
-    This function replaces _check_list_actions() with a function that
-    raises an error. _oneloop() handles the exception.
-    """
-
-    def raise_exception():
-        raise Exception('Test exception handling.')
-
-    original__check_list_actions = runner._check_list_actions
-    runner._check_list_actions = raise_exception
-    try:
-        yield
-    finally:
-        runner._check_list_actions = original__check_list_actions
-
-
-class TestXMLRPCRunnerTimeout(TestCase):
-    """Make sure that we set a timeout on our xmlrpc connections."""
-
-    layer = BaseLayer
-
-    def test_timeout_used(self):
-        proxy = get_mailing_list_api_proxy()
-        # We don't want to trigger the proxy if we misspell something, so we
-        # look in the dict.
-        transport = proxy.__dict__['_ServerProxy__transport']
-        self.assertTrue(isinstance(transport, Transport))
-        self.assertEqual(mm_cfg.XMLRPC_TIMEOUT, transport.timeout)
-        # This is a bit rickety--if the mailman config was built under a
-        # different instance that has a different timeout value, this will
-        # fail.  Removing this next assertion would probably be OK then, but
-        # I think it is nice to have.
-        self.assertEqual(config.mailman.xmlrpc_timeout, mm_cfg.XMLRPC_TIMEOUT)
-
-
-class TestXMLRPCRunnerHeatBeat(MailmanTestCase):
-    """Test XMLRPCRunner._hearbeat method."""
-
-    layer = DatabaseFunctionalLayer
-
-    def setUp(self):
-        super(TestXMLRPCRunnerHeatBeat, self).setUp()
-        self.mm_list = None
-        syslog.write_ex('xmlrpc', 'Ensure the log is open.')
-        self.reset_log()
-        self.runner = XMLRPCRunner()
-        # MailmanTestCase's setup of the test proxy is ignored because
-        # the runner had a reference to the true proxy in its __init__.
-        self.runner._proxy = get_mailing_list_api_test_proxy()
-
-    def test_heartbeat_on_start(self):
-        # A heartbeat is recorded in the log on start.
-        mark = self.get_mark()
-        self.assertTrue(mark is not None)
-
-    def test_heatbeat_frequency_no_heartbeat(self):
-        # A heartbeat is not recorded when the that last beat less than
-        # the heartbeat_frequency.
-        self.runner._heartbeat()
-        self.reset_log()
-        self.runner._heartbeat()
-        now = datetime.now()
-        last_heartbeat = self.runner.last_heartbeat
-        self.assertTrue(
-            now - last_heartbeat < self.runner.heartbeat_frequency)
-        mark = self.get_mark()
-        self.assertTrue(mark is None)
-
-    def test__oneloop_success_heartbeat(self):
-        # A heartbeat is recorded when the loop completes successfully.
-        self.reset_log()
-        self.runner.last_heartbeat = (
-            self.runner.last_heartbeat - self.runner.heartbeat_frequency)
-        self.runner._oneloop()
-        mark = self.get_mark()
-        self.assertTrue(mark is not None)
-
-    def test__oneloop_exception_no_heartbeat(self):
-        # A heartbeat is not recorded when there is an exception in the loop.
-        self.reset_log()
-        self.runner.last_heartbeat = (
-            self.runner.last_heartbeat - self.runner.heartbeat_frequency)
-        # Hack runner to raise an oops.
-        with one_loop_exception(self.runner):
-            self.runner._oneloop()
-        mark = self.get_mark()
-        self.assertTrue(mark is None)
-
-
-class TestHandleProxyError(MailmanTestCase):
-    """Test XMLRPCRunner.handle_proxy_error function."""
-
-    layer = DatabaseFunctionalLayer
-
-    def setUp(self):
-        super(TestHandleProxyError, self).setUp()
-        self.team, self.mailing_list = self.factory.makeTeamAndMailingList(
-            'team-1', 'team-1-owner')
-        self.mm_list = self.makeMailmanList(self.mailing_list)
-        syslog.write_ex('xmlrpc', 'Ensure the log is open.')
-        self.reset_log()
-
-    def test_communication_log_entry(self):
-        # Connection errors are reported in the log.
-        error = socket.error('Testing socket error.')
-        handle_proxy_error(error)
-        mark = self.get_log_entry('Cannot talk to Launchpad:')
-        self.assertTrue(mark is not None)
-
-    def test_fault_log_entry(self):
-        # Fault errors are reported in the log.
-        error = Exception('Testing generic error.')
-        handle_proxy_error(error)
-        mark = self.get_log_entry('Launchpad exception:')
-        self.assertTrue(mark is not None)
-
-    def test_message_raises_discard_message_error(self):
-        # When message is passed to the function, DiscardMessage is raised
-        # and the message is re-enqueued in the incoming queue.
-        error = Exception('Testing generic error.')
-        msg = self.makeMailmanMessage(
-            self.mm_list, 'lost@xxxxxxxxxxx', 'subject', 'any content.')
-        msg_data = {}
-        self.assertRaises(
-            Errors.DiscardMessage, handle_proxy_error, error, msg, msg_data)
-        self.assertIsEnqueued(msg)
-
-
-class OopsReportingTestCase(MailmanTestCase):
-    """Test XMLRPCRunner reports oopses."""
-
-    layer = DatabaseFunctionalLayer
-
-    def setUp(self):
-        super(OopsReportingTestCase, self).setUp()
-        self.mm_list = None
-        syslog.write_ex('xmlrpc', 'Ensure the log is open.')
-        self.reset_log()
-        self.runner = XMLRPCRunner()
-        # MailmanTestCase's setup of the test proxy is ignored because
-        # the runner had a reference to the true proxy in its __init__.
-        self.runner._proxy = get_mailing_list_api_test_proxy()
-
-    def test_oops_reporting(self):
-        capture = CaptureOops()
-        capture.setUp()
-        with one_loop_exception(self.runner):
-            self.runner._oneloop()
-        oops = capture.oopses[0]
-        capture.cleanUp()
-        self.assertEqual('T-mailman', oops['reporter'])
-        self.assertTrue(oops['id'].startswith('OOPS-'))
-        self.assertEqual('Exception', oops['type'])
-        self.assertEqual('Test exception handling.', oops['value'])
-        self.assertTrue(
-            oops['tb_text'].startswith('Traceback (most recent call last):'))
-
-
-@contextmanager
-def locked_list(mm_list):
-    """Ensure a lock is not held."""
-    mm_list.Lock()
-    try:
-        yield
-    finally:
-        mm_list.Unlock()
-
-
-class OneLoopTestCase(MailmanTestCase):
-    """Test XMLRPCRunner._oneloop method.
-
-    The _oneloop() method calls all the methods used to sync Lp to Mailman.
-    """
-
-    layer = DatabaseFunctionalLayer
-
-    def setUp(self):
-        super(OneLoopTestCase, self).setUp()
-        self.mm_list = None
-        self.runner = XMLRPCRunner()
-        # MailmanTestCase's setup of the test proxy is ignored because
-        # the runner had a reference to the true proxy in its __init__.
-        self.runner._proxy = get_mailing_list_api_test_proxy()
-
-    def makeTeamList(self, team_name, owner_name, need_mm_list=True):
-        team, mailing_list = self.factory.makeTeamAndMailingList(
-            team_name, owner_name)
-        if need_mm_list:
-            self.mm_list = self.makeMailmanList(mailing_list)
-            self.mm_list.Unlock()
-        return team, mailing_list
-
-    def test_create(self):
-        # Lists are created in mailman after they are created in Lp.
-        team = self.factory.makeTeam(name='team-1')
-        # The factory cannot be used because it forces the list into a
-        # usable state.
-        mailing_list = getUtility(IMailingListSet).new(team, team.teamowner)
-        self.runner._oneloop()
-        self.assertContentEqual(
-            [mm_cfg.MAILMAN_SITE_LIST, 'team-1'], list_names())
-        mm_list = MailList.MailList('team-1')
-        self.addCleanup(self.cleanMailmanList, mm_list)
-        self.assertEqual(
-            'team-1@xxxxxxxxxxxxxxxxxxxx', mm_list.getListAddress())
-        self.assertEqual(MailingListStatus.ACTIVE, mailing_list.status)
-
-    def test_deactivate(self):
-        # Lists are deactivted in mailman after they are deactivate in Lp.
-        team, mailing_list = self.makeTeamList('team-1', 'owner-1')
-        mailing_list.deactivate()
-        self.runner._oneloop()
-        self.assertContentEqual([mm_cfg.MAILMAN_SITE_LIST], list_names())
-        backup_file = os.path.join(mm_cfg.VAR_PREFIX, 'backups', 'team-1.tgz')
-        self.assertTrue(os.path.exists(backup_file))
-        tarball = tarfile.open(backup_file, 'r:gz')
-        content = ['team-1', 'team-1/config.pck']
-        self.assertContentEqual(content, tarball.getnames())
-        self.assertEqual(MailingListStatus.INACTIVE, mailing_list.status)
-
-    def test_modify(self):
-        # Lists are modified in mailman after they are modified in Lp.
-        team, mailing_list = self.makeTeamList('team-1', 'owner-1')
-        with person_logged_in(team.teamowner):
-            mailing_list.welcome_message = 'hello'
-        self.assertEqual(MailingListStatus.MODIFIED, mailing_list.status)
-        self.runner._oneloop()
-        self.mm_list.Load()
-        self.assertEqual('hello', self.mm_list.welcome_msg)
-        self.assertEqual(MailingListStatus.ACTIVE, mailing_list.status)
-
-    def test_reactivate(self):
-        # Lists are deactivted in mailman after they are deactivate in Lp.
-        team, mailing_list = self.makeTeamList('team-1', 'owner-1')
-        mailing_list.deactivate()
-        self.runner._oneloop()
-        backup_file = os.path.join(mm_cfg.VAR_PREFIX, 'backups', 'team-1.tgz')
-        self.assertTrue(os.path.exists(backup_file))
-        mailing_list.reactivate()
-        self.runner._oneloop()
-        self.assertFalse(os.path.exists(backup_file))
-        self.assertEqual(
-            'team-1@xxxxxxxxxxxxxxxxxxxx', self.mm_list.getListAddress())
-        self.assertEqual(MailingListStatus.ACTIVE, mailing_list.status)
-
-    def test_get_subscriptions_add(self):
-        # List members are added in mailman after they are subscribed in Lp.
-        team, mailing_list = self.makeTeamList('team-1', 'owner-1')
-        lp_user_email = 'albatros@xxxxxx'
-        lp_user = self.factory.makePerson(name='albatros', email=lp_user_email)
-        with person_logged_in(lp_user):
-            # The factory person has auto join mailing list enabled.
-            lp_user.join(team)
-        self.runner._oneloop()
-        with locked_list(self.mm_list):
-            self.assertEqual(1, self.mm_list.isMember(lp_user_email))
-
-    def test_get_subscriptions_add_alternate(self):
-        # List members can have alternate addresses provided by Lp..
-        team, mailing_list = self.makeTeamList('team-1', 'owner-1')
-        lp_user_email = 'albatros@xxxxxx'
-        lp_user = self.factory.makePerson(name='albatros', email=lp_user_email)
-        alt_email = self.factory.makeEmail('bat@xxxxxx', person=lp_user)
-        with person_logged_in(lp_user):
-            lp_user.join(team)
-            mailing_list.unsubscribe(lp_user)
-            mailing_list.subscribe(lp_user, alt_email)
-        self.runner._oneloop()
-        with locked_list(self.mm_list):
-            self.assertEqual(1, self.mm_list.isMember('bat@xxxxxx'))
-
-    def test_get_subscriptions_leave_team(self):
-        # List members are removed when the leave the team.
-        team, mailing_list = self.makeTeamList('team-1', 'owner-1')
-        lp_user_email = 'albatros@xxxxxx'
-        lp_user = self.factory.makePerson(name='albatros', email=lp_user_email)
-        with person_logged_in(lp_user):
-            lp_user.join(team)
-        self.runner._oneloop()
-        with person_logged_in(lp_user):
-            lp_user.leave(team)
-        self.runner._oneloop()
-        with locked_list(self.mm_list):
-            self.assertEqual(0, self.mm_list.isMember('albatros@xxxxxx'))
-
-    def test_get_subscriptions_rejoin_team(self):
-        # Former list members are restored when they rejoin the team.
-        team, mailing_list = self.makeTeamList('team-1', 'owner-1')
-        lp_user_email = 'albatros@xxxxxx'
-        lp_user = self.factory.makePerson(name='albatros', email=lp_user_email)
-        with person_logged_in(lp_user):
-            lp_user.join(team)
-        self.runner._oneloop()
-        with person_logged_in(lp_user):
-            lp_user.leave(team)
-        self.runner._oneloop()
-        with person_logged_in(lp_user):
-            lp_user.join(team)
-        self.runner._oneloop()
-        with locked_list(self.mm_list):
-            self.assertEqual(1, self.mm_list.isMember('albatros@xxxxxx'))
-
-    def test_get_subscriptions_batching(self):
-        # get_subscriptions iterates over batches of lists.
-        config.push('batching test',
-            """
-            [mailman]
-            subscription_batch_size: 1
-            """)
-        self.addCleanup(config.pop, 'batching test')
-        team_1, mailing_list_1 = self.makeTeamList('team-1', 'owner-1')
-        mm_list_1 = self.mm_list
-        team_2, mailing_list_2 = self.makeTeamList('team-2', 'owner-2')
-        mm_list_2 = self.mm_list
-        self.addCleanup(self.cleanMailmanList, mm_list_1)
-        lp_user_email = 'albatros@xxxxxx'
-        lp_user = self.factory.makePerson(name='albatros', email=lp_user_email)
-        with person_logged_in(lp_user):
-            # The factory person has auto join mailing list enabled.
-            lp_user.join(team_1)
-            lp_user.join(team_2)
-        self.runner._oneloop()
-        with locked_list(mm_list_1):
-            self.assertEqual(1, mm_list_1.isMember(lp_user_email))
-        with locked_list(mm_list_2):
-            self.assertEqual(1, mm_list_2.isMember(lp_user_email))
-
-    def test_get_subscriptions_shortcircut(self):
-        # The method exist earlty without completing the update when
-        # the runner is stopping.
-        team, mailing_list = self.makeTeamList('team-1', 'owner-1')
-        lp_user_email = 'albatros@xxxxxx'
-        lp_user = self.factory.makePerson(name='albatros', email=lp_user_email)
-        with person_logged_in(lp_user):
-            # The factory person has auto join mailing list enabled.
-            lp_user.join(team)
-        self.runner.stop()
-        self.runner._get_subscriptions()
-        with locked_list(self.mm_list):
-            self.assertEqual(0, self.mm_list.isMember(lp_user_email))
-
-    def test_constructing_to_active_recovery(self):
-        # Lp is informed of the active list if it wrongly believes it is
-        # being constructed.
-        team = self.factory.makeTeam(name='team-1')
-        mailing_list = getUtility(IMailingListSet).new(team, team.teamowner)
-        self.addCleanup(self.cleanMailmanList, None, 'team-1')
-        self.runner._oneloop()
-        removeSecurityProxy(mailing_list).status = (
-            MailingListStatus.CONSTRUCTING)
-        self.runner._oneloop()
-        self.assertEqual(MailingListStatus.ACTIVE, mailing_list.status)
-
-    def test_nonexistent_to_active_recovery(self):
-        # Mailman will build the list if Lp thinks it is exists in the
-        # CONSTRUCTING state
-        team = self.factory.makeTeam(name='team-1')
-        mailing_list = getUtility(IMailingListSet).new(team, team.teamowner)
-        removeSecurityProxy(mailing_list).status = (
-            MailingListStatus.CONSTRUCTING)
-        self.runner._oneloop()
-        self.assertContentEqual(
-            [mm_cfg.MAILMAN_SITE_LIST, 'team-1'], list_names())
-        mm_list = MailList.MailList('team-1')
-        self.addCleanup(self.cleanMailmanList, mm_list)
-        self.assertEqual(
-            'team-1@xxxxxxxxxxxxxxxxxxxx', mm_list.getListAddress())
-        self.assertEqual(MailingListStatus.ACTIVE, mailing_list.status)
-
-    def test_updating_to_active_recovery(self):
-        # Lp is informed of the active list if it wrongly believes it is
-        # being updated.
-        team = self.factory.makeTeam(name='team-1')
-        mailing_list = getUtility(IMailingListSet).new(team, team.teamowner)
-        self.addCleanup(self.cleanMailmanList, None, 'team-1')
-        self.runner._oneloop()
-        removeSecurityProxy(mailing_list).status = (
-            MailingListStatus.UPDATING)
-        self.runner._oneloop()
-        self.assertEqual(MailingListStatus.ACTIVE, mailing_list.status)
-
-    def test_shortcircuit(self):
-        # Oneloop will exit early if the runner is stopping.
-        class State:
-
-            def __init__(self):
-                self.checked = None
-
-        shortcircut = State()
-
-        def fake_called():
-            shortcircut.checked = False
-
-        def fake_stop():
-            shortcircut.checked = True
-            self.runner.stop()
-
-        with monkey_patch(self.runner,
-                _check_list_actions=fake_stop, _get_subscriptions=fake_called):
-            self.runner._oneloop()
-            self.assertTrue(self.runner._shortcircuit())
-            self.assertTrue(shortcircut.checked)
diff --git a/lib/lp/testing/layers.py b/lib/lp/testing/layers.py
index 5c7950a..c12718f 100644
--- a/lib/lp/testing/layers.py
+++ b/lib/lp/testing/layers.py
@@ -156,7 +156,6 @@ from lp.testing import (
     reset_logging,
     )
 from lp.testing.pgsql import PgTestSetup
-from lp.testing.smtpd import SMTPController
 import zcml
 
 
@@ -1759,9 +1758,8 @@ class TwistedLaunchpadZopelessLayer(TwistedLayer, LaunchpadZopelessLayer):
 class LayerProcessController:
     """Controller for starting and stopping subprocesses.
 
-    Layers which need to start and stop a child process appserver or smtp
-    server should call the methods in this class, but should NOT inherit from
-    this class.
+    Layers which need to start and stop a child process appserver should
+    call the methods in this class, but should NOT inherit from this class.
     """
 
     # Holds the Popen instance of the spawned app server.
@@ -1770,10 +1768,6 @@ class LayerProcessController:
     # The config used by the spawned app server.
     appserver_config = None
 
-    # The SMTP server for layer tests.  See
-    # configs/testrunner-appserver/mail-configure.zcml
-    smtp_controller = None
-
     @classmethod
     def setConfig(cls):
         """Stash a config for use."""
@@ -1783,31 +1777,10 @@ class LayerProcessController:
     @classmethod
     def setUp(cls):
         cls.setConfig()
-        cls.startSMTPServer()
         cls.startAppServer()
 
     @classmethod
     @profiled
-    def startSMTPServer(cls):
-        """Start the SMTP server if it hasn't already been started."""
-        if cls.smtp_controller is not None:
-            raise LayerInvariantError('SMTP server already running')
-        # Ensure that the SMTP server does proper logging.
-        log = logging.getLogger('lazr.smtptest')
-        log_file = os.path.join(config.mailman.build_var_dir, 'logs', 'smtpd')
-        handler = logging.FileHandler(log_file)
-        formatter = logging.Formatter(
-            fmt='%(asctime)s (%(process)d) %(message)s',
-            datefmt='%b %d %H:%M:%S %Y')
-        handler.setFormatter(formatter)
-        log.setLevel(logging.DEBUG)
-        log.addHandler(handler)
-        log.propagate = False
-        cls.smtp_controller = SMTPController('localhost', 9025)
-        cls.smtp_controller.start()
-
-    @classmethod
-    @profiled
     def startAppServer(cls, run_name='run'):
         """Start the app server if it hasn't already been started."""
         if cls.appserver is not None:
@@ -1817,15 +1790,6 @@ class LayerProcessController:
         cls._waitUntilAppServerIsReady()
 
     @classmethod
-    @profiled
-    def stopSMTPServer(cls):
-        """Kill the SMTP server and wait until it's exited."""
-        if cls.smtp_controller is not None:
-            cls.smtp_controller.reset()
-            cls.smtp_controller.stop()
-            cls.smtp_controller = None
-
-    @classmethod
     def _kill(cls, sig):
         """Kill the appserver with `sig`.
 
@@ -1966,7 +1930,6 @@ class AppServerLayer(LaunchpadFunctionalLayer):
     @profiled
     def tearDown(cls):
         LayerProcessController.stopAppServer()
-        LayerProcessController.stopSMTPServer()
 
     @classmethod
     @profiled
@@ -2046,7 +2009,6 @@ class ZopelessAppServerLayer(LaunchpadZopelessLayer):
     @profiled
     def tearDown(cls):
         LayerProcessController.stopAppServer()
-        LayerProcessController.stopSMTPServer()
 
     @classmethod
     @profiled
@@ -2072,7 +2034,6 @@ class TwistedAppServerLayer(TwistedLaunchpadZopelessLayer):
     @profiled
     def tearDown(cls):
         LayerProcessController.stopAppServer()
-        LayerProcessController.stopSMTPServer()
 
     @classmethod
     @profiled
diff --git a/lib/lp/testing/smtpd.py b/lib/lp/testing/smtpd.py
deleted file mode 100644
index 79cbd86..0000000
--- a/lib/lp/testing/smtpd.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""SMTP test helper."""
-
-
-__metaclass__ = type
-__all__ = [
-    'SMTPController',
-    ]
-
-
-import fcntl
-import logging
-import Queue as queue
-
-from lazr.smtptest.controller import QueueController
-from lazr.smtptest.server import QueueServer
-
-
-log = logging.getLogger('lazr.smtptest')
-
-
-class SMTPServer(QueueServer):
-    """SMTP server which knows about Launchpad test specifics."""
-
-    def handle_message(self, message):
-        """See `QueueServer.handle_message()`."""
-        message_id = message.get('message-id', 'n/a')
-        log.debug('msgid: %s, to: %s, beenthere: %s, from: %s, rcpt: %s',
-                  message_id, message['to'],
-                  message['x-beenthere'],
-                  message['x-mailfrom'], message['x-rcptto'])
-        self.queue.put(message)
-
-    def reset(self):
-        # Base class is old-style.
-        QueueServer.reset(self)
-        # Consume everything out of the queue.
-        while True:
-            try:
-                self.queue.get_nowait()
-            except queue.Empty:
-                break
-
-
-class SMTPController(QueueController):
-    """A controller for the `SMTPServer`."""
-
-    def _make_server(self, host, port):
-        """See `QueueController`."""
-        self.server = SMTPServer(host, port, self.queue)
-        # Set FD_CLOEXEC on the port's file descriptor, so that forked
-        # processes like uuidd won't steal the port.
-        flags = fcntl.fcntl(self.server._fileno, fcntl.F_GETFD)
-        flags |= fcntl.FD_CLOEXEC
-        fcntl.fcntl(self.server._fileno, fcntl.F_SETFD, flags)
diff --git a/lib/lp/testing/tests/test_layers_functional.py b/lib/lp/testing/tests/test_layers_functional.py
index 1dbfeda..bc1b103 100644
--- a/lib/lp/testing/tests/test_layers_functional.py
+++ b/lib/lp/testing/tests/test_layers_functional.py
@@ -14,7 +14,6 @@ __metaclass__ = type
 from cStringIO import StringIO
 import os
 import signal
-import smtplib
 import uuid
 
 import amqp
@@ -23,7 +22,6 @@ from fixtures import (
     Fixture,
     TestWithFixtures,
     )
-from lazr.config import as_host_port
 from six.moves.urllib.error import HTTPError
 from six.moves.urllib.request import urlopen
 from zope.component import (
@@ -492,23 +490,11 @@ class LayerProcessControllerInvariantsTestCase(BaseTestCase):
             'Is your project registered yet?' in home_page,
             "Home page couldn't be retrieved:\n%s" % home_page)
 
-    def testSMTPServerIsAvailable(self):
-        # Test that the SMTP server is up and running.
-        smtpd = smtplib.SMTP()
-        host, port = as_host_port(config.mailman.smtp)
-        code, message = smtpd.connect(host, port)
-        self.assertEqual(code, 220)
-
     def testStartingAppServerTwiceRaisesInvariantError(self):
         # Starting the appserver twice should raise an exception.
         self.assertRaises(LayerInvariantError,
                           LayerProcessController.startAppServer)
 
-    def testStartingSMTPServerTwiceRaisesInvariantError(self):
-        # Starting the SMTP server twice should raise an exception.
-        self.assertRaises(LayerInvariantError,
-                          LayerProcessController.startSMTPServer)
-
 
 class LayerProcessControllerTestCase(TestCase):
     """Tests for the `LayerProcessController`."""
@@ -517,8 +503,7 @@ class LayerProcessControllerTestCase(TestCase):
 
     def tearDown(self):
         super(LayerProcessControllerTestCase, self).tearDown()
-        # Stop both servers.  It's okay if they aren't running.
-        LayerProcessController.stopSMTPServer()
+        # Stop the app server.  It's okay if it isn't running.
         LayerProcessController.stopAppServer()
 
     def test_stopAppServer(self):
diff --git a/scripts/mlist-sync.py b/scripts/mlist-sync.py
deleted file mode 100755
index 503ba57..0000000
--- a/scripts/mlist-sync.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/python -S
-
-# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Sync Mailman data from one Launchpad to another."""
-
-import _pythonpath
-
-import sys
-
-from lp.services.mailman.scripts.mlist_sync import MailingListSyncScript
-
-
-if __name__ == '__main__':
-    script = MailingListSyncScript('mlist-sync', dbuser='mlist-sync')
-    status = script.lock_and_run()
-    sys.exit(status)
diff --git a/setup.py b/setup.py
index ef792da..84f146a 100644
--- a/setup.py
+++ b/setup.py
@@ -182,7 +182,6 @@ setup(
         'lazr.jobrunner',
         'lazr.lifecycle',
         'lazr.restful',
-        'lazr.smtptest',
         'lazr.sshserver',
         'lazr.uri',
         'lpjsmin',
diff --git a/utilities/sourcedeps.cache b/utilities/sourcedeps.cache
index 44f8cfd..412ea52 100644
--- a/utilities/sourcedeps.cache
+++ b/utilities/sourcedeps.cache
@@ -27,10 +27,6 @@
         494,
         "cjwatson@xxxxxxxxxxxxx-20190919081036-q1symc2h2iedtlh3"
     ],
-    "mailman": [
-        977,
-        "launchpad@xxxxxxxxxxxxxxxxx-20130405041235-9ud0xancja2eefd7"
-    ],
     "old_xmlplus": [
         4,
         "sinzui-20090526164636-1swugzupwvjgomo4"
diff --git a/utilities/sourcedeps.conf b/utilities/sourcedeps.conf
index f21c98f..b19007d 100644
--- a/utilities/sourcedeps.conf
+++ b/utilities/sourcedeps.conf
@@ -14,6 +14,5 @@ bzr-svn lp:~launchpad-pqm/bzr-svn/devel;revno=2725
 cscvs lp:~launchpad-pqm/launchpad-cscvs/devel;revno=433
 difftacular lp:~launchpad/difftacular/trunk;revno=11
 loggerhead lp:~loggerhead-team/loggerhead/trunk-rich;revno=494
-mailman lp:~launchpad-pqm/mailman/2.1;revno=977
 old_xmlplus lp:~launchpad-pqm/dtdparser/trunk;revno=4
 pygettextpo lp:~launchpad-pqm/pygettextpo/trunk;revno=25