summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/commands/cli_aliases.py76
-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.zcml5
-rw-r--r--src/mailman/interfaces/mta.py22
-rw-r--r--src/mailman/mta/aliases.py65
-rw-r--r--src/mailman/mta/base.py8
-rw-r--r--src/mailman/mta/null.py10
-rw-r--r--src/mailman/mta/postfix.py53
-rw-r--r--src/mailman/mta/tests/__init__.py0
-rw-r--r--src/mailman/mta/tests/test_aliases.py128
-rw-r--r--src/mailman/testing/mta.py4
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