# Copyright (C) 2001-2015 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 . """Creation/deletion hooks for the Postfix MTA.""" __all__ = [ 'LMTP', ] import os import logging from flufl.lock import Lock 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) from mailman.utilities.datetime import now from operator import attrgetter from zope.component import getUtility from zope.interface import implementer log = logging.getLogger('mailman.error') ALIASTMPL = '{0:{2}}lmtp:[{1.mta.lmtp_host}]:{1.mta.lmtp_port}' NL = '\n' class _FakeList: """Duck-typed list for the `IMailTransportAgentAliases` interface.""" def __init__(self, list_name, mail_host): self.list_name = list_name self.mail_host = mail_host self.posting_address = '{0}@{1}'.format(list_name, mail_host) @implementer(IMailTransportAgentLifecycle) 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 # generate the entire file every time. self.regenerate() delete = create 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): 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(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(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 = self.postmap_command + ' ' + 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 _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) print("""\ # AUTOMATICALLY GENERATED BY MAILMAN ON {0} # # This file is generated by Mailman, and is kept in sync with the binary hash # 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) for domain in sorted(by_domain): print("""\ # 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)) width = max(len(alias) for alias in aliases) + 3 print(ALIASTMPL.format(aliases.pop(0), config, width), file=fp) for alias in aliases: print(ALIASTMPL.format(alias, config, width), file=fp) print(file=fp) 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: domains.add(mail_host) print("""\ # AUTOMATICALLY GENERATED BY MAILMAN ON {0} # # This file is generated by Mailman, and is kept in sync with the binary hash # 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) for domain in sorted(domains): print('{0} {0}'.format(domain), file=fp) print(file=fp)