# Copyright (C) 2001-2014 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/>.

"""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)
