summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.bzrignore1
-rw-r--r--src/mailman/app/docs/bounces.rst2
-rw-r--r--src/mailman/archiving/mailarchive.py1
-rw-r--r--src/mailman/archiving/mhonarc.py1
-rw-r--r--src/mailman/archiving/prototype.py3
-rw-r--r--src/mailman/commands/tests/test_conf.py7
-rw-r--r--src/mailman/config/config.py10
-rw-r--r--src/mailman/config/configure.zcml6
-rw-r--r--src/mailman/config/exim4.cfg2
-rw-r--r--src/mailman/config/schema.cfg2
-rw-r--r--src/mailman/config/tests/test_archivers.py56
-rw-r--r--src/mailman/core/logging.py3
-rw-r--r--src/mailman/database/base.py11
-rw-r--r--src/mailman/database/docs/migration.rst54
-rw-r--r--src/mailman/database/factory.py17
-rw-r--r--src/mailman/database/schema/helpers.py43
-rw-r--r--src/mailman/database/schema/mm_20120407000000.py12
-rw-r--r--src/mailman/database/schema/mm_20121015000000.py30
-rw-r--r--src/mailman/database/schema/mm_20130406000000.py65
-rw-r--r--src/mailman/database/schema/sqlite_20120407000000_01.sql30
-rw-r--r--src/mailman/database/schema/sqlite_20121015000000_01.sql12
-rw-r--r--src/mailman/database/schema/sqlite_20130406000000_01.sql46
-rw-r--r--src/mailman/database/tests/test_migrations.py78
-rw-r--r--src/mailman/docs/MTA.rst51
-rw-r--r--src/mailman/docs/NEWS.rst14
-rw-r--r--src/mailman/handlers/rfc_2369.py11
-rw-r--r--src/mailman/interfaces/archiver.py2
-rw-r--r--src/mailman/interfaces/bounce.py4
-rw-r--r--src/mailman/interfaces/listmanager.py8
-rw-r--r--src/mailman/interfaces/mailinglist.py36
-rw-r--r--src/mailman/model/bounce.py8
-rw-r--r--src/mailman/model/docs/bounce.rst4
-rw-r--r--src/mailman/model/mailinglist.py70
-rw-r--r--src/mailman/model/tests/test_bounce.py4
-rw-r--r--src/mailman/model/tests/test_domain.py2
-rw-r--r--src/mailman/model/tests/test_listmanager.py5
-rw-r--r--src/mailman/model/tests/test_mailinglist.py106
-rw-r--r--src/mailman/mta/exim4.py11
-rw-r--r--src/mailman/rest/docs/lists.rst58
-rw-r--r--src/mailman/rest/lists.py69
-rw-r--r--src/mailman/rest/tests/test_lists.py69
-rw-r--r--src/mailman/runners/archive.py10
-rw-r--r--src/mailman/runners/tests/test_archiver.py15
-rw-r--r--src/mailman/runners/tests/test_bounce.py6
-rw-r--r--src/mailman/runners/tests/test_outgoing.py2
-rw-r--r--src/mailman/testing/helpers.py4
-rw-r--r--src/mailman/testing/layers.py33
-rw-r--r--src/mailman/testing/nose.py19
-rw-r--r--src/mailman/utilities/importer.py13
-rw-r--r--src/mailman/utilities/tests/test_import.py55
50 files changed, 991 insertions, 190 deletions
diff --git a/.bzrignore b/.bzrignore
index c83f1c227..4e902afc2 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -19,3 +19,4 @@ eggs
diff.txt
distribute-*.egg
distribute-*.tar.gz
+.coverage
diff --git a/src/mailman/app/docs/bounces.rst b/src/mailman/app/docs/bounces.rst
index 5510f2207..31d2e51d2 100644
--- a/src/mailman/app/docs/bounces.rst
+++ b/src/mailman/app/docs/bounces.rst
@@ -2,7 +2,7 @@
Bounces
=======
-An important feature of Mailman is automatic bounce process.
+An important feature of Mailman is automatic bounce processing.
Bounces, or message rejection
diff --git a/src/mailman/archiving/mailarchive.py b/src/mailman/archiving/mailarchive.py
index 34a10fd25..a8489d02e 100644
--- a/src/mailman/archiving/mailarchive.py
+++ b/src/mailman/archiving/mailarchive.py
@@ -43,6 +43,7 @@ class MailArchive:
"""
name = 'mail-archive'
+ is_enabled = False
def __init__(self):
# Read our specific configuration file
diff --git a/src/mailman/archiving/mhonarc.py b/src/mailman/archiving/mhonarc.py
index 6f8f3e168..646030f5e 100644
--- a/src/mailman/archiving/mhonarc.py
+++ b/src/mailman/archiving/mhonarc.py
@@ -46,6 +46,7 @@ class MHonArc:
"""Local MHonArc archiver."""
name = 'mhonarc'
+ is_enabled = False
def __init__(self):
# Read our specific configuration file
diff --git a/src/mailman/archiving/prototype.py b/src/mailman/archiving/prototype.py
index df215a0da..356fea1bd 100644
--- a/src/mailman/archiving/prototype.py
+++ b/src/mailman/archiving/prototype.py
@@ -52,6 +52,7 @@ class Prototype:
"""
name = 'prototype'
+ is_enabled = False
@staticmethod
def list_url(mlist):
@@ -77,7 +78,7 @@ class Prototype:
"""
archive_dir = os.path.join(config.ARCHIVE_DIR, 'prototype')
try:
- os.makedirs(archive_dir, 0775)
+ os.makedirs(archive_dir, 0o775)
except OSError as error:
# If this already exists, then we're fine
if error.errno != errno.EEXIST:
diff --git a/src/mailman/commands/tests/test_conf.py b/src/mailman/commands/tests/test_conf.py
index 307151c74..04ce4c9b5 100644
--- a/src/mailman/commands/tests/test_conf.py
+++ b/src/mailman/commands/tests/test_conf.py
@@ -110,4 +110,9 @@ class TestConf(unittest.TestCase):
self.command.process(self.args)
last_line = ''
for line in output.getvalue().splitlines():
- self.assertTrue(line > last_line)
+ if not line.startswith('['):
+ # This is a continuation line. --sort doesn't sort these.
+ continue
+ self.assertTrue(line > last_line,
+ '{} !> {}'.format(line, last_line))
+ last_line = line
diff --git a/src/mailman/config/config.py b/src/mailman/config/config.py
index 74931c029..86919e3f1 100644
--- a/src/mailman/config/config.py
+++ b/src/mailman/config/config.py
@@ -249,12 +249,14 @@ class Configuration:
@property
def archivers(self):
- """Iterate over all the enabled archivers."""
+ """Iterate over all the archivers."""
for section in self._config.getByCategory('archiver', []):
- if not as_boolean(section.enable):
+ class_path = section['class'].strip()
+ if len(class_path) == 0:
continue
- class_path = section['class']
- yield call_name(class_path)
+ archiver = call_name(class_path)
+ archiver.is_enabled = as_boolean(section.enable)
+ yield archiver
@property
def style_configs(self):
diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml
index efb449538..f9b9cb093 100644
--- a/src/mailman/config/configure.zcml
+++ b/src/mailman/config/configure.zcml
@@ -30,6 +30,12 @@
<adapter
for="mailman.interfaces.mailinglist.IMailingList"
+ provides="mailman.interfaces.mailinglist.IListArchiverSet"
+ factory="mailman.model.mailinglist.ListArchiverSet"
+ />
+
+ <adapter
+ for="mailman.interfaces.mailinglist.IMailingList"
provides="mailman.interfaces.requests.IListRequests"
factory="mailman.model.requests.ListRequests"
/>
diff --git a/src/mailman/config/exim4.cfg b/src/mailman/config/exim4.cfg
index e7a5187f0..e614ad182 100644
--- a/src/mailman/config/exim4.cfg
+++ b/src/mailman/config/exim4.cfg
@@ -1,4 +1,4 @@
-# Additional configuration variables for the Exim MTA, version 4.
[exim4]
+# Additional configuration variables for the Exim MTA, version 4.
# Exim doesn't need any additional configuration yet.
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index f132b0358..33fd99cb3 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -532,7 +532,7 @@ register_bounces_every: 15m
# following values.
# The class implementing the IArchiver interface.
-class: mailman.archiving.prototype.Prototype
+class:
# Set this to 'yes' to enable the archiver.
enable: no
diff --git a/src/mailman/config/tests/test_archivers.py b/src/mailman/config/tests/test_archivers.py
new file mode 100644
index 000000000..cd84714d5
--- /dev/null
+++ b/src/mailman/config/tests/test_archivers.py
@@ -0,0 +1,56 @@
+# Copyright (C) 2013 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Site-wide archiver configuration tests."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestArchivers',
+ ]
+
+
+import unittest
+
+from mailman.config import config
+from mailman.testing.helpers import configuration
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestArchivers(unittest.TestCase):
+ layer = ConfigLayer
+
+ def test_enabled(self):
+ # By default, the testing configuration enables the archivers.
+ archivers = {}
+ for archiver in config.archivers:
+ archivers[archiver.name] = archiver
+ self.assertTrue(archivers['prototype'].is_enabled)
+ self.assertTrue(archivers['mail-archive'].is_enabled)
+ self.assertTrue(archivers['mhonarc'].is_enabled)
+
+ @configuration('archiver.mhonarc', enable='no')
+ def test_disabled(self):
+ # We just disabled one of the archivers.
+ archivers = {}
+ for archiver in config.archivers:
+ archivers[archiver.name] = archiver
+ self.assertTrue(archivers['prototype'].is_enabled)
+ self.assertTrue(archivers['mail-archive'].is_enabled)
+ self.assertFalse(archivers['mhonarc'].is_enabled)
diff --git a/src/mailman/core/logging.py b/src/mailman/core/logging.py
index 7554c3651..c80535fc1 100644
--- a/src/mailman/core/logging.py
+++ b/src/mailman/core/logging.py
@@ -117,8 +117,7 @@ def initialize(propagate=None):
# sublogs. The root logger should log to stderr.
logging.basicConfig(format=config.logging.root.format,
datefmt=config.logging.root.datefmt,
- level=as_log_level(config.logging.root.level),
- stream=sys.stderr)
+ level=as_log_level(config.logging.root.level))
# Create the sub-loggers. Note that we'll redirect flufl.lock to
# mailman.locks.
for logger_config in config.logger_configs:
diff --git a/src/mailman/database/base.py b/src/mailman/database/base.py
index 98e30de7c..0b23c8adc 100644
--- a/src/mailman/database/base.py
+++ b/src/mailman/database/base.py
@@ -27,7 +27,6 @@ import os
import sys
import logging
-from flufl.lock import Lock
from lazr.config import as_boolean
from pkg_resources import resource_listdir, resource_string
from storm.cache import GenerationalCache
@@ -60,13 +59,6 @@ class StormBaseDatabase:
self.url = None
self.store = None
- def initialize(self, debug=None):
- """See `IDatabase`."""
- # Serialize this so we don't get multiple processes trying to create
- # the database at the same time.
- with Lock(os.path.join(config.LOCK_DIR, 'dbcreate.lck')):
- self._create(debug)
-
def begin(self):
"""See `IDatabase`."""
# Storm takes care of this for us.
@@ -117,7 +109,8 @@ class StormBaseDatabase:
"""
pass
- def _create(self, debug):
+ def initialize(self, debug=None):
+ """See `IDatabase`."""
# Calculate the engine url.
url = expand(config.database.url, config.paths)
log.debug('Database url: %s', url)
diff --git a/src/mailman/database/docs/migration.rst b/src/mailman/database/docs/migration.rst
index 2988579d3..de9c41999 100644
--- a/src/mailman/database/docs/migration.rst
+++ b/src/mailman/database/docs/migration.rst
@@ -30,13 +30,14 @@ already applied.
>>> from mailman.model.version import Version
>>> results = config.db.store.find(Version, component='schema')
>>> results.count()
- 3
+ 4
>>> versions = sorted(result.version for result in results)
>>> for version in versions:
... print version
00000000000000
20120407000000
20121015000000
+ 20130406000000
Migrations
@@ -79,7 +80,7 @@ This migration module just adds a marker to the `version` table.
>>> with open(os.path.join(path, '__init__.py'), 'w') as fp:
... pass
- >>> with open(os.path.join(path, 'mm_20129999000000.py'), 'w') as fp:
+ >>> with open(os.path.join(path, 'mm_20159999000000.py'), 'w') as fp:
... print >> fp, """
... from __future__ import unicode_literals
... from mailman.model.version import Version
@@ -98,14 +99,15 @@ This will load the new migration, since it hasn't been loaded before.
00000000000000
20120407000000
20121015000000
- 20129999000000
+ 20130406000000
+ 20159999000000
>>> test = config.db.store.find(Version, component='test').one()
>>> print test.version
- 20129999000000
+ 20159999000000
Migrations will only be loaded once.
- >>> with open(os.path.join(path, 'mm_20129999000001.py'), 'w') as fp:
+ >>> with open(os.path.join(path, 'mm_20159999000001.py'), 'w') as fp:
... print >> fp, """
... from __future__ import unicode_literals
... from mailman.model.version import Version
@@ -129,13 +131,14 @@ The first time we load this new migration, we'll get the 801 marker.
00000000000000
20120407000000
20121015000000
- 20129999000000
- 20129999000001
+ 20130406000000
+ 20159999000000
+ 20159999000001
>>> test = config.db.store.find(Version, component='test')
>>> for marker in sorted(marker.version for marker in test):
... print marker
00000000000801
- 20129999000000
+ 20159999000000
We do not get an 802 marker because the migration has already been loaded.
@@ -146,13 +149,14 @@ We do not get an 802 marker because the migration has already been loaded.
00000000000000
20120407000000
20121015000000
- 20129999000000
- 20129999000001
+ 20130406000000
+ 20159999000000
+ 20159999000001
>>> test = config.db.store.find(Version, component='test')
>>> for marker in sorted(marker.version for marker in test):
... print marker
00000000000801
- 20129999000000
+ 20159999000000
Partial upgrades
@@ -165,13 +169,13 @@ additional migrations, intended to be applied in sequential order.
>>> from shutil import copyfile
>>> from mailman.testing.helpers import chdir
>>> with chdir(path):
- ... copyfile('mm_20129999000000.py', 'mm_20129999000002.py')
- ... copyfile('mm_20129999000000.py', 'mm_20129999000003.py')
- ... copyfile('mm_20129999000000.py', 'mm_20129999000004.py')
+ ... copyfile('mm_20159999000000.py', 'mm_20159999000002.py')
+ ... copyfile('mm_20159999000000.py', 'mm_20159999000003.py')
+ ... copyfile('mm_20159999000000.py', 'mm_20159999000004.py')
Now, only migrate to the ...03 timestamp.
- >>> config.db.load_migrations('20129999000003')
+ >>> config.db.load_migrations('20159999000003')
You'll notice that the ...04 version is not present.
@@ -181,10 +185,11 @@ You'll notice that the ...04 version is not present.
00000000000000
20120407000000
20121015000000
- 20129999000000
- 20129999000001
- 20129999000002
- 20129999000003
+ 20130406000000
+ 20159999000000
+ 20159999000001
+ 20159999000002
+ 20159999000003
.. cleanup:
@@ -194,8 +199,9 @@ You'll notice that the ...04 version is not present.
this will cause migration.rst to fail on subsequent runs. So let's just
clean up the database explicitly.
- >>> results = config.db.store.execute("""
- ... DELETE FROM version WHERE version.version >= '201299990000'
- ... OR version.component = 'test';
- ... """)
- >>> config.db.commit()
+ >>> if config.db.TAG != 'sqlite':
+ ... results = config.db.store.execute("""
+ ... DELETE FROM version WHERE version.version >= '201299990000'
+ ... OR version.component = 'test';
+ ... """)
+ ... config.db.commit()
diff --git a/src/mailman/database/factory.py b/src/mailman/database/factory.py
index bf4d0df7a..f02354c11 100644
--- a/src/mailman/database/factory.py
+++ b/src/mailman/database/factory.py
@@ -27,8 +27,10 @@ __all__ = [
]
+import os
import types
+from flufl.lock import Lock
from zope.component import getAdapter
from zope.interface import implementer
from zope.interface.verify import verifyObject
@@ -47,13 +49,14 @@ class DatabaseFactory:
@staticmethod
def create():
"""See `IDatabaseFactory`."""
- database_class = config.database['class']
- database = call_name(database_class)
- verifyObject(IDatabase, database)
- database.initialize()
- database.load_migrations()
- database.commit()
- return database
+ with Lock(os.path.join(config.LOCK_DIR, 'dbcreate.lck')):
+ database_class = config.database['class']
+ database = call_name(database_class)
+ verifyObject(IDatabase, database)
+ database.initialize()
+ database.load_migrations()
+ database.commit()
+ return database
diff --git a/src/mailman/database/schema/helpers.py b/src/mailman/database/schema/helpers.py
new file mode 100644
index 000000000..c8638a12a
--- /dev/null
+++ b/src/mailman/database/schema/helpers.py
@@ -0,0 +1,43 @@
+# Copyright (C) 2013 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Schema migration helpers."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'make_listid',
+ ]
+
+
+
+def make_listid(fqdn_listname):
+ """Turn a FQDN list name into a List-ID."""
+ list_name, at, mail_host = fqdn_listname.partition('@')
+ if at == '':
+ # If there is no @ sign in the value, assume it already contains the
+ # list-id.
+ return fqdn_listname
+ return '{0}.{1}'.format(list_name, mail_host)
+
+
+
+def pivot(store, table_name):
+ """Pivot a backup table into the real table name."""
+ store.execute('DROP TABLE {}'.format(table_name))
+ store.execute('ALTER TABLE {0}_backup RENAME TO {0}'.format(table_name))
diff --git a/src/mailman/database/schema/mm_20120407000000.py b/src/mailman/database/schema/mm_20120407000000.py
index 0b1e51386..c1045fa91 100644
--- a/src/mailman/database/schema/mm_20120407000000.py
+++ b/src/mailman/database/schema/mm_20120407000000.py
@@ -48,11 +48,11 @@ __all__ = [
]
+from mailman.database.schema.helpers import pivot
from mailman.interfaces.archiver import ArchivePolicy
VERSION = '20120407000000'
-_helper = None
@@ -98,7 +98,7 @@ def upgrade_sqlite(database, store, version, module_path):
list_id = '{0}.{1}'.format(list_name, mail_host)
fqdn_listname = '{0}@{1}'.format(list_name, mail_host)
store.execute("""
- UPDATE ml_backup SET
+ UPDATE mailinglist_backup SET
allow_list_posts = {0},
newsgroup_moderation = {1},
nntp_prefix_subject_too = {2},
@@ -120,8 +120,7 @@ def upgrade_sqlite(database, store, version, module_path):
WHERE mailing_list = '{1}';
""".format(list_id, fqdn_listname))
# Pivot the backup table to the real thing.
- store.execute('DROP TABLE mailinglist;')
- store.execute('ALTER TABLE ml_backup RENAME TO mailinglist;')
+ pivot(store, 'mailinglist')
# Now add some indexes that were previously missing.
store.execute(
'CREATE INDEX ix_mailinglist_list_id ON mailinglist (list_id);')
@@ -137,12 +136,11 @@ def upgrade_sqlite(database, store, version, module_path):
else:
list_id = '{0}.{1}'.format(list_name, mail_host)
store.execute("""
- UPDATE mem_backup SET list_id = '{0}'
+ UPDATE member_backup SET list_id = '{0}'
WHERE id = {1};
""".format(list_id, id))
# Pivot the backup table to the real thing.
- store.execute('DROP TABLE member;')
- store.execute('ALTER TABLE mem_backup RENAME TO member;')
+ pivot(store, 'member')
diff --git a/src/mailman/database/schema/mm_20121015000000.py b/src/mailman/database/schema/mm_20121015000000.py
index 09078901d..da3671998 100644
--- a/src/mailman/database/schema/mm_20121015000000.py
+++ b/src/mailman/database/schema/mm_20121015000000.py
@@ -33,6 +33,9 @@ __all__ = [
]
+from mailman.database.schema.helpers import make_listid, pivot
+
+
VERSION = '20121015000000'
@@ -45,19 +48,9 @@ def upgrade(database, store, version, module_path):
-def _make_listid(fqdn_listname):
- list_name, at, mail_host = fqdn_listname.partition('@')
- if at == '':
- # If there is no @ sign in the value, assume it already contains the
- # list-id.
- return fqdn_listname
- return '{0}.{1}'.format(list_name, mail_host)
-
-
-
def upgrade_sqlite(database, store, version, module_path):
database.load_schema(
- store, version, 'sqlite_{0}_01.sql'.format(version), module_path)
+ store, version, 'sqlite_{}_01.sql'.format(version), module_path)
results = store.execute("""
SELECT id, mailing_list
FROM ban;
@@ -67,15 +60,12 @@ def upgrade_sqlite(database, store, version, module_path):
if mailing_list is None:
continue
store.execute("""
- UPDATE ban_backup SET list_id = '{0}'
- WHERE id = {1};
- """.format(_make_listid(mailing_list), id))
+ UPDATE ban_backup SET list_id = '{}'
+ WHERE id = {};
+ """.format(make_listid(mailing_list), id))
# Pivot the bans backup table to the real thing.
- store.execute('DROP TABLE ban;')
- store.execute('ALTER TABLE ban_backup RENAME TO ban;')
- # Pivot the mailinglist backup table to the real thing.
- store.execute('DROP TABLE mailinglist;')
- store.execute('ALTER TABLE ml_backup RENAME TO mailinglist;')
+ pivot(store, 'ban')
+ pivot(store, 'mailinglist')
@@ -90,7 +80,7 @@ def upgrade_postgres(database, store, version, module_path):
store.execute("""
UPDATE ban SET list_id = '{0}'
WHERE id = {1};
- """.format(_make_listid(mailing_list), id))
+ """.format(make_listid(mailing_list), id))
store.execute('ALTER TABLE ban DROP COLUMN mailing_list;')
store.execute('ALTER TABLE mailinglist DROP COLUMN new_member_options;')
store.execute('ALTER TABLE mailinglist DROP COLUMN send_reminders;')
diff --git a/src/mailman/database/schema/mm_20130406000000.py b/src/mailman/database/schema/mm_20130406000000.py
new file mode 100644
index 000000000..11ba70869
--- /dev/null
+++ b/src/mailman/database/schema/mm_20130406000000.py
@@ -0,0 +1,65 @@
+# Copyright (C) 2013 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""3.0b3 -> 3.0b4 schema migrations.
+
+Renamed:
+ * bounceevent.list_name -> bounceevent.list_id
+"""
+
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'upgrade'
+ ]
+
+
+from mailman.database.schema.helpers import make_listid, pivot
+
+
+VERSION = '20130406000000'
+
+
+
+def upgrade(database, store, version, module_path):
+ if database.TAG == 'sqlite':
+ upgrade_sqlite(database, store, version, module_path)
+ else:
+ upgrade_postgres(database, store, version, module_path)
+
+
+
+def upgrade_sqlite(database, store, version, module_path):
+ database.load_schema(
+ store, version, 'sqlite_{}_01.sql'.format(version), module_path)
+ results = store.execute("""
+ SELECT id, list_name
+ FROM bounceevent;
+ """)
+ for id, list_name in results:
+ store.execute("""
+ UPDATE bounceevent_backup SET list_id = '{}'
+ WHERE id = {};
+ """.format(make_listid(list_name), id))
+ pivot(store, 'bounceevent')
+
+
+
+def upgrade_postgres(database, store, version, module_path):
+ pass
diff --git a/src/mailman/database/schema/sqlite_20120407000000_01.sql b/src/mailman/database/schema/sqlite_20120407000000_01.sql
index 5eacc4047..a8db75be9 100644
--- a/src/mailman/database/schema/sqlite_20120407000000_01.sql
+++ b/src/mailman/database/schema/sqlite_20120407000000_01.sql
@@ -1,12 +1,12 @@
--- THIS FILE CONTAINS THE SQLITE3 SCHEMA MIGRATION FROM
+-- This file contains the sqlite3 schema migration from
-- 3.0b1 TO 3.0b2
--
--- AFTER 3.0b2 IS RELEASED YOU MAY NOT EDIT THIS FILE.
+-- 3.0b2 has been released thus you MAY NOT edit this file.
-- For SQLite3 migration strategy, see
-- http://sqlite.org/faq.html#q11
--- REMOVALS from the mailinglist table.
+-- REMOVALS from the mailinglist table:
-- REM archive
-- REM archive_private
-- REM archive_volume_frequency
@@ -15,7 +15,7 @@
-- REM news_prefix_subject_too
-- REM nntp_host
--
--- ADDS to the mailing list table.
+-- ADDS to the mailing list table:
-- ADD allow_list_posts
-- ADD archive_policy
-- ADD list_id
@@ -25,16 +25,16 @@
-- LP: #971013
-- LP: #967238
--- REMOVALS from the member table.
+-- REMOVALS from the member table:
-- REM mailing_list
--- ADDS to the member table.
+-- ADDS to the member table:
-- ADD list_id
-- LP: #1024509
-CREATE TABLE ml_backup (
+CREATE TABLE mailinglist_backup (
id INTEGER NOT NULL,
-- List identity
list_name TEXT,
@@ -142,7 +142,7 @@ CREATE TABLE ml_backup (
PRIMARY KEY (id)
);
-INSERT INTO ml_backup SELECT
+INSERT INTO mailinglist_backup SELECT
id,
-- List identity
list_name,
@@ -249,7 +249,7 @@ INSERT INTO ml_backup SELECT
welcome_message_uri
FROM mailinglist;
-CREATE TABLE mem_backup(
+CREATE TABLE member_backup(
id INTEGER NOT NULL,
_member_id TEXT,
role INTEGER,
@@ -260,7 +260,7 @@ CREATE TABLE mem_backup(
PRIMARY KEY (id)
);
-INSERT INTO mem_backup SELECT
+INSERT INTO member_backup SELECT
id,
_member_id,
role,
@@ -272,9 +272,9 @@ INSERT INTO mem_backup SELECT
-- Add the new columns. They'll get inserted at the Python layer.
-ALTER TABLE ml_backup ADD COLUMN archive_policy INTEGER;
-ALTER TABLE ml_backup ADD COLUMN list_id TEXT;
-ALTER TABLE ml_backup ADD COLUMN nntp_prefix_subject_too INTEGER;
-ALTER TABLE ml_backup ADD COLUMN newsgroup_moderation INTEGER;
+ALTER TABLE mailinglist_backup ADD COLUMN archive_policy INTEGER;
+ALTER TABLE mailinglist_backup ADD COLUMN list_id TEXT;
+ALTER TABLE mailinglist_backup ADD COLUMN nntp_prefix_subject_too INTEGER;
+ALTER TABLE mailinglist_backup ADD COLUMN newsgroup_moderation INTEGER;
-ALTER TABLE mem_backup ADD COLUMN list_id TEXT;
+ALTER TABLE member_backup ADD COLUMN list_id TEXT;
diff --git a/src/mailman/database/schema/sqlite_20121015000000_01.sql b/src/mailman/database/schema/sqlite_20121015000000_01.sql
index 3e2410c3b..a80dc03df 100644
--- a/src/mailman/database/schema/sqlite_20121015000000_01.sql
+++ b/src/mailman/database/schema/sqlite_20121015000000_01.sql
@@ -1,12 +1,12 @@
--- THIS FILE CONTAINS THE SQLITE3 SCHEMA MIGRATION FROM
+-- This file contains the sqlite3 schema migration from
-- 3.0b2 TO 3.0b3
--
--- AFTER 3.0b3 IS RELEASED YOU MAY NOT EDIT THIS FILE.
+-- 3.0b3 has been released thus you MAY NOT edit this file.
--- REMOVALS from the ban table.
+-- REMOVALS from the ban table:
-- REM mailing_list
--- ADDS to the ban table.
+-- ADDS to the ban table:
-- ADD list_id
CREATE TABLE ban_backup (
@@ -30,7 +30,7 @@ ALTER TABLE ban_backup ADD COLUMN list_id TEXT;
-- REM private_roster
-- REM admin_member_chunksize
-CREATE TABLE ml_backup (
+CREATE TABLE mailinglist_backup (
id INTEGER NOT NULL,
list_name TEXT,
mail_host TEXT,
@@ -130,7 +130,7 @@ CREATE TABLE ml_backup (
PRIMARY KEY (id)
);
-INSERT INTO ml_backup SELECT
+INSERT INTO mailinglist_backup SELECT
id,
list_name,
mail_host,
diff --git a/src/mailman/database/schema/sqlite_20130406000000_01.sql b/src/mailman/database/schema/sqlite_20130406000000_01.sql
new file mode 100644
index 000000000..fe30ed247
--- /dev/null
+++ b/src/mailman/database/schema/sqlite_20130406000000_01.sql
@@ -0,0 +1,46 @@
+-- This file contains the SQLite schema migration from
+-- 3.0b3 to 3.0b4
+--
+-- After 3.0b4 is released you may not edit this file.
+
+-- For SQLite3 migration strategy, see
+-- http://sqlite.org/faq.html#q11
+
+-- ADD listarchiver table.
+
+-- REMOVALs from the bounceevent table:
+-- REM list_name
+
+-- ADDs to the bounceevent table:
+-- ADD list_id
+
+-- ADDs to the mailinglist table:
+-- ADD archiver_id
+
+CREATE TABLE bounceevent_backup (
+ id INTEGER NOT NULL,
+ email TEXT,
+ 'timestamp' TIMESTAMP,
+ message_id TEXT,
+ context INTEGER,
+ processed BOOLEAN,
+ PRIMARY KEY (id)
+ );
+
+INSERT INTO bounceevent_backup SELECT
+ id, email, "timestamp", message_id,
+ context, processed
+ FROM bounceevent;
+
+ALTER TABLE bounceevent_backup ADD COLUMN list_id TEXT;
+
+CREATE TABLE listarchiver (
+ id INTEGER NOT NULL,
+ mailing_list_id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ _is_enabled BOOLEAN,
+ PRIMARY KEY (id)
+ );
+
+CREATE INDEX ix_listarchiver_mailing_list_id
+ ON listarchiver(mailing_list_id);
diff --git a/src/mailman/database/tests/test_migrations.py b/src/mailman/database/tests/test_migrations.py
index 410192605..44f594ba7 100644
--- a/src/mailman/database/tests/test_migrations.py
+++ b/src/mailman/database/tests/test_migrations.py
@@ -26,24 +26,30 @@ __all__ = [
'TestMigration20120407UnchangedData',
'TestMigration20121015MigratedData',
'TestMigration20121015Schema',
+ 'TestMigration20130406MigratedData',
+ 'TestMigration20130406Schema',
]
import unittest
+from datetime import datetime
from operator import attrgetter
from pkg_resources import resource_string
+from sqlite3 import OperationalError
from storm.exceptions import DatabaseError
from zope.component import getUtility
from mailman.interfaces.database import IDatabaseFactory
from mailman.interfaces.domain import IDomainManager
from mailman.interfaces.archiver import ArchivePolicy
+from mailman.interfaces.bounce import BounceContext
from mailman.interfaces.listmanager import IListManager
from mailman.interfaces.mailinglist import IAcceptableAliasSet
from mailman.interfaces.nntp import NewsgroupModeration
from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.model.bans import Ban
+from mailman.model.bounce import BounceEvent
from mailman.testing.helpers import temporary_db
from mailman.testing.layers import ConfigLayer
@@ -60,6 +66,23 @@ class MigrationTestBase(unittest.TestCase):
def tearDown(self):
self._database._cleanup()
+ def _table_missing_present(self, migrations, missing, present):
+ """The appropriate migrations leave some tables missing and present.
+
+ :param migrations: Sequence of migrations to load.
+ :param missing: Tables which should be missing.
+ :param present: Tables which should be present.
+ """
+ for migration in migrations:
+ self._database.load_migrations(migration)
+ self._database.store.commit()
+ for table in missing:
+ self.assertRaises(OperationalError,
+ self._database.store.execute,
+ 'select * from {};'.format(table))
+ for table in present:
+ self._database.store.execute('select * from {};'.format(table))
+
def _missing_present(self, table, migrations, missing, present):
"""The appropriate migrations leave columns missing and present.
@@ -426,3 +449,58 @@ class TestMigration20121015MigratedData(MigrationTestBase):
self.assertEqual(bans[0].list_id, 'test.example.com')
self.assertEqual(bans[1].email, 'bart@example.com')
self.assertEqual(bans[1].list_id, None)
+
+
+
+class TestMigration20130406Schema(MigrationTestBase):
+ """Test column migrations."""
+
+ def test_pre_upgrade_column_migrations(self):
+ self._missing_present('bounceevent',
+ ['20130405999999'],
+ ('list_id',),
+ ('list_name',))
+
+ def test_post_upgrade_column_migrations(self):
+ self._missing_present('bounceevent',
+ ['20130405999999',
+ '20130406000000'],
+ ('list_name',),
+ ('list_id',))
+
+ def test_pre_listarchiver_table(self):
+ self._table_missing_present(['20130405999999'], ('listarchiver',), ())
+
+ def test_post_listarchiver_table(self):
+ self._table_missing_present(['20130405999999',
+ '20130406000000'],
+ (),
+ ('listarchiver',))
+
+
+
+class TestMigration20130406MigratedData(MigrationTestBase):
+ """Test migrated data."""
+
+ def test_migration_bounceevent(self):
+ # Load all migrations to just before the one we're testing.
+ self._database.load_migrations('20130405999999')
+ # Insert a bounce event.
+ self._database.store.execute("""
+ INSERT INTO bounceevent VALUES (
+ 1, 'test@example.com', 'anne@example.com',
+ '2013-04-06 21:12:00', '<abc@example.com>',
+ 1, 0);
+ """)
+ # Update to the current migration we're testing
+ self._database.load_migrations('20130406000000')
+ # The bounce event should exist, but with a list-id instead of a fqdn
+ # list name.
+ events = list(self._database.store.find(BounceEvent))
+ self.assertEqual(len(events), 1)
+ self.assertEqual(events[0].list_id, 'test.example.com')
+ self.assertEqual(events[0].email, 'anne@example.com')
+ self.assertEqual(events[0].timestamp, datetime(2013, 4, 6, 21, 12))
+ self.assertEqual(events[0].message_id, '<abc@example.com>')
+ self.assertEqual(events[0].context, BounceContext.normal)
+ self.assertFalse(events[0].processed)
diff --git a/src/mailman/docs/MTA.rst b/src/mailman/docs/MTA.rst
index 04eef8a25..feec21106 100644
--- a/src/mailman/docs/MTA.rst
+++ b/src/mailman/docs/MTA.rst
@@ -38,25 +38,24 @@ add (or edit) a section like the following::
lmtp_port: 8024
smtp_host: localhost
smtp_port: 25
- configuration: mailman.config.postfix
+ configuration: python:mailman.config.postfix
This configuration is for a system where Mailman and the MTA are on
the same host.
-Note that the modules that configure the communication protocol
-(especially ``incoming``) are full-fledged Python programs, and may use
-these configuration parameters to automatically configure the MTA to
-recognize the list addresses and other attributes of the communication
-channel. This is why some constraints on the format of attributes arise
-(e.g., ``lmtp_host``), even though Mailman itself has no problem with
-them.
+Note that the modules that configure the communication protocol (especially
+``incoming``) are full-fledged Python modules, and may use these configuration
+parameters to automatically configure the MTA to recognize the list addresses
+and other attributes of the communication channel. This is why some
+constraints on the format of attributes arise (e.g., ``lmtp_host``), even
+though Mailman itself has no problem with them.
-The ``incoming`` and ``outgoing`` parameters identify the Python objects
-used to communicate with the MTA. They should be dotted Python module
-specifications. The ``deliver`` module used in ``outgoing`` should be
-satisfactory for most MTAs. The ``postfix`` module in ``incoming`` is
-specific to the Postfix MTA. See the section for your MTA below for details on
-these parameters.
+The ``incoming`` and ``outgoing`` parameters identify the Python objects used
+to communicate with the MTA. The ``python:`` scheme indicates that the paths
+should be a dotted Python module specification. The ``deliver`` module used
+in ``outgoing`` should be satisfactory for most MTAs. The ``postfix`` module
+in ``incoming`` is specific to the Postfix MTA. See the section for your MTA
+below for details on these parameters.
``lmtp_host`` and ``lmtp_port`` are parameters which are used by
Mailman, but also will be passed to the MTA to identify the Mailman
@@ -188,13 +187,13 @@ Exim
`Exim 4`_ is an MTA maintained by the `University of Cambridge`_ and
distributed by most open source OS distributions.
-Mailman settings:
------------------
+Mailman settings
+----------------
Add or edit a stanza like this in mailman.cfg::
[mta]
- # For all Exim4 installations
+ # For all Exim4 installations.
incoming: mailman.mta.exim4.LMTP
outgoing: mailman.mta.deliver.deliver
# Typical single host with MTA and Mailman configuration.
@@ -217,10 +216,10 @@ For further information about these settings, see
Exim4 configuration
-------------------
-The configuration presented below is mostly boilerplate that allows Exim
-to automatically discover your list addresses, and route both posts and
-administrative messages to the right Mailman services. For this reason,
-the mailman.mta.exim4 module ends up with all methods being no-ops.
+The configuration presented below is mostly boilerplate that allows Exim to
+automatically discover your list addresses, and route both posts and
+administrative messages to the right Mailman services. For this reason, the
+`mailman.mta.exim4` module ends up with all methods being no-ops.
This configuration is field-tested in a Debian "conf.d"-style Exim
installation, with multiple configuration files that are assembled by a
@@ -275,11 +274,11 @@ Troubleshooting
---------------
The most likely causes of failure to deliver to Mailman are typos in the
-configuration, and errors in the ``MM3_HOME`` macro or the
-``mm_domains`` list. Mismatches in the LMTP port could be a cause.
-Finally, Exim's router configuration is order-sensitive. Especially if
-you are being tricky and supporting Mailman 2 and Mailman 3 at the same
-time, you could have one shadow the other.
+configuration, and errors in the ``MM3_HOME`` macro or the ``mm_domains``
+list. Mismatches in the LMTP port could be a cause. Finally, Exim's router
+configuration is order-sensitive. Especially if you are being tricky and
+supporting Mailman 2 and Mailman 3 at the same time, you could have one shadow
+the other.
Exim 4 documentation
--------------------
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index 6572c9a7b..fc93a05f5 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -16,7 +16,8 @@ Development
-----------
* Mailman 3 no longer uses ``zc.buildout`` and tests are now run by the
``nose2`` test runner. See ``src/mailman/docs/START.rst`` for details on
- how to build Mailman and run the test suite.
+ how to build Mailman and run the test suite. Also, use ``-P`` to select a
+ test pattern and ``-E`` to enable stderr debugging in runners.
* Use the ``enum34`` package instead of ``flufl.enum``.
REST
@@ -27,6 +28,8 @@ REST
Given by Florian Fuchs. (LP: #1156529)
* Expose ``hide_address`` to the ``.../preferences`` REST API. Contributed
by Sneha Priscilla.
+ * Mailing lists can now individually enable or disable any archiver available
+ site-wide. Contributed by Joanna Skrzeszewska. (LP: #1158040)
Commands
--------
@@ -36,12 +39,19 @@ Commands
Configuration
-------------
+ * Add support for the Exim 4 MTA. Contributed by Stephen Turnbull.
* When creating the initial file system layout in ``var``, e.g. via
``bin/mailman info``, add an ``var/etc/mailman.cfg`` file if one does not
already exist. Also, when initializing the system, look for that file as
the configuration file, just after ``./mailman.cfg`` and before
``~/.mailman.cfg``. (LP: #1157861)
+Database
+--------
+ * The `bounceevent` table now uses list-ids to cross-reference the mailing
+ list, to match other tables. Similarly for the `IBounceEvent` interface.
+ * Added a `listarchiver` table to support list-specific archivers.
+
Bugs
----
* Non-queue runners should not create ``var/queue`` subdirectories. Fixed by
@@ -52,6 +62,8 @@ Bugs
signals. (LP: #1184376)
* Add `subject_prefix` to the `IMailingList` interface, and clarify the
docstring for `display_name`. (LP: #1181498)
+ * Fix importation from MM2.1 to MM3 of the archive policy. Given by Aurélien
+ Bompard. (LP: #1227658)
3.0 beta 3 -- "Here Again"
diff --git a/src/mailman/handlers/rfc_2369.py b/src/mailman/handlers/rfc_2369.py
index d203f747a..07a4289b5 100644
--- a/src/mailman/handlers/rfc_2369.py
+++ b/src/mailman/handlers/rfc_2369.py
@@ -28,10 +28,10 @@ __all__ = [
from email.utils import formataddr
from zope.interface import implementer
-from mailman.config import config
from mailman.core.i18n import _
from mailman.handlers.cook_headers import uheader
from mailman.interfaces.archiver import ArchivePolicy
+from mailman.interfaces.mailinglist import IListArchiverSet
from mailman.interfaces.handler import IHandler
@@ -84,10 +84,13 @@ def process(mlist, msg, msgdata):
headers['List-Post'] = list_post
# Add RFC 2369 and 5064 archiving headers, if archiving is enabled.
if mlist.archive_policy is not ArchivePolicy.never:
- for archiver in config.archivers:
+ archiver_set = IListArchiverSet(mlist)
+ for archiver in archiver_set.archivers:
+ if not archiver.is_enabled:
+ continue
headers['List-Archive'] = '<{0}>'.format(
- archiver.list_url(mlist))
- permalink = archiver.permalink(mlist, msg)
+ archiver.system_archiver.list_url(mlist))
+ permalink = archiver.system_archiver.permalink(mlist, msg)
if permalink is not None:
headers['Archived-At'] = permalink
# XXX RFC 2369 also defines a List-Owner header which we are not currently
diff --git a/src/mailman/interfaces/archiver.py b/src/mailman/interfaces/archiver.py
index 5f074503e..aac372865 100644
--- a/src/mailman/interfaces/archiver.py
+++ b/src/mailman/interfaces/archiver.py
@@ -50,6 +50,8 @@ class IArchiver(Interface):
"""An interface to the archiver."""
name = Attribute('The name of this archiver')
+ is_enabled = Attribute(
+ 'A flag indicating whether this archiver is enabled site-wide.')
def list_url(mlist):
"""Return the url to the top of the list's archive.
diff --git a/src/mailman/interfaces/bounce.py b/src/mailman/interfaces/bounce.py
index 8e7266687..ff7a47732 100644
--- a/src/mailman/interfaces/bounce.py
+++ b/src/mailman/interfaces/bounce.py
@@ -60,8 +60,8 @@ class UnrecognizedBounceDisposition(Enum):
class IBounceEvent(Interface):
"""Registration record for a single bounce event."""
- list_name = Attribute(
- """The name of the mailing list that received this bounce.""")
+ list_id = Attribute(
+ """The List-ID of the mailing list that received this bounce.""")
email = Attribute(
"""The email address that bounced.""")
diff --git a/src/mailman/interfaces/listmanager.py b/src/mailman/interfaces/listmanager.py
index 45b12af53..837abf310 100644
--- a/src/mailman/interfaces/listmanager.py
+++ b/src/mailman/interfaces/listmanager.py
@@ -97,9 +97,9 @@ class IListManager(Interface):
def create(fqdn_listname):
"""Create a mailing list with the given name.
- :type fqdn_listname: Unicode
:param fqdn_listname: The fully qualified name of the mailing list,
e.g. `mylist@example.com`.
+ :type fqdn_listname: Unicode
:return: The newly created `IMailingList`.
:raise `ListAlreadyExistsError` if the named list already exists.
"""
@@ -107,8 +107,8 @@ class IListManager(Interface):
def get(fqdn_listname):
"""Return the mailing list with the given name, if it exists.
- :type fqdn_listname: Unicode.
:param fqdn_listname: The fully qualified name of the mailing list.
+ :type fqdn_listname: Unicode.
:return: the matching `IMailingList` or None if the named list does
not exist.
"""
@@ -116,8 +116,8 @@ class IListManager(Interface):
def get_by_list_id(list_id):
"""Return the mailing list with the given list id, if it exists.
- :type fqdn_listname: Unicode.
:param fqdn_listname: The fully qualified name of the mailing list.
+ :type fqdn_listname: Unicode.
:return: the matching `IMailingList` or None if the named list does
not exist.
"""
@@ -125,8 +125,8 @@ class IListManager(Interface):
def delete(mlist):
"""Remove the mailing list from the database.
- :type mlist: `IMailingList`
:param mlist: The mailing list to delete.
+ :type mlist: `IMailingList`
"""
mailing_lists = Attribute(
diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py
index 7beaf9c46..b12d84ec9 100644
--- a/src/mailman/interfaces/mailinglist.py
+++ b/src/mailman/interfaces/mailinglist.py
@@ -23,6 +23,8 @@ __metaclass__ = type
__all__ = [
'IAcceptableAlias',
'IAcceptableAliasSet',
+ 'IListArchiver',
+ 'IListArchiverSet',
'IMailingList',
'Personalization',
'ReplyToMunging',
@@ -791,3 +793,37 @@ class IAcceptableAliasSet(Interface):
aliases = Attribute(
"""An iterator over all the acceptable aliases.""")
+
+
+
+class IListArchiver(Interface):
+ """An archiver for a mailing list.
+
+ The named archiver must be enabled site-wide in order for a mailing list
+ to be able to enable it.
+ """
+
+ mailing_list = Attribute('The associated mailing list.')
+
+ name = Attribute('The name of the archiver.')
+
+ is_enabled = Attribute('Is this archiver enabled for this mailing list?')
+
+ system_archiver = Attribute(
+ 'The associated system-wide IArchiver instance.')
+
+
+class IListArchiverSet(Interface):
+ """The set of archivers (enabled or disabled) for a mailing list."""
+
+ archivers = Attribute(
+ """An iterator over all the archivers for this mailing list.""")
+
+ def get(archiver_name):
+ """Return the `IListArchiver` with the given name, if it exists.
+
+ :param archiver_name: The name of the archiver.
+ :type archiver_name: unicode.
+ :return: the matching `IListArchiver` or None if the named archiver
+ does not exist.
+ """
diff --git a/src/mailman/model/bounce.py b/src/mailman/model/bounce.py
index 47a6ed248..9ae2b585d 100644
--- a/src/mailman/model/bounce.py
+++ b/src/mailman/model/bounce.py
@@ -43,15 +43,15 @@ class BounceEvent(Model):
"""See `IBounceEvent`."""
id = Int(primary=True)
- list_name = Unicode()
+ list_id = Unicode()
email = Unicode()
timestamp = DateTime()
message_id = Unicode()
context = Enum(BounceContext)
processed = Bool()
- def __init__(self, list_name, email, msg, context=None):
- self.list_name = list_name
+ def __init__(self, list_id, email, msg, context=None):
+ self.list_id = list_id
self.email = email
self.timestamp = now()
self.message_id = msg['message-id']
@@ -67,7 +67,7 @@ class BounceProcessor:
@dbconnection
def register(self, store, mlist, email, msg, where=None):
"""See `IBounceProcessor`."""
- event = BounceEvent(mlist.fqdn_listname, email, msg, where)
+ event = BounceEvent(mlist.list_id, email, msg, where)
store.add(event)
return event
diff --git a/src/mailman/model/docs/bounce.rst b/src/mailman/model/docs/bounce.rst
index b1491e607..f427689bd 100644
--- a/src/mailman/model/docs/bounce.rst
+++ b/src/mailman/model/docs/bounce.rst
@@ -39,8 +39,8 @@ of bouncing email addresses. These are passed one-by-one to the registration
interface.
>>> event = processor.register(mlist, 'anne@example.com', msg)
- >>> print event.list_name
- test@example.com
+ >>> print event.list_id
+ test.example.com
>>> print event.email
anne@example.com
>>> print event.message_id
diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py
index dd7a528c3..e9601e412 100644
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -47,8 +47,8 @@ from mailman.interfaces.digests import DigestFrequency
from mailman.interfaces.domain import IDomainManager
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.mailinglist import (
- IAcceptableAlias, IAcceptableAliasSet, IMailingList, Personalization,
- ReplyToMunging)
+ IAcceptableAlias, IAcceptableAliasSet, IListArchiver, IListArchiverSet,
+ IMailingList, Personalization, ReplyToMunging)
from mailman.interfaces.member import (
AlreadySubscribedError, MemberRole, MissingPreferredAddressError,
SubscriptionEvent)
@@ -538,3 +538,69 @@ class AcceptableAliasSet:
AcceptableAlias.mailing_list == self._mailing_list)
for alias in aliases:
yield alias.alias
+
+
+
+@implementer(IListArchiver)
+class ListArchiver(Model):
+ """See `IListArchiver`."""
+
+ id = Int(primary=True)
+
+ mailing_list_id = Int()
+ mailing_list = Reference(mailing_list_id, MailingList.id)
+ name = Unicode()
+ _is_enabled = Bool()
+
+ def __init__(self, mailing_list, archiver_name, system_archiver):
+ self.mailing_list = mailing_list
+ self.name = archiver_name
+ self._is_enabled = system_archiver.is_enabled
+
+ @property
+ def system_archiver(self):
+ for archiver in config.archivers:
+ if archiver.name == self.name:
+ return archiver
+ return None
+
+ @property
+ def is_enabled(self):
+ return self.system_archiver.is_enabled and self._is_enabled
+
+ @is_enabled.setter
+ def is_enabled(self, value):
+ self._is_enabled = value
+
+
+@implementer(IListArchiverSet)
+class ListArchiverSet:
+ def __init__(self, mailing_list):
+ self._mailing_list = mailing_list
+ system_archivers = {}
+ for archiver in config.archivers:
+ system_archivers[archiver.name] = archiver
+ # Add any system enabled archivers which aren't already associated
+ # with the mailing list.
+ store = Store.of(self._mailing_list)
+ for archiver_name in system_archivers:
+ exists = store.find(
+ ListArchiver,
+ And(ListArchiver.mailing_list == mailing_list,
+ ListArchiver.name == archiver_name)).one()
+ if exists is None:
+ store.add(ListArchiver(mailing_list, archiver_name,
+ system_archivers[archiver_name]))
+
+ @property
+ def archivers(self):
+ entries = Store.of(self._mailing_list).find(
+ ListArchiver, ListArchiver.mailing_list == self._mailing_list)
+ for entry in entries:
+ yield entry
+
+ def get(self, archiver_name):
+ return Store.of(self._mailing_list).find(
+ ListArchiver,
+ And(ListArchiver.mailing_list == self._mailing_list,
+ ListArchiver.name == archiver_name)).one()
diff --git a/src/mailman/model/tests/test_bounce.py b/src/mailman/model/tests/test_bounce.py
index 0657c78f3..ad3467d11 100644
--- a/src/mailman/model/tests/test_bounce.py
+++ b/src/mailman/model/tests/test_bounce.py
@@ -58,7 +58,7 @@ Message-Id: <first>
events = list(self._processor.events)
self.assertEqual(len(events), 1)
event = events[0]
- self.assertEqual(event.list_name, 'test@example.com')
+ self.assertEqual(event.list_id, 'test.example.com')
self.assertEqual(event.email, 'anne@example.com')
self.assertEqual(event.timestamp, datetime(2005, 8, 1, 7, 49, 23))
self.assertEqual(event.message_id, '<first>')
@@ -68,7 +68,7 @@ Message-Id: <first>
unprocessed = list(self._processor.unprocessed)
self.assertEqual(len(unprocessed), 1)
event = unprocessed[0]
- self.assertEqual(event.list_name, 'test@example.com')
+ self.assertEqual(event.list_id, 'test.example.com')
self.assertEqual(event.email, 'anne@example.com')
self.assertEqual(event.timestamp, datetime(2005, 8, 1, 7, 49, 23))
self.assertEqual(event.message_id, '<first>')
diff --git a/src/mailman/model/tests/test_domain.py b/src/mailman/model/tests/test_domain.py
index 3d7f95615..67924d393 100644
--- a/src/mailman/model/tests/test_domain.py
+++ b/src/mailman/model/tests/test_domain.py
@@ -21,6 +21,8 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestDomainLifecycleEvents',
+ 'TestDomainManager',
]
diff --git a/src/mailman/model/tests/test_listmanager.py b/src/mailman/model/tests/test_listmanager.py
index 152d96b9f..b18c8e5d1 100644
--- a/src/mailman/model/tests/test_listmanager.py
+++ b/src/mailman/model/tests/test_listmanager.py
@@ -17,10 +17,13 @@
"""Test the ListManager."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestListCreation',
+ 'TestListLifecycleEvents',
+ 'TestListManager',
]
diff --git a/src/mailman/model/tests/test_mailinglist.py b/src/mailman/model/tests/test_mailinglist.py
new file mode 100644
index 000000000..b2dbbf1ca
--- /dev/null
+++ b/src/mailman/model/tests/test_mailinglist.py
@@ -0,0 +1,106 @@
+# Copyright (C) 2013 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test MailingLists and related model objects.."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestListArchiver',
+ 'TestDisabledListArchiver',
+ ]
+
+
+import unittest
+
+from mailman.app.lifecycle import create_list
+from mailman.config import config
+from mailman.interfaces.mailinglist import IListArchiverSet
+from mailman.testing.helpers import configuration
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestListArchiver(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('ant@example.com')
+ self._set = IListArchiverSet(self._mlist)
+
+ def test_list_archivers(self):
+ # Find the set of archivers registered for this mailing list.
+ self.assertEqual(
+ ['mail-archive', 'mhonarc', 'prototype'],
+ sorted(archiver.name for archiver in self._set.archivers))
+
+ def test_get_archiver(self):
+ # Use .get() to see if a mailing list has an archiver.
+ archiver = self._set.get('prototype')
+ self.assertEqual(archiver.name, 'prototype')
+ self.assertTrue(archiver.is_enabled)
+ self.assertEqual(archiver.mailing_list, self._mlist)
+ self.assertEqual(archiver.system_archiver.name, 'prototype')
+
+ def test_get_archiver_no_such(self):
+ # Using .get() on a non-existing name returns None.
+ self.assertIsNone(self._set.get('no-such-archiver'))
+
+ def test_site_disabled(self):
+ # Here the system configuration enables all the archivers in time for
+ # the archive set to be created with all list archivers enabled. But
+ # then the site-wide archiver gets disabled, so the list specific
+ # archiver will also be disabled.
+ archiver_set = IListArchiverSet(self._mlist)
+ archiver = archiver_set.get('prototype')
+ self.assertTrue(archiver.is_enabled)
+ # Disable the site-wide archiver.
+ config.push('enable prototype', """\
+ [archiver.prototype]
+ enable: no
+ """)
+ self.assertFalse(archiver.is_enabled)
+ config.pop('enable prototype')
+
+
+
+class TestDisabledListArchiver(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('ant@example.com')
+
+ @configuration('archiver.prototype', enable='no')
+ def test_enable_list_archiver(self):
+ # When the system configuration file disables an archiver site-wide,
+ # the list-specific mailing list will get initialized as not enabled.
+ # Create the archiver set on the fly so that it doesn't get
+ # initialized with a configuration that enables the prototype archiver.
+ archiver_set = IListArchiverSet(self._mlist)
+ archiver = archiver_set.get('prototype')
+ self.assertFalse(archiver.is_enabled)
+ # Enable both the list archiver and the system archiver.
+ archiver.is_enabled = True
+ config.push('enable prototype', """\
+ [archiver.prototype]
+ enable: yes
+ """)
+ # Get the IListArchiver again.
+ archiver = archiver_set.get('prototype')
+ self.assertTrue(archiver.is_enabled)
+ config.pop('enable prototype')
diff --git a/src/mailman/mta/exim4.py b/src/mailman/mta/exim4.py
index d66d8d6bc..c2a364ae5 100644
--- a/src/mailman/mta/exim4.py
+++ b/src/mailman/mta/exim4.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2001-2013 by the Free Software Foundation, Inc.
+# Copyright (C) 2013 by the Free Software Foundation, Inc.
#
# This file is part of GNU Mailman.
#
@@ -17,16 +17,16 @@
"""Creation/deletion hooks for the Exim4 MTA."""
-# if needed:
-# from __future__ import absolute_import, print_function, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'LMTP',
]
-from zope.interface import implementer
+
from mailman.interfaces.mta import IMailTransportAgentLifecycle
+from zope.interface import implementer
@@ -34,7 +34,8 @@ from mailman.interfaces.mta import IMailTransportAgentLifecycle
class LMTP:
"""Connect Mailman to Exim4 via LMTP.
- See `IMailTransportAgentLifecycle`."""
+ See `IMailTransportAgentLifecycle`.
+ """
# Exim4 handles all configuration itself, by finding the list config file.
def __init__(self):
diff --git a/src/mailman/rest/docs/lists.rst b/src/mailman/rest/docs/lists.rst
index 295e8c0b7..27503c1c1 100644
--- a/src/mailman/rest/docs/lists.rst
+++ b/src/mailman/rest/docs/lists.rst
@@ -230,3 +230,61 @@ The mailing list does not exist.
>>> print list_manager.get('ant@example.com')
None
+
+
+Managing mailing list archivers
+===============================
+
+The Mailman system has some site-wide enabled archivers, and each mailing list
+can enable or disable these archivers individually. This gives list owners
+control over where traffic to their list is archived. You can see which
+archivers are available, and whether they are enabled for this mailing list.
+::
+
+ >>> mlist = create_list('dog@example.com')
+ >>> transaction.commit()
+
+ >>> dump_json('http://localhost:9001/3.0/lists/dog@example.com/archivers')
+ http_etag: "..."
+ mail-archive: True
+ mhonarc: True
+ prototype: True
+
+You can set all the archiver states by putting new state flags on the
+resource.
+::
+
+ >>> dump_json(
+ ... 'http://localhost:9001/3.0/lists/dog@example.com/archivers', {
+ ... 'mail-archive': False,
+ ... 'mhonarc': True,
+ ... 'prototype': False,
+ ... }, method='PUT')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.0/lists/dog@example.com/archivers')
+ http_etag: "..."
+ mail-archive: False
+ mhonarc: True
+ prototype: False
+
+You can change the state of a subset of the list archivers.
+::
+
+ >>> dump_json(
+ ... 'http://localhost:9001/3.0/lists/dog@example.com/archivers', {
+ ... 'mhonarc': False,
+ ... }, method='PATCH')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.0/lists/dog@example.com/archivers')
+ http_etag: "..."
+ mail-archive: False
+ mhonarc: False
+ prototype: False
diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py
index 32e22a76b..b8e754647 100644
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -23,11 +23,13 @@ __metaclass__ = type
__all__ = [
'AList',
'AllLists',
+ 'ListArchivers',
'ListConfiguration',
'ListsForDomain',
]
+from lazr.config import as_boolean
from operator import attrgetter
from restish import http, resource
from zope.component import getUtility
@@ -36,11 +38,13 @@ from mailman.app.lifecycle import create_list, remove_list
from mailman.interfaces.domain import BadDomainSpecificationError
from mailman.interfaces.listmanager import (
IListManager, ListAlreadyExistsError)
+from mailman.interfaces.mailinglist import IListArchiverSet
from mailman.interfaces.member import MemberRole
from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.rest.configuration import ListConfiguration
from mailman.rest.helpers import (
- CollectionMixin, etag, no_content, paginate, path_to, restish_matcher)
+ CollectionMixin, GetterSetter, PATCH, etag, no_content, paginate, path_to,
+ restish_matcher)
from mailman.rest.members import AMember, MemberCollection
from mailman.rest.moderation import HeldMessages, SubscriptionRequests
from mailman.rest.validator import Validator
@@ -189,6 +193,13 @@ class AList(_ListBase):
return http.not_found()
return SubscriptionRequests(self._mlist)
+ @resource.child()
+ def archivers(self, request, segments):
+ """Return a representation of mailing list archivers."""
+ if self._mlist is None:
+ return http.not_found()
+ return ListArchivers(self._mlist)
+
class AllLists(_ListBase):
@@ -256,3 +267,59 @@ class ListsForDomain(_ListBase):
def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(self._domain.mailing_lists)
+
+
+
+class ArchiverGetterSetter(GetterSetter):
+ """Resource for updating archiver statuses."""
+
+ def __init__(self, mlist):
+ super(ArchiverGetterSetter, self).__init__()
+ self._archiver_set = IListArchiverSet(mlist)
+
+ def put(self, mlist, attribute, value):
+ # attribute will contain the (bytes) name of the archiver that is
+ # getting a new status. value will be the representation of the new
+ # boolean status.
+ archiver = self._archiver_set.get(attribute.decode('utf-8'))
+ if archiver is None:
+ raise ValueError('No such archiver: {}'.format(attribute))
+ archiver.is_enabled = as_boolean(value)
+
+
+class ListArchivers(resource.Resource):
+ """The archivers for a list, with their enabled flags."""
+
+ def __init__(self, mlist):
+ self._mlist = mlist
+
+ @resource.GET()
+ def statuses(self, request):
+ """Get all the archiver statuses."""
+ archiver_set = IListArchiverSet(self._mlist)
+ resource = {archiver.name: archiver.is_enabled
+ for archiver in archiver_set.archivers}
+ return http.ok([], etag(resource))
+
+ def patch_put(self, request, is_optional):
+ archiver_set = IListArchiverSet(self._mlist)
+ kws = {archiver.name: ArchiverGetterSetter(self._mlist)
+ for archiver in archiver_set.archivers}
+ if is_optional:
+ # For a PUT, all attributes are optional.
+ kws['_optional'] = kws.keys()
+ try:
+ Validator(**kws).update(self._mlist, request)
+ except ValueError as error:
+ return http.bad_request([], str(error))
+ return no_content()
+
+ @resource.PUT()
+ def put_statuses(self, request):
+ """Update all the archiver statuses."""
+ return self.patch_put(request, is_optional=False)
+
+ @PATCH()
+ def patch_statuses(self, request):
+ """Patch some archiver statueses."""
+ return self.patch_put(request, is_optional=True)
diff --git a/src/mailman/rest/tests/test_lists.py b/src/mailman/rest/tests/test_lists.py
index 7c2182c9c..77b85895b 100644
--- a/src/mailman/rest/tests/test_lists.py
+++ b/src/mailman/rest/tests/test_lists.py
@@ -21,6 +21,7 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestListArchivers',
'TestLists',
'TestListsMissing',
]
@@ -159,3 +160,71 @@ class TestLists(unittest.TestCase):
call_api('http://localhost:9001/3.0/lists/ant.example.com',
method='DELETE')
self.assertEqual(cm.exception.code, 404)
+
+
+
+class TestListArchivers(unittest.TestCase):
+ """Test corner cases for list archivers."""
+
+ layer = RESTLayer
+
+ def setUp(self):
+ with transaction():
+ self._mlist = create_list('ant@example.com')
+
+ def test_archiver_statuses(self):
+ resource, response = call_api(
+ 'http://localhost:9001/3.0/lists/ant.example.com/archivers')
+ self.assertEqual(response.status, 200)
+ # Remove the variable data.
+ resource.pop('http_etag')
+ self.assertEqual(resource, {
+ 'mail-archive': True,
+ 'mhonarc': True,
+ 'prototype': True,
+ })
+
+ def test_archiver_statuses_on_missing_lists(self):
+ # You cannot get the archiver statuses on a list that doesn't exist.
+ with self.assertRaises(HTTPError) as cm:
+ call_api(
+ 'http://localhost:9001/3.0/lists/bee.example.com/archivers')
+ self.assertEqual(cm.exception.code, 404)
+
+ def test_patch_status_on_bogus_archiver(self):
+ # You cannot set the status on an archiver the list doesn't know about.
+ with self.assertRaises(HTTPError) as cm:
+ call_api(
+ 'http://localhost:9001/3.0/lists/ant.example.com/archivers', {
+ 'bogus-archiver': True,
+ },
+ method='PATCH')
+ self.assertEqual(cm.exception.code, 400)
+ self.assertEqual(cm.exception.reason,
+ 'Unexpected parameters: bogus-archiver')
+
+ def test_put_incomplete_statuses(self):
+ # PUT requires the full resource representation. This one forgets to
+ # specify the prototype and mhonarc archiver.
+ with self.assertRaises(HTTPError) as cm:
+ call_api(
+ 'http://localhost:9001/3.0/lists/ant.example.com/archivers', {
+ 'mail-archive': True,
+ },
+ method='PUT')
+ self.assertEqual(cm.exception.code, 400)
+ self.assertEqual(cm.exception.reason,
+ 'Missing parameters: mhonarc, prototype')
+
+ def test_patch_bogus_status(self):
+ # Archiver statuses must be interpretable as booleans.
+ with self.assertRaises(HTTPError) as cm:
+ call_api(
+ 'http://localhost:9001/3.0/lists/ant.example.com/archivers', {
+ 'mail-archive': 'sure',
+ 'mhonarc': False,
+ 'prototype': 'no'
+ },
+ method='PATCH')
+ self.assertEqual(cm.exception.code, 400)
+ self.assertEqual(cm.exception.reason, 'Invalid boolean value: sure')
diff --git a/src/mailman/runners/archive.py b/src/mailman/runners/archive.py
index f18bd7c61..907ba5707 100644
--- a/src/mailman/runners/archive.py
+++ b/src/mailman/runners/archive.py
@@ -36,6 +36,7 @@ from mailman.config import config
from mailman.core.runner import Runner
from mailman.interfaces.archiver import ClobberDate
from mailman.utilities.datetime import RFC822_DATE_FMT, now
+from mailman.interfaces.mailinglist import IListArchiverSet
log = logging.getLogger('mailman.error')
@@ -90,7 +91,12 @@ class ArchiveRunner(Runner):
def _dispose(self, mlist, msg, msgdata):
received_time = msgdata.get('received_time', now(strip_tzinfo=False))
- for archiver in config.archivers:
+ archiver_set = IListArchiverSet(mlist)
+ for archiver in archiver_set.archivers:
+ # The archiver is disabled if either the list-specific or
+ # site-wide archiver is disabled.
+ if not archiver.is_enabled:
+ continue
msg_copy = copy.deepcopy(msg)
if _should_clobber(msg, msgdata, archiver.name):
original_date = msg_copy['date']
@@ -102,6 +108,6 @@ class ArchiveRunner(Runner):
# A problem in one archiver should not prevent other archivers
# from running.
try:
- archiver.archive_message(mlist, msg_copy)
+ archiver.system_archiver.archive_message(mlist, msg_copy)
except Exception:
log.exception('Broken archiver: %s' % archiver.name)
diff --git a/src/mailman/runners/tests/test_archiver.py b/src/mailman/runners/tests/test_archiver.py
index 80a676dfd..f7087f28f 100644
--- a/src/mailman/runners/tests/test_archiver.py
+++ b/src/mailman/runners/tests/test_archiver.py
@@ -34,6 +34,7 @@ from zope.interface import implementer
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.interfaces.archiver import IArchiver
+from mailman.interfaces.mailinglist import IListArchiverSet
from mailman.runners.archive import ArchiveRunner
from mailman.testing.helpers import (
configuration,
@@ -99,6 +100,7 @@ X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
First post!
""")
self._runner = make_testable_runner(ArchiveRunner)
+ IListArchiverSet(self._mlist).get('dummy').is_enabled = True
def tearDown(self):
config.pop('dummy')
@@ -237,3 +239,16 @@ First post!
self.assertEqual(archived['message-id'], '<first>')
self.assertEqual(archived['date'], 'Mon, 01 Aug 2005 07:49:23 +0000')
self.assertEqual(archived['x-original-date'], None)
+
+ @configuration('archiver.dummy', enable='yes')
+ def test_disable_all_list_archivers(self):
+ # Let's disable all the archivers for the mailing list, but not the
+ # global archivers. No messages will get archived.
+ for archiver in IListArchiverSet(self._mlist).archivers:
+ archiver.is_enabled = False
+ config.db.store.commit()
+ self._archiveq.enqueue(
+ self._msg, {},
+ listname=self._mlist.fqdn_listname)
+ self._runner.run()
+ self.assertEqual(os.listdir(config.MESSAGES_DIR), [])
diff --git a/src/mailman/runners/tests/test_bounce.py b/src/mailman/runners/tests/test_bounce.py
index 36920da2b..fd890aed1 100644
--- a/src/mailman/runners/tests/test_bounce.py
+++ b/src/mailman/runners/tests/test_bounce.py
@@ -96,7 +96,7 @@ Message-Id: <first>
events = list(self._processor.events)
self.assertEqual(len(events), 1)
self.assertEqual(events[0].email, 'anne@example.com')
- self.assertEqual(events[0].list_name, 'test@example.com')
+ self.assertEqual(events[0].list_id, 'test.example.com')
self.assertEqual(events[0].message_id, '<first>')
self.assertEqual(events[0].context, BounceContext.normal)
self.assertEqual(events[0].processed, False)
@@ -145,7 +145,7 @@ Message-Id: <second>
events = list(self._processor.events)
self.assertEqual(len(events), 1)
self.assertEqual(events[0].email, 'anne@example.com')
- self.assertEqual(events[0].list_name, 'test@example.com')
+ self.assertEqual(events[0].list_id, 'test.example.com')
self.assertEqual(events[0].message_id, '<second>')
self.assertEqual(events[0].context, BounceContext.probe)
self.assertEqual(events[0].processed, False)
@@ -175,7 +175,7 @@ Original-Recipient: rfc822; bart@example.com
events = list(self._processor.events)
self.assertEqual(len(events), 1)
self.assertEqual(events[0].email, 'bart@example.com')
- self.assertEqual(events[0].list_name, 'test@example.com')
+ self.assertEqual(events[0].list_id, 'test.example.com')
self.assertEqual(events[0].message_id, '<first>')
self.assertEqual(events[0].context, BounceContext.normal)
self.assertEqual(events[0].processed, False)
diff --git a/src/mailman/runners/tests/test_outgoing.py b/src/mailman/runners/tests/test_outgoing.py
index 1281712b8..c897fc013 100644
--- a/src/mailman/runners/tests/test_outgoing.py
+++ b/src/mailman/runners/tests/test_outgoing.py
@@ -374,7 +374,7 @@ Message-Id: <first>
events = list(self._processor.unprocessed)
self.assertEqual(len(events), 1)
event = events[0]
- self.assertEqual(event.list_name, 'test@example.com')
+ self.assertEqual(event.list_id, 'test.example.com')
self.assertEqual(event.email, 'anne@example.com')
self.assertEqual(event.timestamp, datetime(2005, 8, 1, 7, 49, 23))
self.assertEqual(event.message_id, '<first>')
diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py
index 8201f952a..9f9e28dc6 100644
--- a/src/mailman/testing/helpers.py
+++ b/src/mailman/testing/helpers.py
@@ -478,6 +478,10 @@ def reset_the_world():
with transaction():
for message in message_store.messages:
message_store.delete_message(message['message-id'])
+ # Delete any other residual messages.
+ for dirpath, dirnames, filenames in os.walk(config.MESSAGES_DIR):
+ for filename in filenames:
+ os.remove(os.path.join(dirpath, filename))
# Reset the global style manager.
getUtility(IStyleManager).populate()
# Remove all dynamic header-match rules.
diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
index 6d150815f..2d2552f93 100644
--- a/src/mailman/testing/layers.py
+++ b/src/mailman/testing/layers.py
@@ -143,7 +143,6 @@ class ConfigLayer(MockAndMonkeyLayer):
if cls.stderr:
test_config += dedent("""
[logging.root]
- propagate: yes
level: debug
""")
# Enable log message propagation and reset the log paths so that the
@@ -154,7 +153,7 @@ class ConfigLayer(MockAndMonkeyLayer):
continue
logger_name = 'mailman.' + sub_name
log = logging.getLogger(logger_name)
- #log.propagate = True
+ log.propagate = cls.stderr
# Reopen the file to a new path that tests can get at. Instead of
# using the configuration file path though, use a path that's
# specific to the logger so that tests can find expected output
@@ -170,15 +169,16 @@ class ConfigLayer(MockAndMonkeyLayer):
propagate: yes
level: debug
"""), dict(name=sub_name, path=path))
- # zope.testing sets up logging before we get to our own initialization
- # function. This messes with the root logger, so explicitly set it to
- # go to stderr.
+ # The root logger will already have a handler, but it's not the right
+ # handler. Remove that and set our own.
if cls.stderr:
console = logging.StreamHandler(sys.stderr)
formatter = logging.Formatter(config.logging.root.format,
config.logging.root.datefmt)
console.setFormatter(formatter)
- logging.getLogger().addHandler(console)
+ root = logging.getLogger()
+ del root.handlers[:]
+ root.addHandler(console)
# Write the configuration file for subprocesses and set up the config
# object to pass that properly on the -C option.
config_file = os.path.join(cls.var_dir, 'test.cfg')
@@ -209,27 +209,6 @@ class ConfigLayer(MockAndMonkeyLayer):
# Flag to indicate that loggers should propagate to the console.
stderr = False
- @classmethod
- def enable_stderr(cls):
- """Enable stderr logging if -e/--stderr is given.
-
- We used to hack our way into the zc.testing framework, but that was
- undocumented and way too fragile. Well, this probably is too, but now
- we just scan sys.argv for -e/--stderr and enable logging if found.
- Then we remove the option from sys.argv. This works because this
- method is called before zope.testrunner sees the options.
-
- As a bonus, we'll check an environment variable too.
- """
- if '-e' in sys.argv:
- cls.stderr = True
- sys.argv.remove('-e')
- if '--stderr' in sys.argv:
- cls.stderr = True
- sys.argv.remove('--stderr')
- if len(os.environ.get('MM_VERBOSE_TESTLOG', '').strip()) > 0:
- cls.stderr = True
-
# The top of our source tree, for tests that care (e.g. hooks.txt).
root_directory = None
diff --git a/src/mailman/testing/nose.py b/src/mailman/testing/nose.py
index 86a3e6a01..b258a67b9 100644
--- a/src/mailman/testing/nose.py
+++ b/src/mailman/testing/nose.py
@@ -47,12 +47,19 @@ class NosePlugin(Plugin):
def __init__(self):
super(NosePlugin, self).__init__()
self.patterns = []
+ self.stderr = False
+ def set_stderr(ignore):
+ self.stderr = True
self.addArgument(self.patterns, 'P', 'pattern',
'Add a test matching pattern')
+ self.addFlag(set_stderr, 'E', 'stderr',
+ 'Enable stderr logging to sub-runners')
def startTestRun(self, event):
MockAndMonkeyLayer.testing_mode = True
- ConfigLayer.enable_stderr()
+ if ( self.stderr or
+ len(os.environ.get('MM_VERBOSE_TESTLOG', '').strip()) > 0):
+ ConfigLayer.stderr = True
def getTestCaseNames(self, event):
if len(self.patterns) == 0:
@@ -60,15 +67,19 @@ class NosePlugin(Plugin):
return
# Does the pattern match the fully qualified class name?
for pattern in self.patterns:
- full_name = '{}.{}'.format(
+ full_class_name = '{}.{}'.format(
event.testCase.__module__, event.testCase.__name__)
- if re.search(pattern, full_name):
+ if re.search(pattern, full_class_name):
# Don't suppress this test class.
return
names = filter(event.isTestMethod, dir(event.testCase))
for name in names:
+ full_test_name = '{}.{}.{}'.format(
+ event.testCase.__module__,
+ event.testCase.__name__,
+ name)
for pattern in self.patterns:
- if re.search(pattern, name):
+ if re.search(pattern, full_test_name):
break
else:
event.excludedNames.append(name)
diff --git a/src/mailman/utilities/importer.py b/src/mailman/utilities/importer.py
index f5aa8d10a..21a1e2f09 100644
--- a/src/mailman/utilities/importer.py
+++ b/src/mailman/utilities/importer.py
@@ -29,6 +29,7 @@ import sys
import datetime
from mailman.interfaces.action import FilterAction
+from mailman.interfaces.archiver import ArchivePolicy
from mailman.interfaces.autorespond import ResponseAction
from mailman.interfaces.digests import DigestFrequency
from mailman.interfaces.mailinglist import Personalization, ReplyToMunging
@@ -90,3 +91,15 @@ def import_config_pck(mlist, config_dict):
except TypeError:
print('Type conversion error:', key, file=sys.stderr)
raise
+ # Handle the archiving policy. In MM2.1 there were two boolean options
+ # but only three of the four possible states were valid. Now there's just
+ # an enum.
+ if config_dict.get('archive'):
+ # For maximum safety, if for some strange reason there's no
+ # archive_private key, treat the list as having private archives.
+ if config_dict.get('archive_private', True):
+ mlist.archive_policy = ArchivePolicy.private
+ else:
+ mlist.archive_policy = ArchivePolicy.public
+ else:
+ mlist.archive_policy = ArchivePolicy.never
diff --git a/src/mailman/utilities/tests/test_import.py b/src/mailman/utilities/tests/test_import.py
index c8da32e42..b64da7501 100644
--- a/src/mailman/utilities/tests/test_import.py
+++ b/src/mailman/utilities/tests/test_import.py
@@ -21,6 +21,7 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestArchiveImport',
'TestBasicImport',
]
@@ -29,6 +30,7 @@ import cPickle
import unittest
from mailman.app.lifecycle import create_list, remove_list
+from mailman.interfaces.archiver import ArchivePolicy
from mailman.testing.layers import ConfigLayer
from mailman.utilities.importer import import_config_pck
from pkg_resources import resource_filename
@@ -68,3 +70,56 @@ class TestBasicImport(unittest.TestCase):
self._import()
self.assertTrue(self._mlist.allow_list_posts)
self.assertTrue(self._mlist.include_rfc2369_headers)
+
+
+
+class TestArchiveImport(unittest.TestCase):
+ """Test conversion of the archive policies.
+
+ Mailman 2.1 had two variables `archive` and `archive_private`. Now
+ there's just a single `archive_policy` enum.
+ """
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('blank@example.com')
+ self._mlist.archive_policy = 'INITIAL-TEST-VALUE'
+
+ def _do_test(self, pckdict, expected):
+ import_config_pck(self._mlist, pckdict)
+ self.assertEqual(self._mlist.archive_policy, expected)
+
+ def test_public(self):
+ self._do_test(dict(archive=True, archive_private=False),
+ ArchivePolicy.public)
+
+ def test_private(self):
+ self._do_test(dict(archive=True, archive_private=True),
+ ArchivePolicy.private)
+
+ def test_no_archive(self):
+ self._do_test(dict(archive=False, archive_private=False),
+ ArchivePolicy.never)
+
+ def test_bad_state(self):
+ # For some reason, the old list has the invalid archiving state where
+ # `archive` is False and `archive_private` is True. It doesn't matter
+ # because this still collapses to the same enum value.
+ self._do_test(dict(archive=False, archive_private=True),
+ ArchivePolicy.never)
+
+ def test_missing_archive_key(self):
+ # For some reason, the old list didn't have an `archive` key. We
+ # treat this as if no archiving is done.
+ self._do_test(dict(archive_private=False), ArchivePolicy.never)
+
+ def test_missing_archive_key_archive_public(self):
+ # For some reason, the old list didn't have an `archive` key, and it
+ # has weird value for archive_private. We treat this as if no
+ # archiving is done.
+ self._do_test(dict(archive_private=True), ArchivePolicy.never)
+
+ def test_missing_archive_private_key(self):
+ # For some reason, the old list was missing an `archive_private` key.
+ # For maximum safety, we treat this as private archiving.
+ self._do_test(dict(archive=True), ArchivePolicy.private)