launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24340
[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$">←
- 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>
- •
- <a href="https://launchpad.net/+tour">Take the tour</a>
- •
- <a href="https://help.launchpad.net/">Read the guide</a>
- •
- <a href="https://help.launchpad.net/Teams/MailingLists"
- >Help for mailing lists</a>
-
- <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">
- © 2004-2020
- <a href="http://canonical.com/">Canonical Ltd.</a>
- •
- <a href="https://launchpad.net/legal">Terms of use</a>
- •
- <a href="https://www.ubuntu.com/legal/dataprivacy">Data privacy</a>
- •
- <a href="https://launchpad.net/support">Contact Launchpad Support</a>
- •
- <a href="http://blog.launchpad.net/">Blog</a>
- •
- <a href="http://www.canonical.com/about-canonical/careers">Careers</a>
- •
- <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> • <a href="$PG(PREV)$">Next</a>
-</PREVPGLINK>
-
-<PREVPGLINKIA>
-<span class="inactive">Last • Next</span>
-</PREVPGLINKIA>
-
-<TPREVPGLINK>
-<a href="$PG(TFIRST)$">Last</a> • <a href="$PG(TPREV)$">Next</a>
-</TPREVPGLINK>
-
-<TPREVPGLINKIA>
-<span class="inactive">Last • Next</span>
-</TPREVPGLINKIA>
-
-<NEXTPGLINK>
-<a class="next" href="$PG(NEXT)$">Previous</a>
- • <a href="$PG(LAST)$">First</a>
-</NEXTPGLINK>
-
-<NEXTPGLINKIA>
-<span class="next inactive">Previous</span>
- • <span class="inactive">First</span>
-</NEXTPGLINKIA>
-
-<TNEXTPGLINK>
-<a class="next" href="$PG(TNEXT)$">Previous</a>
- • <a href="$PG(TLAST)$">First</a>
-</TNEXTPGLINK>
-
-<TNEXTPGLINKIA>
-<span class="next inactive">Previous</span>
- •<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)$ • $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)$ • $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)$ • $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)$ • $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>
-
- </td>
- <td class="batch-navigation-links">
- <a href="$MSG(TPREV)$">Thread Previous</a> •
- <a href="$MSG(PREV)$">Date Previous</a> •
- <a class="next" href="$MSG(NEXT)$">Date Next</a> •
- <a href="$MSG(TNEXT)$">Thread Next</a>
- </td>
- </tr>
-</table>
-</TopLinks>
-
-<BotLinks>
-<table class="wide lower-batch-nav">
- <tr>
- <td>
-
- </td>
- <td class="batch-navigation-links">
- <a href="$MSG(TPREV)$">Thread Previous</a> •
- <a href="$MSG(PREV)$">Date Previous</a> •
- <a class="next" href="$MSG(NEXT)$">Date Next</a> •
- <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