diff options
Diffstat (limited to 'src/mailman/mta')
| -rw-r--r-- | src/mailman/mta/postfix.py | 139 | ||||
| -rw-r--r-- | src/mailman/mta/tests/test_aliases.py | 109 |
2 files changed, 142 insertions, 106 deletions
diff --git a/src/mailman/mta/postfix.py b/src/mailman/mta/postfix.py index 4fc097ffc..072581374 100644 --- a/src/mailman/mta/postfix.py +++ b/src/mailman/mta/postfix.py @@ -42,6 +42,7 @@ from mailman.utilities.datetime import now log = logging.getLogger('mailman.error') ALIASTMPL = '{0:{2}}lmtp:[{1.mta.lmtp_host}]:{1.mta.lmtp_port}' +NL = '\n' @@ -64,96 +65,52 @@ class LMTP: # We can ignore the mlist argument because for LMTP delivery, we just # generate the entire file every time. self.regenerate() - self.regenerate_domain() delete = create - def regenerate(self, output=None): - """See `IMailTransportAgentLifecycle`. - - The format for Postfix's LMTP transport map is defined here: - http://www.postfix.org/transport.5.html - """ + def regenerate(self, directory=None): + """See `IMailTransportAgentLifecycle`.""" # Acquire a lock file to prevent other processes from racing us here. + if directory is None: + directory = config.DATA_DIR 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. - if output is None: - path = os.path.join(config.DATA_DIR, 'postfix_lmtp') - path_new = path + '.new' - elif isinstance(output, basestring): - path = output - path_new = output + '.new' - else: - path = path_new = None - if path_new is None: - self._do_write_file(output) - # There's nothing to rename, and we can't generate the .db - # file, so we're done. - return - # Write the file. - with open(path_new, 'w') as fp: - self._do_write_file(fp) + lmtp_path = os.path.join(directory, 'postfix_lmtp') + lmtp_path_new = lmtp_path + '.new' + with open(lmtp_path_new, 'w') as fp: + self._generate_lmtp_file(fp) # Atomically rename to the intended path. - os.rename(path + '.new', path) - # Now that the new file is in place, we must tell Postfix to - # generate a new .db file. - command = config.mta.postfix_map_cmd + ' ' + path - status = (os.system(command) >> 8) & 0xff - if status: - msg = 'command failure: %s, %s, %s' - errstr = os.strerror(status) - log.error(msg, command, status, errstr) - raise RuntimeError(msg % (command, status, errstr)) - - def regenerate_domain(self, output=None): - """The map for all list domains - - 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. - 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. - if output is None: - path = os.path.join(config.DATA_DIR, 'postfix_domains') - path_new = path + '.new' - elif isinstance(output, basestring): - path = output - path_new = output + '.new' - else: - path = path_new = None - if path_new is None: - self._do_write_file_domains(output) - # There's nothing to rename, and we can't generate the .db - # file, so we're done. - return - # Write the file. - with open(path_new, 'w') as fp: - self._do_write_file_domains(fp) + os.rename(lmtp_path_new, lmtp_path) + domains_path = os.path.join(directory, 'postfix_domains') + domains_path_new = domains_path + '.new' + with open(domains_path_new, 'w') as fp: + self._generate_domains_file(fp) # Atomically rename to the intended path. - os.rename(path + '.new', path) - # Now that the new file is in place, we must tell Postfix to - # generate a new .db file. - command = config.mta.postfix_map_cmd + ' ' + path - status = (os.system(command) >> 8) & 0xff - if status: - msg = 'command failure: %s, %s, %s' - errstr = os.strerror(status) - log.error(msg, command, status, errstr) - raise RuntimeError(msg % (command, status, errstr)) + os.rename(domains_path_new, domains_path) + # Now, run the postmap command on both newly generated files. If + # one files, still try the other one. + errors = [] + for path in (lmtp_path, domains_path): + command = config.mta.postfix_map_cmd + ' ' + path + status = (os.system(command) >> 8) & 0xff + if status: + msg = 'command failure: %s, %s, %s' + errstr = os.strerror(status) + log.error(msg, command, status, errstr) + errors.append(msg % (command, status, errstr)) + if errors: + raise RuntimeError(NL.join(errors)) - def _do_write_file(self, fp): - """Do the actual file writes for list creation.""" - # Sort all existing mailing list names first by domain, then by local - # part. For postfix we need a dummy entry for the domain. + def _generate_lmtp_file(self, fp): + # The format for Postfix's LMTP transport map is defined here: + # http://www.postfix.org/transport.5.html + # + # Sort all existing mailing list names first by domain, then by + # local part. For Postfix we need a dummy entry for the domain. list_manager = getUtility(IListManager) + utility = getUtility(IMailTransportAgentAliases) by_domain = {} + sort_key = attrgetter('list_name') for list_name, mail_host in list_manager.name_components: mlist = _FakeList(list_name, mail_host) by_domain.setdefault(mlist.mail_host, []).append(mlist) @@ -164,14 +121,12 @@ class LMTP: # 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. -""".format(now().replace(microsecond=0)), file=fp) - sort_key = attrgetter('list_name') + """.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), - file=fp) +# Aliases which are visible only in the @{0} domain.""".format(domain), + file=fp) for mlist in sorted(by_domain[domain], key=sort_key): - utility = getUtility(IMailTransportAgentAliases) aliases = list(utility.aliases(mlist)) width = max(len(alias) for alias in aliases) + 3 print(ALIASTMPL.format(aliases.pop(0), config, width), file=fp) @@ -179,13 +134,11 @@ class LMTP: print(ALIASTMPL.format(alias, config, width), file=fp) print(file=fp) - def _do_write_file_domains(self, fp): - """Do the actual file writes of the domain map for list creation.""" - # 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 = [] + def _generate_domains_file(self, fp): + # Uniquify the domains, then sort them alphabetically. + domains = set() for list_name, mail_host in getUtility(IListManager).name_components: - by_domain.append(mail_host) + domains.add(mail_host) print("""\ # AUTOMATICALLY GENERATED BY MAILMAN ON {0} # @@ -194,6 +147,6 @@ class LMTP: # doing, and can keep the two files properly in sync. If you screw it up, # you're on your own. """.format(now().replace(microsecond=0)), file=fp) - for domain in sorted(by_domain): - print("""{0} {0}""".format(domain), file=fp) - + for domain in sorted(domains): + print('{0} {0}'.format(domain), file=fp) + print(file=fp) diff --git a/src/mailman/mta/tests/test_aliases.py b/src/mailman/mta/tests/test_aliases.py index b1a60bc95..cc6b677b3 100644 --- a/src/mailman/mta/tests/test_aliases.py +++ b/src/mailman/mta/tests/test_aliases.py @@ -15,28 +15,40 @@ # 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.""" +"""Test the MTA file generating utility.""" from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ + 'TestAliases', + 'TestPostfix', ] +import os +import shutil +import tempfile import unittest -from cStringIO import StringIO from zope.component import getUtility 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 + NL = '\n' +def _strip_header(contents): + lines = contents.splitlines() + return NL.join(lines[7:]) + + class TestAliases(unittest.TestCase): @@ -129,21 +141,35 @@ class TestPostfix(unittest.TestCase): layer = ConfigLayer def setUp(self): + self.tempdir = tempfile.mkdtemp() self.utility = getUtility(IMailTransportAgentAliases) self.mlist = create_list('test@example.com') - self.output = StringIO() self.postfix = LMTP() # Python 2.7 has assertMultiLineEqual. Let this 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.output) - # Strip out the variable and unimportant bits of the output. - lines = self.output.getvalue().splitlines() - output = NL.join(lines[7:]) - self.eq(output, """\ + self.postfix.regenerate(self.tempdir) + # There are two files in this directory. + self.assertEqual(sorted(os.listdir(self.tempdir)), + ['postfix_domains', 'postfix_lmtp']) + # The domains file, just contains the example.com domain. We have to + # ignore the file header. + with open(os.path.join(self.tempdir, 'postfix_domains')) as fp: + contents = _strip_header(fp.read()) + self.eq(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, """\ # 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 @@ -156,15 +182,26 @@ 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. create_list('other@example.com') - self.postfix.regenerate(self.output) - # Strip out the variable and unimportant bits of the output. - lines = self.output.getvalue().splitlines() - output = NL.join(lines[7:]) - self.eq(output, """\ + self.postfix.regenerate(self.tempdir) + # There are two files in this directory. + self.assertEqual(sorted(os.listdir(self.tempdir)), + ['postfix_domains', 'postfix_lmtp']) + # Because both lists are in the same domain, there should be only one + # entry in the relays file. + with open(os.path.join(self.tempdir, 'postfix_domains')) as fp: + contents = _strip_header(fp.read()) + self.eq(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, """\ # 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 @@ -186,3 +223,49 @@ 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 """) + + @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 + # the postfix_domains file. + getUtility(IDomainManager).add('example.net') + create_list('other@example.net') + self.postfix.regenerate(self.tempdir) + # There are two files in this directory. + self.assertEqual(sorted(os.listdir(self.tempdir)), + ['postfix_domains', 'postfix_lmtp']) + # Because both lists are in the same domain, there should be only one + # entry in the relays file. + with open(os.path.join(self.tempdir, 'postfix_domains')) as fp: + contents = _strip_header(fp.read()) + self.eq(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, """\ +# 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 + +# Aliases which are visible only in the @example.net domain. +other@example.net lmtp:[127.0.0.1]:9024 +other-bounces@example.net lmtp:[127.0.0.1]:9024 +other-confirm@example.net lmtp:[127.0.0.1]:9024 +other-join@example.net lmtp:[127.0.0.1]:9024 +other-leave@example.net lmtp:[127.0.0.1]:9024 +other-owner@example.net lmtp:[127.0.0.1]:9024 +other-request@example.net lmtp:[127.0.0.1]:9024 +other-subscribe@example.net lmtp:[127.0.0.1]:9024 +other-unsubscribe@example.net lmtp:[127.0.0.1]:9024 +""") |
