summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2013-11-27 15:13:10 -0500
committerBarry Warsaw2013-11-27 15:13:10 -0500
commit0ce1d7a1da2e93270acc49d4527417fa6c20a911 (patch)
treebbfa43d9f71690096ed218ca5862550a726f5994
parentadde8bfb3f90f2d2204500bce75550fee8369bcb (diff)
parentb3ce2a4f6106fa4b2d014ab921f9b6a25b067de3 (diff)
downloadmailman-0ce1d7a1da2e93270acc49d4527417fa6c20a911.tar.gz
mailman-0ce1d7a1da2e93270acc49d4527417fa6c20a911.tar.zst
mailman-0ce1d7a1da2e93270acc49d4527417fa6c20a911.zip
-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/config/config.py10
-rw-r--r--src/mailman/config/configure.zcml6
-rw-r--r--src/mailman/config/schema.cfg2
-rw-r--r--src/mailman/config/tests/test_archivers.py56
-rw-r--r--src/mailman/database/schema/sqlite_20130406000000_01.sql20
-rw-r--r--src/mailman/database/tests/test_migrations.py27
-rw-r--r--src/mailman/docs/NEWS.rst2
-rw-r--r--src/mailman/handlers/rfc_2369.py11
-rw-r--r--src/mailman/interfaces/archiver.py2
-rw-r--r--src/mailman/interfaces/listmanager.py8
-rw-r--r--src/mailman/interfaces/mailinglist.py36
-rw-r--r--src/mailman/model/mailinglist.py70
-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/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/testing/helpers.py4
24 files changed, 571 insertions, 22 deletions
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/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/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/database/schema/sqlite_20130406000000_01.sql b/src/mailman/database/schema/sqlite_20130406000000_01.sql
index 9bdc2aae0..fe30ed247 100644
--- a/src/mailman/database/schema/sqlite_20130406000000_01.sql
+++ b/src/mailman/database/schema/sqlite_20130406000000_01.sql
@@ -6,12 +6,17 @@
-- For SQLite3 migration strategy, see
-- http://sqlite.org/faq.html#q11
--- REMOVALS from the bounceevent table:
+-- ADD listarchiver table.
+
+-- REMOVALs from the bounceevent table:
-- REM list_name
--- ADDS to the ban bounceevent table:
+-- 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,
@@ -28,3 +33,14 @@ INSERT INTO bounceevent_backup SELECT
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 d983f9891..44f594ba7 100644
--- a/src/mailman/database/tests/test_migrations.py
+++ b/src/mailman/database/tests/test_migrations.py
@@ -36,6 +36,7 @@ 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
@@ -65,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.
@@ -450,6 +468,15 @@ class TestMigration20130406Schema(MigrationTestBase):
('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):
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index 37a05c3ed..83ee52f72 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -28,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
--------
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/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/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_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/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/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.