summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/app/tests/test_bounces.py5
-rw-r--r--src/mailman/app/tests/test_inject.py15
-rw-r--r--src/mailman/app/tests/test_moderation.py3
-rw-r--r--src/mailman/app/tests/test_notifications.py5
-rw-r--r--src/mailman/archiving/mailarchive.py24
-rw-r--r--src/mailman/archiving/mhonarc.py23
-rw-r--r--src/mailman/chains/tests/test_hold.py5
-rw-r--r--src/mailman/commands/docs/aliases.rst1
-rw-r--r--src/mailman/config/config.py59
-rw-r--r--src/mailman/config/mail_archive.cfg29
-rw-r--r--src/mailman/config/mhonarc.cfg27
-rw-r--r--src/mailman/config/postfix.cfg8
-rw-r--r--src/mailman/config/schema.cfg38
-rw-r--r--src/mailman/config/tests/test_configuration.py50
-rw-r--r--src/mailman/docs/ACKNOWLEDGMENTS.rst1
-rw-r--r--src/mailman/docs/NEWS.rst8
-rw-r--r--src/mailman/handlers/tests/test_mimedel.py7
-rw-r--r--src/mailman/interfaces/configuration.py11
-rw-r--r--src/mailman/mta/postfix.py10
-rw-r--r--src/mailman/mta/tests/test_aliases.py19
-rw-r--r--src/mailman/mta/tests/test_delivery.py5
-rw-r--r--src/mailman/rules/tests/test_approved.py4
-rw-r--r--src/mailman/runners/tests/test_owner.py3
-rw-r--r--src/mailman/testing/layers.py15
-rw-r--r--src/mailman/testing/mail_archive.cfg4
-rw-r--r--src/mailman/testing/mhonarc.cfg4
-rw-r--r--src/mailman/testing/testing.cfg7
-rw-r--r--src/mailman/utilities/passwords.py11
-rw-r--r--src/mailman/utilities/tests/test_passwords.py2
29 files changed, 297 insertions, 106 deletions
diff --git a/src/mailman/app/tests/test_bounces.py b/src/mailman/app/tests/test_bounces.py
index 4be272c34..fae30724e 100644
--- a/src/mailman/app/tests/test_bounces.py
+++ b/src/mailman/app/tests/test_bounces.py
@@ -320,9 +320,8 @@ $address
$optionsurl
$owneraddr
""", file=fp)
- # Python 2.7 has assertMultiLineEqual. Let this work without bounds.
+ # Let assertMultiLineEqual work without bounds.
self.maxDiff = None
- self.eq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
def tearDown(self):
config.pop('xx template dir')
@@ -343,7 +342,7 @@ $owneraddr
send_probe(self._member, self._msg)
message = get_queue_messages('virgin')[0].msg
notice = message.get_payload(0).get_payload()
- self.eq(notice, """\
+ self.assertMultiLineEqual(notice, """\
blah blah blah test@example.com anne@example.com
http://example.com/anne@example.com test-owner@example.com""")
diff --git a/src/mailman/app/tests/test_inject.py b/src/mailman/app/tests/test_inject.py
index 4e08ff0a7..7a47ed74a 100644
--- a/src/mailman/app/tests/test_inject.py
+++ b/src/mailman/app/tests/test_inject.py
@@ -55,16 +55,15 @@ Date: Tue, 14 Jun 2011 21:12:00 -0400
Nothing.
""")
- # Python 2.7 has assertMultiLineEqual. Let this work without bounds.
- self.maxDiff = None
- self.eq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
+ # Let assertMultiLineEqual work without bounds.
def test_inject_message(self):
# Test basic inject_message() call.
inject_message(self.mlist, self.msg)
items = get_queue_messages('in')
self.assertEqual(len(items), 1)
- self.eq(items[0].msg.as_string(), self.msg.as_string())
+ self.assertMultiLineEqual(items[0].msg.as_string(),
+ self.msg.as_string())
self.assertEqual(items[0].msgdata['listname'], 'test@example.com')
self.assertEqual(items[0].msgdata['original_size'],
len(self.msg.as_string()))
@@ -83,7 +82,8 @@ Nothing.
self.assertEqual(len(items), 0)
items = get_queue_messages('virgin')
self.assertEqual(len(items), 1)
- self.eq(items[0].msg.as_string(), self.msg.as_string())
+ self.assertMultiLineEqual(items[0].msg.as_string(),
+ self.msg.as_string())
self.assertEqual(items[0].msgdata['listname'], 'test@example.com')
self.assertEqual(items[0].msgdata['original_size'],
len(self.msg.as_string()))
@@ -155,7 +155,6 @@ Nothing.
"""
# Python 2.7 has a better equality tester for message texts.
self.maxDiff = None
- self.eq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
def _remove_line(self, header):
return NL.join(line for line in self.text.splitlines()
@@ -171,7 +170,7 @@ Nothing.
'GUXXQKNCHBFQAHGBFMGCME6HKZCUUH3K')
# Delete that header because it is not in the original text.
del items[0].msg['x-message-id-hash']
- self.eq(items[0].msg.as_string(), self.text)
+ self.assertMultiLineEqual(items[0].msg.as_string(), self.text)
self.assertEqual(items[0].msgdata['listname'], 'test@example.com')
self.assertEqual(items[0].msgdata['original_size'],
# Add back the X-Message-ID-Header which was in the
@@ -196,7 +195,7 @@ Nothing.
self.assertEqual(len(items), 1)
# Remove the X-Message-ID-Hash header which isn't in the original text.
del items[0].msg['x-message-id-hash']
- self.eq(items[0].msg.as_string(), self.text)
+ self.assertMultiLineEqual(items[0].msg.as_string(), self.text)
self.assertEqual(items[0].msgdata['listname'], 'test@example.com')
self.assertEqual(items[0].msgdata['original_size'],
# Add back the X-Message-ID-Header which was in the
diff --git a/src/mailman/app/tests/test_moderation.py b/src/mailman/app/tests/test_moderation.py
index ef6adf5ed..dc1217d67 100644
--- a/src/mailman/app/tests/test_moderation.py
+++ b/src/mailman/app/tests/test_moderation.py
@@ -58,9 +58,6 @@ Message-ID: <alpha>
self._in = make_testable_runner(IncomingRunner, 'in')
self._pipeline = make_testable_runner(PipelineRunner, 'pipeline')
self._out = make_testable_runner(OutgoingRunner, 'out')
- # Python 2.7 has assertMultiLineEqual. Let this work without bounds.
- self.maxDiff = None
- self.eq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
def test_accepted_message_gets_posted(self):
# A message that is accepted by the moderator should get posted to the
diff --git a/src/mailman/app/tests/test_notifications.py b/src/mailman/app/tests/test_notifications.py
index 8cce1be6f..303fa020a 100644
--- a/src/mailman/app/tests/test_notifications.py
+++ b/src/mailman/app/tests/test_notifications.py
@@ -74,9 +74,8 @@ Welcome to the $list_name mailing list.
os.makedirs(path)
with open(os.path.join(path, 'welcome.txt'), 'w') as fp:
print('You just joined the $list_name mailing list!', file=fp)
- # Python 2.7 has assertMultiLineEqual. Let this work without bounds.
+ # Let assertMultiLineEqual work without bounds.
self.maxDiff = None
- self.eq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
def tearDown(self):
config.pop('template config')
@@ -94,7 +93,7 @@ Welcome to the $list_name mailing list.
message = messages[0].msg
self.assertEqual(str(message['subject']),
'Welcome to the "Test List" mailing list')
- self.eq(message.get_payload(), """\
+ self.assertMultiLineEqual(message.get_payload(), """\
Welcome to the Test List mailing list.
Posting address: test@example.com
diff --git a/src/mailman/archiving/mailarchive.py b/src/mailman/archiving/mailarchive.py
index e61683a09..bbd3d5ce0 100644
--- a/src/mailman/archiving/mailarchive.py
+++ b/src/mailman/archiving/mailarchive.py
@@ -30,6 +30,7 @@ from urlparse import urljoin
from zope.interface import implementer
from mailman.config import config
+from mailman.config.config import external_configuration
from mailman.interfaces.archiver import ArchivePolicy, IArchiver
@@ -43,16 +44,20 @@ class MailArchive:
name = 'mail-archive'
- @staticmethod
- def list_url(mlist):
+ def __init__(self):
+ # Read our specific configuration file
+ archiver_config = external_configuration(
+ config.archiver.mail_archive.configuration)
+ self.base_url = archiver_config.get('general', 'base_url')
+ self.recipient = archiver_config.get('general', 'recipient')
+
+ def list_url(self, mlist):
"""See `IArchiver`."""
if mlist.archive_policy is ArchivePolicy.public:
- return urljoin(config.archiver.mail_archive.base_url,
- quote(mlist.posting_address))
+ return urljoin(self.base_url, quote(mlist.posting_address))
return None
- @staticmethod
- def permalink(mlist, msg):
+ def permalink(self, mlist, msg):
"""See `IArchiver`."""
if mlist.archive_policy is not ArchivePolicy.public:
return None
@@ -62,13 +67,12 @@ class MailArchive:
message_id_hash = msg.get('x-message-id-hash')
if message_id_hash is None:
return None
- return urljoin(config.archiver.mail_archive.base_url, message_id_hash)
+ return urljoin(self.base_url, message_id_hash)
- @staticmethod
- def archive_message(mlist, msg):
+ def archive_message(self, mlist, msg):
"""See `IArchiver`."""
if mlist.archive_policy is ArchivePolicy.public:
config.switchboards['out'].enqueue(
msg,
listname=mlist.fqdn_listname,
- recipients=[config.archiver.mail_archive.recipient])
+ recipients=[self.recipient])
diff --git a/src/mailman/archiving/mhonarc.py b/src/mailman/archiving/mhonarc.py
index 7f0af6cd6..06119ae35 100644
--- a/src/mailman/archiving/mhonarc.py
+++ b/src/mailman/archiving/mhonarc.py
@@ -32,6 +32,7 @@ from urlparse import urljoin
from zope.interface import implementer
from mailman.config import config
+from mailman.config.config import external_configuration
from mailman.interfaces.archiver import IArchiver
from mailman.utilities.string import expand
@@ -46,18 +47,23 @@ class MHonArc:
name = 'mhonarc'
- @staticmethod
- def list_url(mlist):
+ def __init__(self):
+ # Read our specific configuration file
+ archiver_config = external_configuration(
+ config.archiver.mhonarc.configuration)
+ self.base_url = archiver_config.get('general', 'base_url')
+ self.command = archiver_config.get('general', 'command')
+
+ def list_url(self, mlist):
"""See `IArchiver`."""
# XXX What about private MHonArc archives?
- return expand(config.archiver.mhonarc.base_url,
+ return expand(self.base_url,
dict(listname=mlist.fqdn_listname,
hostname=mlist.domain.url_host,
fqdn_listname=mlist.fqdn_listname,
))
- @staticmethod
- def permalink(mlist, msg):
+ def permalink(self, mlist, msg):
"""See `IArchiver`."""
# XXX What about private MHonArc archives?
# It is the LMTP server's responsibility to ensure that the message
@@ -66,14 +72,13 @@ class MHonArc:
message_id_hash = msg.get('x-message-id-hash')
if message_id_hash is None:
return None
- return urljoin(MHonArc.list_url(mlist), message_id_hash)
+ return urljoin(self.list_url(mlist), message_id_hash)
- @staticmethod
- def archive_message(mlist, msg):
+ def archive_message(self, mlist, msg):
"""See `IArchiver`."""
substitutions = config.__dict__.copy()
substitutions['listname'] = mlist.fqdn_listname
- command = expand(config.archiver.mhonarc.command, substitutions)
+ command = expand(self.command, substitutions)
proc = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=True)
diff --git a/src/mailman/chains/tests/test_hold.py b/src/mailman/chains/tests/test_hold.py
index 515894505..66f43fa60 100644
--- a/src/mailman/chains/tests/test_hold.py
+++ b/src/mailman/chains/tests/test_hold.py
@@ -44,9 +44,8 @@ class TestAutorespond(unittest.TestCase):
def setUp(self):
self._mlist = create_list('test@example.com')
- # Python 2.7 has assertMultiLineEqual. Let this work without bounds.
+ # Let assertMultiLineEqual work without bounds.
self.maxDiff = None
- self.eq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
@configuration('mta', max_autoresponses_per_day=1)
def test_max_autoresponses_per_day(self):
@@ -71,7 +70,7 @@ class TestAutorespond(unittest.TestCase):
del message['message-id']
self.assertTrue('date' in message)
del message['date']
- self.eq(messages[0].msg.as_string(), """\
+ self.assertMultiLineEqual(messages[0].msg.as_string(), """\
MIME-Version: 1.0
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
diff --git a/src/mailman/commands/docs/aliases.rst b/src/mailman/commands/docs/aliases.rst
index 89843c8ad..713064b0f 100644
--- a/src/mailman/commands/docs/aliases.rst
+++ b/src/mailman/commands/docs/aliases.rst
@@ -26,7 +26,6 @@ generation.
... incoming: mailman.mta.postfix.LMTP
... lmtp_host: lmtp.example.com
... lmtp_port: 24
- ... postfix_map_cmd: true
... """)
Let's create a mailing list and then display the transport map for it. We'll
diff --git a/src/mailman/config/config.py b/src/mailman/config/config.py
index f6c39fcec..0293dacd4 100644
--- a/src/mailman/config/config.py
+++ b/src/mailman/config/config.py
@@ -22,14 +22,17 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'Configuration',
+ 'external_configuration',
+ 'load_external'
]
import os
import sys
+from ConfigParser import SafeConfigParser
from lazr.config import ConfigSchema, as_boolean
-from pkg_resources import resource_stream
+from pkg_resources import resource_filename, resource_stream, resource_string
from string import Template
from zope.component import getUtility
from zope.event import notify
@@ -39,7 +42,7 @@ import mailman.templates
from mailman import version
from mailman.interfaces.configuration import (
- ConfigurationUpdatedEvent, IConfiguration)
+ ConfigurationUpdatedEvent, IConfiguration, MissingConfigurationFileError)
from mailman.interfaces.languages import ILanguageManager
from mailman.utilities.filesystem import makedirs
from mailman.utilities.modules import call_name
@@ -235,3 +238,55 @@ class Configuration:
"""Iterate over all the language configuration sections."""
for section in self._config.getByCategory('language', []):
yield section
+
+
+
+def load_external(path, encoding=None):
+ """Load the configuration file named by path.
+
+ :param path: A string naming the location of the external configuration
+ file. This is either an absolute file system path or a special
+ ``python:`` path. When path begins with ``python:``, the rest of the
+ value must name a ``.cfg`` file located within Python's import path,
+ however the trailing ``.cfg`` suffix is implied (don't provide it
+ here).
+ :param encoding: The encoding to apply to the data read from path. If
+ None, then bytes will be returned.
+ :return: A unicode string or bytes, depending on ``encoding``.
+ """
+ # Is the context coming from a file system or Python path?
+ if path.startswith('python:'):
+ resource_path = path[7:]
+ package, dot, resource = resource_path.rpartition('.')
+ config_string = resource_string(package, resource + '.cfg')
+ else:
+ with open(path, 'rb') as fp:
+ config_string = fp.read()
+ if encoding is None:
+ return config_string
+ return config_string.decode(encoding)
+
+
+def external_configuration(path):
+ """Parse the configuration file named by path.
+
+ :param path: A string naming the location of the external configuration
+ file. This is either an absolute file system path or a special
+ ``python:`` path. When path begins with ``python:``, the rest of the
+ value must name a ``.cfg`` file located within Python's import path,
+ however the trailing ``.cfg`` suffix is implied (don't provide it
+ here).
+ :return: A `ConfigParser` instance.
+ """
+ # Is the context coming from a file system or Python path?
+ if path.startswith('python:'):
+ resource_path = path[7:]
+ package, dot, resource = resource_path.rpartition('.')
+ cfg_path = resource_filename(package, resource + '.cfg')
+ else:
+ cfg_path = path
+ parser = SafeConfigParser()
+ files = parser.read(cfg_path)
+ if files != [cfg_path]:
+ raise MissingConfigurationFileError(path)
+ return parser
diff --git a/src/mailman/config/mail_archive.cfg b/src/mailman/config/mail_archive.cfg
new file mode 100644
index 000000000..ee8430317
--- /dev/null
+++ b/src/mailman/config/mail_archive.cfg
@@ -0,0 +1,29 @@
+# Copyright (C) 2008-2012 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/>.
+
+# This is the configuration file for the mail-archive.com archiver
+
+[general]
+# The base url for the archiver. This is used to to calculate links to
+# individual messages in the archive.
+base_url: http://www.mail-archive.com/
+
+# If the archiver works by getting a copy of the message, this is the address
+# to send the copy to.
+#
+# See: http://www.mail-archive.com/faq.html#newlist
+recipient: archive@mail-archive.com
diff --git a/src/mailman/config/mhonarc.cfg b/src/mailman/config/mhonarc.cfg
new file mode 100644
index 000000000..310c3d471
--- /dev/null
+++ b/src/mailman/config/mhonarc.cfg
@@ -0,0 +1,27 @@
+# Copyright (C) 2008-2012 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/>.
+
+# This is the configuration file for the MHonArc archiver
+
+[general]
+# The base url for the archiver. This is used to to calculate links to
+# individual messages in the archive.
+base_url: http://$hostname/archives/$fqdn_listname
+
+# If the archiver works by calling a command on the local machine, this is the
+# command to call.
+command: /usr/bin/mhonarc -outdir /path/to/archive/$listname -add
diff --git a/src/mailman/config/postfix.cfg b/src/mailman/config/postfix.cfg
new file mode 100644
index 000000000..9bdba221e
--- /dev/null
+++ b/src/mailman/config/postfix.cfg
@@ -0,0 +1,8 @@
+[postfix]
+# Additional configuration variables for the postfix MTA.
+
+# This variable describe the program to use for regenerating the transport map
+# db file, from the associated plain text files. The file being updated will
+# be appended to this string (with a separating space), so it must be
+# appropriate for os.system().
+postmap_command: /usr/sbin/postmap
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index e36d33c10..37322418a 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -159,7 +159,7 @@ wait: 10s
# system paths must be absolute since no guarantees are made about the current
# working directory. Python paths should not include the trailing .cfg, which
# the file must end with.
-path: python:mailman.config.passlib
+configuration: python:mailman.config.passlib
# When Mailman generates them, this is the default length of passwords.
password_length: 8
@@ -513,11 +513,13 @@ max_autoresponses_per_day: 10
# may wish to remove these headers by setting this to 'yes'.
remove_dkim_headers: no
-# This variable describe the program to use for regenerating the transport map
-# db file, from the associated plain text files. The file being updated will
-# be appended to this string (with a separating space), so it must be
-# appropriate for os.system().
-postfix_map_cmd: /usr/sbin/postmap
+# Where can we find the mail server specific configuration file? The path can
+# be either a file system path or a Python import path. If the value starts
+# with python: then it is a Python import path, otherwise it is a file system
+# path. File system paths must be absolute since no guarantees are made about
+# the current working directory. Python paths should not include the trailing
+# .cfg, which the file must end with.
+configuration: python:mailman.config.postfix
[bounces]
@@ -535,17 +537,13 @@ class: mailman.archiving.prototype.Prototype
# Set this to 'yes' to enable the archiver.
enable: no
-# The base url for the archiver. This is used to to calculate links to
-# individual messages in the archive.
-base_url: http://archive.example.com/
-
-# If the archiver works by getting a copy of the message, this is the address
-# to send the copy to.
-recipient: archive@archive.example.com
-
-# If the archiver works by calling a command on the local machine, this is the
-# command to call.
-command: /bin/echo
+# Additional configuration for the archiver. The path can be either a file
+# system path or a Python import path. If the value starts with python: then
+# it is a Python import path, otherwise it is a file system path. File system
+# paths must be absolute since no guarantees are made about the current
+# working directory. Python paths should not include the trailing .cfg, which
+# the file must end with.
+configuration: changeme
# When sending the message to the archiver, you have the option of
# "clobbering" the Date: header, specifically to make it more sane. Some
@@ -567,14 +565,12 @@ clobber_skew: 1d
[archiver.mhonarc]
# This is the stock MHonArc archiver.
class: mailman.archiving.mhonarc.MHonArc
-
-base_url: http://$hostname/archives/$fqdn_listname
-
+configuration: python:mailman.config.mhonarc
[archiver.mail_archive]
# This is the stock mail-archive.com archiver.
class: mailman.archiving.mailarchive.MailArchive
-
+configuration: python:mailman.config.mail_archive
[archiver.prototype]
# This is a prototypical sample archiver.
diff --git a/src/mailman/config/tests/test_configuration.py b/src/mailman/config/tests/test_configuration.py
index 88e00cbb9..08f27c094 100644
--- a/src/mailman/config/tests/test_configuration.py
+++ b/src/mailman/config/tests/test_configuration.py
@@ -22,12 +22,17 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'TestConfiguration',
+ 'TestExternal',
]
import unittest
-from mailman.interfaces.configuration import ConfigurationUpdatedEvent
+from pkg_resources import resource_filename
+
+from mailman.config.config import external_configuration, load_external
+from mailman.interfaces.configuration import (
+ ConfigurationUpdatedEvent, MissingConfigurationFileError)
from mailman.testing.helpers import configuration, event_subscribers
from mailman.testing.layers import ConfigLayer
@@ -51,3 +56,46 @@ class TestConfiguration(unittest.TestCase):
# for the push leaving 'my test' on the top of the stack, and one for
# the pop, leaving the ConfigLayer's 'test config' on top.
self.assertEqual(events, ['my test', 'test config'])
+
+
+
+class TestExternal(unittest.TestCase):
+ """Test external configuration file loading APIs."""
+
+ def test_load_external_by_filename_as_bytes(self):
+ filename = resource_filename('mailman.config', 'postfix.cfg')
+ contents = load_external(filename)
+ self.assertIsInstance(contents, bytes)
+ self.assertEqual(contents[:9], b'[postfix]')
+
+ def test_load_external_by_path_as_bytes(self):
+ contents = load_external('python:mailman.config.postfix')
+ self.assertIsInstance(contents, bytes)
+ self.assertEqual(contents[:9], b'[postfix]')
+
+ def test_load_external_by_filename_as_string(self):
+ filename = resource_filename('mailman.config', 'postfix.cfg')
+ contents = load_external(filename, encoding='utf-8')
+ self.assertIsInstance(contents, unicode)
+ self.assertEqual(contents[:9], '[postfix]')
+
+ def test_load_external_by_path_as_string(self):
+ contents = load_external('python:mailman.config.postfix', 'utf-8')
+ self.assertIsInstance(contents, unicode)
+ self.assertEqual(contents[:9], '[postfix]')
+
+ def test_external_configuration_by_filename(self):
+ filename = resource_filename('mailman.config', 'postfix.cfg')
+ parser = external_configuration(filename)
+ self.assertEqual(parser.get('postfix', 'postmap_command'),
+ '/usr/sbin/postmap')
+
+ def test_external_configuration_by_path(self):
+ parser = external_configuration('python:mailman.config.postfix')
+ self.assertEqual(parser.get('postfix', 'postmap_command'),
+ '/usr/sbin/postmap')
+
+ def test_missing_configuration_file(self):
+ with self.assertRaises(MissingConfigurationFileError) as cm:
+ external_configuration('path:mailman.config.missing')
+ self.assertEqual(cm.exception.path, 'path:mailman.config.missing')
diff --git a/src/mailman/docs/ACKNOWLEDGMENTS.rst b/src/mailman/docs/ACKNOWLEDGMENTS.rst
index 10d4bcd93..c0ef13a22 100644
--- a/src/mailman/docs/ACKNOWLEDGMENTS.rst
+++ b/src/mailman/docs/ACKNOWLEDGMENTS.rst
@@ -101,6 +101,7 @@ left off the list!
* Stuart Bishop
* David Blomquist
* Bojan
+* Aurélien Bompard
* Søren Bondrup
* Grant Bowman
* Alessio Bragadini
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index fb02f994d..3c7580126 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -35,6 +35,14 @@ REST
unverify an address more than once, but verifying an already verified
address does not change its `.verified_on` date. (LP: #1054730)
+Configuration
+-------------
+ * `[passlib]path` configuration variable renamed to `[passlib]configuration`.
+ * Postfix-specific configurations in the `[mta]` section are moved to a
+ separate file, named by the `[mta]configuration` variable.
+ * In the new `postfix.cfg` file, `postfix_map_cmd` is renamed to
+ `postmap_command`.
+
Database
--------
* The `ban` table now uses list-ids to cross-reference the mailing list,
diff --git a/src/mailman/handlers/tests/test_mimedel.py b/src/mailman/handlers/tests/test_mimedel.py
index 74790fbf7..1f74db6ef 100644
--- a/src/mailman/handlers/tests/test_mimedel.py
+++ b/src/mailman/handlers/tests/test_mimedel.py
@@ -57,13 +57,12 @@ Subject: A disposable message
Message-ID: <ant>
""")
- # Python 2.7 has assertMultiLineEqual. Let this work without bounds.
- self.maxDiff = None
- self.eq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
config.push('dispose', """
[mailman]
site_owner: noreply@example.com
""")
+ # Let assertMultiLineEqual work without bounds.
+ self.maxDiff = None
def tearDown(self):
config.pop('dispose')
@@ -115,7 +114,7 @@ Message-ID: <ant>
# The body of the first part provides the moderators some details.
part0 = message.get_payload(0)
self.assertEqual(part0.get_content_type(), 'text/plain')
- self.eq(part0.get_payload(), """\
+ self.assertMultiLineEqual(part0.get_payload(), """\
The attached message matched the Test mailing list's content
filtering rules and was prevented from being forwarded on to the list
membership. You are receiving the only remaining copy of the discarded
diff --git a/src/mailman/interfaces/configuration.py b/src/mailman/interfaces/configuration.py
index 8c4fb52a6..901706f91 100644
--- a/src/mailman/interfaces/configuration.py
+++ b/src/mailman/interfaces/configuration.py
@@ -23,11 +23,22 @@ __metaclass__ = type
__all__ = [
'ConfigurationUpdatedEvent',
'IConfiguration',
+ 'MissingConfigurationFileError',
]
from zope.interface import Interface
+from mailman.core.errors import MailmanError
+
+
+
+class MissingConfigurationFileError(MailmanError):
+ """A named configuration file was not found."""
+
+ def __init__(self, path):
+ self.path = path
+
class IConfiguration(Interface):
diff --git a/src/mailman/mta/postfix.py b/src/mailman/mta/postfix.py
index 072581374..ecd71db32 100644
--- a/src/mailman/mta/postfix.py
+++ b/src/mailman/mta/postfix.py
@@ -34,6 +34,7 @@ from zope.component import getUtility
from zope.interface import implementer
from mailman.config import config
+from mailman.config.config import external_configuration
from mailman.interfaces.listmanager import IListManager
from mailman.interfaces.mta import (
IMailTransportAgentAliases, IMailTransportAgentLifecycle)
@@ -60,6 +61,11 @@ class _FakeList:
class LMTP:
"""Connect Mailman to Postfix via LMTP."""
+ def __init__(self):
+ # Locate and read the Postfix specific configuration file.
+ mta_config = external_configuration(config.mta.configuration)
+ self.postmap_command = mta_config.get('postfix', 'postmap_command')
+
def create(self, mlist):
"""See `IMailTransportAgentLifecycle`."""
# We can ignore the mlist argument because for LMTP delivery, we just
@@ -91,7 +97,7 @@ class LMTP:
# one files, still try the other one.
errors = []
for path in (lmtp_path, domains_path):
- command = config.mta.postfix_map_cmd + ' ' + path
+ command = self.postmap_command + ' ' + path
status = (os.system(command) >> 8) & 0xff
if status:
msg = 'command failure: %s, %s, %s'
@@ -124,7 +130,7 @@ class LMTP:
""".format(now().replace(microsecond=0)), file=fp)
for domain in sorted(by_domain):
print("""\
-# Aliases which are visible only in the @{0} domain.""".format(domain),
+# Aliases which are visible only in the @{0} domain.""".format(domain),
file=fp)
for mlist in sorted(by_domain[domain], key=sort_key):
aliases = list(utility.aliases(mlist))
diff --git a/src/mailman/mta/tests/test_aliases.py b/src/mailman/mta/tests/test_aliases.py
index cc6b677b3..e22385dcf 100644
--- a/src/mailman/mta/tests/test_aliases.py
+++ b/src/mailman/mta/tests/test_aliases.py
@@ -37,7 +37,6 @@ from mailman.app.lifecycle import create_list
from mailman.interfaces.domain import IDomainManager
from mailman.interfaces.mta import IMailTransportAgentAliases
from mailman.mta.postfix import LMTP
-from mailman.testing.helpers import configuration
from mailman.testing.layers import ConfigLayer
@@ -145,14 +144,12 @@ class TestPostfix(unittest.TestCase):
self.utility = getUtility(IMailTransportAgentAliases)
self.mlist = create_list('test@example.com')
self.postfix = LMTP()
- # Python 2.7 has assertMultiLineEqual. Let this work without bounds.
+ # Let assertMultiLineEqual work without bounds.
self.maxDiff = None
- self.eq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
def tearDown(self):
shutil.rmtree(self.tempdir)
- @configuration('mta', postfix_map_cmd='true')
def test_aliases(self):
# Test the format of the Postfix alias generator.
self.postfix.regenerate(self.tempdir)
@@ -163,13 +160,13 @@ class TestPostfix(unittest.TestCase):
# ignore the file header.
with open(os.path.join(self.tempdir, 'postfix_domains')) as fp:
contents = _strip_header(fp.read())
- self.eq(contents, """\
+ self.assertMultiLineEqual(contents, """\
example.com example.com
""")
# The lmtp file contains transport mappings to the lmtp server.
with open(os.path.join(self.tempdir, 'postfix_lmtp')) as fp:
contents = _strip_header(fp.read())
- self.eq(contents, """\
+ self.assertMultiLineEqual(contents, """\
# Aliases which are visible only in the @example.com domain.
test@example.com lmtp:[127.0.0.1]:9024
test-bounces@example.com lmtp:[127.0.0.1]:9024
@@ -182,7 +179,6 @@ test-subscribe@example.com lmtp:[127.0.0.1]:9024
test-unsubscribe@example.com lmtp:[127.0.0.1]:9024
""")
- @configuration('mta', postfix_map_cmd='true')
def test_two_lists(self):
# Both lists need to show up in the aliases file. LP: #874929.
# Create a second list.
@@ -195,13 +191,13 @@ test-unsubscribe@example.com lmtp:[127.0.0.1]:9024
# entry in the relays file.
with open(os.path.join(self.tempdir, 'postfix_domains')) as fp:
contents = _strip_header(fp.read())
- self.eq(contents, """\
+ self.assertMultiLineEqual(contents, """\
example.com example.com
""")
# The transport file contains entries for both lists.
with open(os.path.join(self.tempdir, 'postfix_lmtp')) as fp:
contents = _strip_header(fp.read())
- self.eq(contents, """\
+ self.assertMultiLineEqual(contents, """\
# Aliases which are visible only in the @example.com domain.
other@example.com lmtp:[127.0.0.1]:9024
other-bounces@example.com lmtp:[127.0.0.1]:9024
@@ -224,7 +220,6 @@ test-subscribe@example.com lmtp:[127.0.0.1]:9024
test-unsubscribe@example.com lmtp:[127.0.0.1]:9024
""")
- @configuration('mta', postfix_map_cmd='true')
def test_two_lists_two_domains(self):
# Now we have two lists in two different domains. Both lists will
# show up in the postfix_lmtp file, and both domains will show up in
@@ -239,14 +234,14 @@ test-unsubscribe@example.com lmtp:[127.0.0.1]:9024
# entry in the relays file.
with open(os.path.join(self.tempdir, 'postfix_domains')) as fp:
contents = _strip_header(fp.read())
- self.eq(contents, """\
+ self.assertMultiLineEqual(contents, """\
example.com example.com
example.net example.net
""")
# The transport file contains entries for both lists.
with open(os.path.join(self.tempdir, 'postfix_lmtp')) as fp:
contents = _strip_header(fp.read())
- self.eq(contents, """\
+ self.assertMultiLineEqual(contents, """\
# Aliases which are visible only in the @example.com domain.
test@example.com lmtp:[127.0.0.1]:9024
test-bounces@example.com lmtp:[127.0.0.1]:9024
diff --git a/src/mailman/mta/tests/test_delivery.py b/src/mailman/mta/tests/test_delivery.py
index fbac0d121..c56058832 100644
--- a/src/mailman/mta/tests/test_delivery.py
+++ b/src/mailman/mta/tests/test_delivery.py
@@ -95,9 +95,8 @@ options : $user_optionsurl
template_dir: {0}
""".format(self._template_dir))
self._mlist.footer_uri = 'mailman:///member-footer.txt'
- # Python 2.7 has assertMultiLineEqual. Let this work without bounds.
+ # Let assertMultiLineEqual work without bounds.
self.maxDiff = None
- self.eq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
def tearDown(self):
# Free references.
@@ -124,7 +123,7 @@ options : $user_optionsurl
self.assertEqual(len(refused), 0)
self.assertEqual(len(_deliveries), 1)
_mlist, _msg, _msgdata, _recipients = _deliveries[0]
- self.eq(_msg.as_string(), """\
+ self.assertMultiLineEqual(_msg.as_string(), """\
From: anne@example.org
To: test@example.com
Subject: test
diff --git a/src/mailman/rules/tests/test_approved.py b/src/mailman/rules/tests/test_approved.py
index a1b8f99ac..19fa2bb06 100644
--- a/src/mailman/rules/tests/test_approved.py
+++ b/src/mailman/rules/tests/test_approved.py
@@ -462,7 +462,7 @@ schemes = roundup_plaintext, plaintext
default = plaintext
deprecated = roundup_plaintext
""", file=fp)
- with configuration('passwords', path=config_file):
+ with configuration('passwords', configuration=config_file):
self._msg['Approved'] = 'super secret'
result = self._rule.check(self._mlist, self._msg, {})
self.assertTrue(result)
@@ -485,7 +485,7 @@ schemes = roundup_plaintext, plaintext
default = plaintext
deprecated = roundup_plaintext
""", file=fp)
- with configuration('passwords', path=config_file):
+ with configuration('passwords', configuration=config_file):
self._msg['Approved'] = 'not the password'
result = self._rule.check(self._mlist, self._msg, {})
self.assertFalse(result)
diff --git a/src/mailman/runners/tests/test_owner.py b/src/mailman/runners/tests/test_owner.py
index 4ea5771be..3118ab9f4 100644
--- a/src/mailman/runners/tests/test_owner.py
+++ b/src/mailman/runners/tests/test_owner.py
@@ -74,9 +74,6 @@ class TestEmailToOwner(unittest.TestCase):
self._inq = make_testable_runner(IncomingRunner, 'in')
self._pipelineq = make_testable_runner(PipelineRunner, 'pipeline')
self._outq = make_testable_runner(OutgoingRunner, 'out')
- # Python 2.7 has assertMultiLineEqual. Let this work without bounds.
- self.maxDiff = None
- self.eq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
def test_owners_get_email(self):
# XXX 2012-03-23 BAW: We can't use a layer here because we need both
diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
index 3a3e1f684..99450d143 100644
--- a/src/mailman/testing/layers.py
+++ b/src/mailman/testing/layers.py
@@ -114,14 +114,25 @@ class ConfigLayer(MockAndMonkeyLayer):
# runners) we'll need a file that we can specify to the with the -C
# option. Craft the full test configuration string here, push it, and
# also write it out to a temp file for -C.
+ #
+ # Create a dummy postfix.cfg file so that the test suite doesn't try
+ # to run the actual postmap command, which may not exist anyway.
+ postfix_cfg = os.path.join(cls.var_dir, 'postfix.cfg')
+ with open(postfix_cfg, 'w') as fp:
+ print(dedent("""
+ [postfix]
+ postmap_command: true
+ """), file=fp)
test_config = dedent("""
[mailman]
layout: testing
[paths.testing]
- var_dir: %s
+ var_dir: {0}
[devmode]
testing: yes
- """ % cls.var_dir)
+ [mta]
+ configuration: {1}
+ """.format(cls.var_dir, postfix_cfg))
# Read the testing config and push it.
test_config += resource_string('mailman.testing', 'testing.cfg')
config.create_paths = True
diff --git a/src/mailman/testing/mail_archive.cfg b/src/mailman/testing/mail_archive.cfg
new file mode 100644
index 000000000..0011781ba
--- /dev/null
+++ b/src/mailman/testing/mail_archive.cfg
@@ -0,0 +1,4 @@
+[general]
+base_url: http://go.mail-archive.dev/
+
+recipient: archive@mail-archive.dev
diff --git a/src/mailman/testing/mhonarc.cfg b/src/mailman/testing/mhonarc.cfg
new file mode 100644
index 000000000..11a15eef3
--- /dev/null
+++ b/src/mailman/testing/mhonarc.cfg
@@ -0,0 +1,4 @@
+[general]
+base_url: http://$hostname/archives/$fqdn_listname
+
+command: /bin/echo "/usr/bin/mhonarc -add -dbfile $PRIVATE_ARCHIVE_FILE_DIR/${listname}.mbox/mhonarc.db -outdir $VAR_DIR/mhonarc/${listname} -stderr $LOG_DIR/mhonarc -stdout $LOG_DIR/mhonarc -spammode -umask 022"
diff --git a/src/mailman/testing/testing.cfg b/src/mailman/testing/testing.cfg
index 141d74a8f..85b284cfe 100644
--- a/src/mailman/testing/testing.cfg
+++ b/src/mailman/testing/testing.cfg
@@ -31,7 +31,7 @@ lmtp_port: 9024
incoming: mailman.testing.mta.FakeMTA
[passwords]
-path: python:mailman.testing.passlib
+configuration: python:mailman.testing.passlib
[webservice]
port: 9001
@@ -74,12 +74,11 @@ enable: yes
[archiver.mail_archive]
enable: yes
-base_url: http://go.mail-archive.dev/
-recipient: archive@mail-archive.dev
+configuration: python:mailman.testing.mail_archive
[archiver.mhonarc]
enable: yes
-command: /bin/echo "/usr/bin/mhonarc -add -dbfile $PRIVATE_ARCHIVE_FILE_DIR/${listname}.mbox/mhonarc.db -outdir $VAR_DIR/mhonarc/${listname} -stderr $LOG_DIR/mhonarc -stdout $LOG_DIR/mhonarc -spammode -umask 022"
+configuration: python:mailman.testing.mhonarc
[language.ja]
description: Japanese
diff --git a/src/mailman/utilities/passwords.py b/src/mailman/utilities/passwords.py
index 95c85c47a..cf08260fa 100644
--- a/src/mailman/utilities/passwords.py
+++ b/src/mailman/utilities/passwords.py
@@ -27,22 +27,15 @@ __all__ = [
from passlib.context import CryptContext
-from pkg_resources import resource_string
+from mailman.config.config import load_external
from mailman.interfaces.configuration import ConfigurationUpdatedEvent
class PasswordContext:
def __init__(self, config):
- # Is the context coming from a file system or Python path?
- if config.passwords.path.startswith('python:'):
- resource_path = config.passwords.path[7:]
- package, dot, resource = resource_path.rpartition('.')
- config_string = resource_string(package, resource + '.cfg')
- else:
- with open(config.passwords.path, 'rb') as fp:
- config_string = fp.read()
+ config_string = load_external(config.passwords.configuration)
self._context = CryptContext.from_string(config_string)
def encrypt(self, secret):
diff --git a/src/mailman/utilities/tests/test_passwords.py b/src/mailman/utilities/tests/test_passwords.py
index 7b2931855..22ef22757 100644
--- a/src/mailman/utilities/tests/test_passwords.py
+++ b/src/mailman/utilities/tests/test_passwords.py
@@ -55,6 +55,6 @@ class TestPasswords(unittest.TestCase):
[passlib]
schemes = plaintext
""", file=fp)
- with configuration('passwords', path=config_file):
+ with configuration('passwords', configuration=config_file):
self.assertEqual(config.password_context.encrypt('my password'),
'my password')