diff options
| author | Barry Warsaw | 2011-06-10 19:52:25 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2011-06-10 19:52:25 -0400 |
| commit | f8596ce463863dc6defb5dac84f5b226c45cb419 (patch) | |
| tree | 616e2f0d952d1654d3b0b60d661f5349d469acf4 /src | |
| parent | bf8b285acb8c2500e52ae2582f27513b9842de54 (diff) | |
| download | mailman-f8596ce463863dc6defb5dac84f5b226c45cb419.tar.gz mailman-f8596ce463863dc6defb5dac84f5b226c45cb419.tar.zst mailman-f8596ce463863dc6defb5dac84f5b226c45cb419.zip | |
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/commands/cli_aliases.py | 76 | ||||
| -rw-r--r-- | src/mailman/commands/docs/aliases.rst (renamed from src/mailman/commands/docs/aliases.txt) | 74 | ||||
| -rw-r--r-- | src/mailman/config/configure.zcml | 5 | ||||
| -rw-r--r-- | src/mailman/interfaces/mta.py | 22 | ||||
| -rw-r--r-- | src/mailman/mta/aliases.py | 65 | ||||
| -rw-r--r-- | src/mailman/mta/base.py | 8 | ||||
| -rw-r--r-- | src/mailman/mta/null.py | 10 | ||||
| -rw-r--r-- | src/mailman/mta/postfix.py | 53 | ||||
| -rw-r--r-- | src/mailman/mta/tests/__init__.py | 0 | ||||
| -rw-r--r-- | src/mailman/mta/tests/test_aliases.py | 128 | ||||
| -rw-r--r-- | src/mailman/testing/mta.py | 4 |
11 files changed, 393 insertions, 52 deletions
diff --git a/src/mailman/commands/cli_aliases.py b/src/mailman/commands/cli_aliases.py index dcb494ebb..b13919121 100644 --- a/src/mailman/commands/cli_aliases.py +++ b/src/mailman/commands/cli_aliases.py @@ -27,11 +27,16 @@ __all__ = [ import sys +from operator import attrgetter +from zope.component import getUtility from zope.interface import implements from mailman.config import config from mailman.core.i18n import _ from mailman.interfaces.command import ICLISubCommand +from mailman.interfaces.listmanager import IListManager +from mailman.interfaces.mta import ( + IMailTransportAgentAliases, IMailTransportAgentLifecycle) from mailman.utilities.modules import call_name @@ -45,14 +50,29 @@ class Aliases: def add(self, parser, command_parser): """See `ICLISubCommand`.""" + self.parser = parser command_parser.add_argument( '-o', '--output', action='store', help=_("""\ File to send the output to. If not given, a file in $VAR/data is used. The argument can be '-' to use standard output..""")) + command_parser.add_argument( + '-f', '--format', + action='store', help=_("""\ + Alternative output format to use. This is the Python object path + to an implementation of the `IMailTransportAgentLifecycle` + interface.""")) + command_parser.add_argument( + '-s', '--simple', + action='store_true', default=False, help=_("""\ + Simply output the list of aliases. + """)) def process(self, args): """See `ICLISubCommand`.""" + if args.format is not None and args.simple: + self.parser.error(_('Cannot use both -s and -f')) + # Does not return. output = None if args.output == '-': output = sys.stdout @@ -60,5 +80,57 @@ class Aliases: output = None else: output = args.output - # Call the MTA-specific regeneration method. - call_name(config.mta.incoming).regenerate(output) + if args.simple: + Dummy().regenerate(output) + else: + format_arg = (config.mta.incoming + if args.format is None + else args.format) + # Call the MTA-specific regeneration method. + call_name(format_arg).regenerate(output) + + + +class Dummy: + """Dummy aliases implementation for simpler output format.""" + + implements(IMailTransportAgentLifecycle) + + def create(self, mlist): + """See `IMailTransportAgentLifecycle`.""" + raise NotImplementedError + + def delete(self, mlist): + """See `IMailTransportAgentLifecycle`.""" + raise NotImplementedError + + def regenerate(self, output=None): + """See `IMailTransportAgentLifecycle`.""" + fp = None + close = False + try: + if output is None: + # There's really no place to print the output. + return + elif isinstance(output, basestring): + fp = open(output, 'w') + close = True + else: + fp = output + self._do_write_file(fp) + finally: + if fp is not None and close: + fp.close() + + def _do_write_file(self, fp): + # First, sort mailing lists by domain. + by_domain = {} + for mlist in getUtility(IListManager).mailing_lists: + by_domain.setdefault(mlist.host_name, []).append(mlist) + sort_key = attrgetter('list_name') + for domain in sorted(by_domain): + for mlist in sorted(by_domain[domain], key=sort_key): + utility = getUtility(IMailTransportAgentAliases) + for alias in utility.aliases(mlist): + print >> fp, alias + print >> fp diff --git a/src/mailman/commands/docs/aliases.txt b/src/mailman/commands/docs/aliases.rst index 0822ad973..c0d4c10c9 100644 --- a/src/mailman/commands/docs/aliases.txt +++ b/src/mailman/commands/docs/aliases.rst @@ -6,11 +6,12 @@ For some mail servers, Mailman must generate a data file that is used to hook Mailman up to the mail server. The details of this differ for each mail server. Generally these files are automatically kept up-to-date when mailing lists are created or removed, but you might occasionally need to manually -regenerate the file. The 'bin/mailman aliases' command does this. +regenerate the file. The ``bin/mailman aliases`` command does this. >>> class FakeArgs: ... output = None - + ... format = None + ... simple = None >>> from mailman.commands.cli_aliases import Aliases >>> command = Aliases() @@ -32,19 +33,13 @@ generation. Let's create a mailing list and then display the transport map for it. We'll send the output to stdout. +:: >>> FakeArgs.output = '-' >>> mlist = create_list('test@example.com') >>> command.process(FakeArgs) # AUTOMATICALLY GENERATED BY MAILMAN ON ... - # - # This file is generated by Mailman, and is kept in sync with the ... - # file. YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you know what you're - # doing, and can keep the two files properly in sync. If you screw it up, - # you're on your own. - <BLANKLINE> - # Aliases which are visible only in the @example.com domain. - <BLANKLINE> + ... test@example.com lmtp:[lmtp.example.com]:24 test-bounces@example.com lmtp:[lmtp.example.com]:24 test-confirm@example.com lmtp:[lmtp.example.com]:24 @@ -56,8 +51,61 @@ send the output to stdout. test-unsubscribe@example.com lmtp:[lmtp.example.com]:24 <BLANKLINE> + >>> config.pop('postfix') -Clean up -======== - >>> config.pop('postfix') +Alternative output +================== + +By using a command line switch, we can select a different output format. The +option must point to an alternative implementation of the +``IMailTransportAgentAliases`` interface. + +Mailman comes with an alternative implementation that just prints the aliases, +with no adornment. + + >>> FakeArgs.format = 'mailman.commands.cli_aliases.Dummy' + >>> command.process(FakeArgs) + test@example.com + test-bounces@example.com + test-confirm@example.com + test-join@example.com + test-leave@example.com + test-owner@example.com + test-request@example.com + test-subscribe@example.com + test-unsubscribe@example.com + <BLANKLINE> + +A simpler way of getting the same output is with the ``--simple`` flag. + + >>> FakeArgs.format = None + >>> FakeArgs.simple = True + >>> command.process(FakeArgs) + test@example.com + test-bounces@example.com + test-confirm@example.com + test-join@example.com + test-leave@example.com + test-owner@example.com + test-request@example.com + test-subscribe@example.com + test-unsubscribe@example.com + <BLANKLINE> + + +Mutually exclusive arguments +============================ + +You cannot use both ``--simple`` and ``--format``. + + >>> FakeArgs.format = 'mailman.commands.cli_aliases.Dummy' + >>> FakeArgs.simple = True + >>> class Parser: + ... def error(self, message): + ... raise RuntimeError(message) + >>> command.parser = Parser() + >>> command.process(FakeArgs) + Traceback (most recent call last): + ... + RuntimeError: Cannot use both -s and -f diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml index 299a0ce67..f34076b78 100644 --- a/src/mailman/config/configure.zcml +++ b/src/mailman/config/configure.zcml @@ -42,6 +42,11 @@ /> <utility + factory="mailman.mta.aliases.MailTransportAgentAliases" + provides="mailman.interfaces.mta.IMailTransportAgentAliases" + /> + + <utility factory="mailman.model.messagestore.MessageStore" provides="mailman.interfaces.messages.IMessageStore" /> diff --git a/src/mailman/interfaces/mta.py b/src/mailman/interfaces/mta.py index 2aeb65172..0e6fd0938 100644 --- a/src/mailman/interfaces/mta.py +++ b/src/mailman/interfaces/mta.py @@ -23,6 +23,7 @@ __metaclass__ = type __all__ = [ 'IMailTransportAgentAliases', 'IMailTransportAgentDelivery', + 'IMailTransportAgentLifecycle', ] @@ -42,7 +43,26 @@ class SomeRecipientsFailed(MailmanError): class IMailTransportAgentAliases(Interface): - """Interface to the MTA aliases generator.""" + """Interface to the MTA utility for generating all the aliases.""" + + def aliases(mlist): + """Generate all the aliases for the mailing list. + + This method is a generator. The posting address will be returned + first, followed by the rest of the aliases in alphabetical order. + """ + + def destinations(mlist): + """Generate just the local parts for the mailing list aliases. + + This method is a generator. The posting address will be returned + first, followed by the rest of the aliases in alphabetical order. + """ + + + +class IMailTransportAgentLifecycle(Interface): + """Interface to the MTA for creating and deleting a mailing list.""" def create(mlist): """Tell the MTA that the mailing list was created.""" diff --git a/src/mailman/mta/aliases.py b/src/mailman/mta/aliases.py new file mode 100644 index 000000000..9f2bd74e3 --- /dev/null +++ b/src/mailman/mta/aliases.py @@ -0,0 +1,65 @@ +# Copyright (C) 2011 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/>. + +"""Utility for generating all the aliases of a mailing list.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'MailTransportAgentAliases', + ] + + +from zope.interface import implements + +from mailman.interfaces.mta import IMailTransportAgentAliases + + +SUBDESTINATIONS = ( + 'bounces', + 'confirm', + 'join', + 'leave', + 'owner', + 'request', + 'subscribe', + 'unsubscribe', + ) + + + +class MailTransportAgentAliases: + """Utility for generating all the aliases of a mailing list.""" + + implements(IMailTransportAgentAliases) + + def aliases(self, mlist): + """See `IMailTransportAgentAliases`.""" + # Always return + yield mlist.posting_address + for destination in sorted(SUBDESTINATIONS): + yield '{0}-{1}@{2}'.format(mlist.list_name, + destination, + mlist.host_name) + + def destinations(self, mlist): + """See `IMailTransportAgentAliases`.""" + # Always return + yield mlist.list_name + for destination in sorted(SUBDESTINATIONS): + yield '{0}-{1}'.format(mlist.list_name, destination) diff --git a/src/mailman/mta/base.py b/src/mailman/mta/base.py index e90fbcf8f..69b1d9c5c 100644 --- a/src/mailman/mta/base.py +++ b/src/mailman/mta/base.py @@ -21,6 +21,7 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ + 'BaseAliases', 'BaseDelivery', 'IndividualDelivery', ] @@ -168,3 +169,10 @@ class IndividualDelivery(BaseDelivery): mlist, message_copy, msgdata_copy, [recipient]) refused.update(status) return refused + + + +class BaseAliases: + """Common alias generation; use as a mixin.""" + + diff --git a/src/mailman/mta/null.py b/src/mailman/mta/null.py index 0afda5fec..c69fb3207 100644 --- a/src/mailman/mta/null.py +++ b/src/mailman/mta/null.py @@ -29,23 +29,23 @@ __all__ = [ from zope.interface import implements -from mailman.interfaces.mta import IMailTransportAgentAliases +from mailman.interfaces.mta import IMailTransportAgentLifecycle class NullMTA: """Null MTA that just satisfies the interface.""" - implements(IMailTransportAgentAliases) + implements(IMailTransportAgentLifecycle) def create(self, mlist): - """See `IMailTransportAgentAliases`.""" + """See `IMailTransportAgentLifecycle`.""" pass def delete(self, mlist): - """See `IMailTransportAgentAliases`.""" + """See `IMailTransportAgentLifecycle`.""" pass def regenerate(self, output=None): - """See `IMailTransportAgentAliases`.""" + """See `IMailTransportAgentLifecycle`.""" pass diff --git a/src/mailman/mta/postfix.py b/src/mailman/mta/postfix.py index 81a2a7945..a4fe0f331 100644 --- a/src/mailman/mta/postfix.py +++ b/src/mailman/mta/postfix.py @@ -30,30 +30,28 @@ import logging import datetime from flufl.lock import Lock +from operator import attrgetter from zope.component import getUtility from zope.interface import implements from mailman.config import config from mailman.interfaces.listmanager import IListManager -from mailman.interfaces.mta import IMailTransportAgentAliases +from mailman.interfaces.mta import ( + IMailTransportAgentAliases, IMailTransportAgentLifecycle) -log = logging.getLogger('mailman.error') -LOCKFILE = os.path.join(config.LOCK_DIR, 'mta') -SUBDESTINATIONS = ( - 'bounces', 'confirm', 'join', 'leave', - 'owner', 'request', 'subscribe', 'unsubscribe', - ) +log = logging.getLogger('mailman.error') +ALIASTMPL = '{0:{2}}lmtp:[{1.mta.lmtp_host}]:{1.mta.lmtp_port}' class LMTP: """Connect Mailman to Postfix via LMTP.""" - implements(IMailTransportAgentAliases) + implements(IMailTransportAgentLifecycle) def create(self, mlist): - """See `IMailTransportAgentAliases`.""" + """See `IMailTransportAgentLifecycle`.""" # We can ignore the mlist argument because for LMTP delivery, we just # generate the entire file every time. self.regenerate() @@ -61,13 +59,14 @@ class LMTP: delete = create def regenerate(self, output=None): - """See `IMailTransportAgentAliases`. + """See `IMailTransportAgentLifecycle`. The format for Postfix's LMTP transport map is defined here: http://www.postfix.org/transport.5.html """ # Acquire a lock file to prevent other processes from racing us here. - with Lock(LOCKFILE): + lock_file = os.path.join(config.LOCK_DIR, 'mta') + with Lock(lock_file): # If output is a filename, open up a backing file and write the # output there, then do the atomic rename dance. First though, if # it's None, we use a calculated path. @@ -104,9 +103,8 @@ class LMTP: # Sort all existing mailing list names first by domain, then my local # part. For postfix we need a dummy entry for the domain. by_domain = {} - for mailing_list in getUtility(IListManager).mailing_lists: - by_domain.setdefault(mailing_list.host_name, []).append( - mailing_list.list_name) + for mlist in getUtility(IListManager).mailing_lists: + by_domain.setdefault(mlist.host_name, []).append(mlist) print >> fp, """\ # AUTOMATICALLY GENERATED BY MAILMAN ON {0} # @@ -115,22 +113,19 @@ class LMTP: # doing, and can keep the two files properly in sync. If you screw it up, # you're on your own. """.format(datetime.datetime.now().replace(microsecond=0)) + sort_key = attrgetter('list_name') for domain in sorted(by_domain): print >> fp, """\ -# Aliases which are visible only in the @{0} domain. -""".format(domain) - for list_name in by_domain[domain]: - # Calculate the field width of the longest alias. 10 == - # len('-subscribe') + '@'. - longest = len(list_name + domain) + 10 - print >> fp, """\ -{0}@{1:{3}}lmtp:[{2.mta.lmtp_host}]:{2.mta.lmtp_port}""".format( - list_name, domain, config, +# Aliases which are visible only in the @{0} domain.""".format(domain) + for mlist in sorted(by_domain[domain], key=sort_key): + utility = getUtility(IMailTransportAgentAliases) + aliases = list(utility.aliases(mlist)) + longest = max(len(alias) for alias in aliases) + print >> fp, ALIASTMPL.format( + aliases.pop(0), config, # Add 1 because the bare list name has no dash. - longest + 1) - for destination in SUBDESTINATIONS: - print >> fp, """\ -{0}-{1}@{2:{4}}lmtp:[{3.mta.lmtp_host}]:{3.mta.lmtp_port}""".format( - list_name, destination, domain, config, - longest - len(destination)) + longest + 3) + for alias in aliases: + print >> fp, ALIASTMPL.format( + alias, config, longest + 3) print >> fp diff --git a/src/mailman/mta/tests/__init__.py b/src/mailman/mta/tests/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/mailman/mta/tests/__init__.py diff --git a/src/mailman/mta/tests/test_aliases.py b/src/mailman/mta/tests/test_aliases.py new file mode 100644 index 000000000..96207c943 --- /dev/null +++ b/src/mailman/mta/tests/test_aliases.py @@ -0,0 +1,128 @@ +# Copyright (C) 2011 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 the template generating utility.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'test_suite', + ] + + +import unittest + +from cStringIO import StringIO +from zope.component import getUtility + +from mailman.app.lifecycle import create_list +from mailman.config import config +from mailman.interfaces.mta import IMailTransportAgentAliases +from mailman.mta.postfix import LMTP +from mailman.testing.layers import ConfigLayer + +NL = '\n' + + + +class TestAliases(unittest.TestCase): + + layer = ConfigLayer + + def setUp(self): + self.utility = getUtility(IMailTransportAgentAliases) + self.mlist = create_list('test@example.com') + + def test_posting_address_first(self): + # The posting address is always first. + aliases = list(self.utility.aliases(self.mlist)) + self.assertEqual(aliases[0], self.mlist.posting_address) + + def test_aliases(self): + # The aliases are the fully qualified email addresses. + aliases = list(self.utility.aliases(self.mlist)) + self.assertEqual(aliases, [ + 'test@example.com', + 'test-bounces@example.com', + 'test-confirm@example.com', + 'test-join@example.com', + 'test-leave@example.com', + 'test-owner@example.com', + 'test-request@example.com', + 'test-subscribe@example.com', + 'test-unsubscribe@example.com', + ]) + + def test_destinations(self): + # The destinations are just the local part. + destinations = list(self.utility.destinations(self.mlist)) + self.assertEqual(destinations, [ + 'test', + 'test-bounces', + 'test-confirm', + 'test-join', + 'test-leave', + 'test-owner', + 'test-request', + 'test-subscribe', + 'test-unsubscribe', + ]) + + + +class TestPostfix(unittest.TestCase): + """Test the Postfix LMTP alias generator.""" + + layer = ConfigLayer + + def setUp(self): + self.utility = getUtility(IMailTransportAgentAliases) + self.mlist = create_list('test@example.com') + self.output = StringIO() + self.postfix = LMTP() + # For Python 2.7's unittest. + self.maxDiff = None + + def test_aliases(self): + # Test the format of the Postfix alias generator. + self.postfix.regenerate(self.output) + # Python 2.7 has assertMultiLineEqual but Python 2.6 does not. + eq = getattr(self, 'assertMultiLineEqual', self.assertEqual) + # Strip out the variable and unimportant bits of the output. + lines = self.output.getvalue().splitlines() + output = NL.join(lines[7:]) + eq(output, """\ +# 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 +test-confirm@example.com lmtp:[127.0.0.1]:9024 +test-join@example.com lmtp:[127.0.0.1]:9024 +test-leave@example.com lmtp:[127.0.0.1]:9024 +test-owner@example.com lmtp:[127.0.0.1]:9024 +test-request@example.com lmtp:[127.0.0.1]:9024 +test-subscribe@example.com lmtp:[127.0.0.1]:9024 +test-unsubscribe@example.com lmtp:[127.0.0.1]:9024 +""") + + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestAliases)) + suite.addTest(unittest.makeSuite(TestPostfix)) + return suite diff --git a/src/mailman/testing/mta.py b/src/mailman/testing/mta.py index 8fae233fa..741c0e607 100644 --- a/src/mailman/testing/mta.py +++ b/src/mailman/testing/mta.py @@ -33,7 +33,7 @@ from lazr.smtptest.controller import QueueController from lazr.smtptest.server import Channel, QueueServer from zope.interface import implements -from mailman.interfaces.mta import IMailTransportAgentAliases +from mailman.interfaces.mta import IMailTransportAgentLifecycle log = logging.getLogger('lazr.smtptest') @@ -43,7 +43,7 @@ log = logging.getLogger('lazr.smtptest') class FakeMTA: """Fake MTA for testing purposes.""" - implements(IMailTransportAgentAliases) + implements(IMailTransportAgentLifecycle) def create(self, mlist): pass |
