diff options
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) |
