# -*- test-case-name: twisted.mail.test.test_mailmail -*-
# Copyright (c) 2001-2008 Twisted Matrix Laboratories.
# See LICENSE for details.

"""
Implementation module for the I{mailmail} command.
"""

import os
import sys
import rfc822
import getpass
from ConfigParser import ConfigParser

try:
    import cStringIO as StringIO
except:
    import StringIO

from twisted.internet import reactor
from twisted.mail import smtp

GLOBAL_CFG = "/etc/mailmail"
LOCAL_CFG = os.path.expanduser("~/.twisted/mailmail")
SMARTHOST = '127.0.0.1'

ERROR_FMT = """\
Subject: Failed Message Delivery

  Message delivery failed.  The following occurred:

  %s
--
The Twisted sendmail application.
"""

def log(message, *args):
    sys.stderr.write(str(message) % args + '\n')

class Options:
    """
    @type to: C{list} of C{str}
    @ivar to: The addresses to which to deliver this message.

    @type sender: C{str}
    @ivar sender: The address from which this message is being sent.

    @type body: C{file}
    @ivar body: The object from which the message is to be read.
    """

def getlogin():
    try:
        return os.getlogin()
    except:
        return getpass.getuser()


_unsupportedOption = SystemExit("Unsupported option.")

def parseOptions(argv):
    o = Options()
    o.to = [e for e in argv if not e.startswith('-')]
    o.sender = getlogin()

    # Just be very stupid

    # Skip -bm -- it is the default

    # -bp lists queue information.  Screw that.
    if '-bp' in argv:
        raise _unsupportedOption

    # -bs makes sendmail use stdin/stdout as its transport.  Screw that.
    if '-bs' in argv:
        raise _unsupportedOption

    # -F sets who the mail is from, but is overridable by the From header
    if '-F' in argv:
        o.sender = argv[argv.index('-F') + 1]
        o.to.remove(o.sender)

    # -i and -oi makes us ignore lone "."
    if ('-i' in argv) or ('-oi' in argv):
        raise _unsupportedOption

    # -odb is background delivery
    if '-odb' in argv:
        o.background = True
    else:
        o.background = False

    # -odf is foreground delivery
    if '-odf' in argv:
        o.background = False
    else:
        o.background = True

    # -oem and -em cause errors to be mailed back to the sender.
    # It is also the default.

    # -oep and -ep cause errors to be printed to stderr
    if ('-oep' in argv) or ('-ep' in argv):
        o.printErrors = True
    else:
        o.printErrors = False

    # -om causes a copy of the message to be sent to the sender if the sender
    # appears in an alias expansion.  We do not support aliases.
    if '-om' in argv:
        raise _unsupportedOption

    # -t causes us to pick the recipients of the message from the To, Cc, and Bcc
    # headers, and to remove the Bcc header if present.
    if '-t' in argv:
        o.recipientsFromHeaders = True
        o.excludeAddresses = o.to
        o.to = []
    else:
        o.recipientsFromHeaders = False
        o.exludeAddresses = []

    requiredHeaders = {
        'from': [],
        'to': [],
        'cc': [],
        'bcc': [],
        'date': [],
    }

    headers = []
    buffer = StringIO.StringIO()
    while 1:
        write = 1
        line = sys.stdin.readline()
        if not line.strip():
            break

        hdrs = line.split(': ', 1)

        hdr = hdrs[0].lower()
        if o.recipientsFromHeaders and hdr in ('to', 'cc', 'bcc'):
            o.to.extend([
                a[1] for a in rfc822.AddressList(hdrs[1]).addresslist
            ])
            if hdr == 'bcc':
                write = 0
        elif hdr == 'from':
            o.sender = rfc822.parseaddr(hdrs[1])[1]

        if hdr in requiredHeaders:
            requiredHeaders[hdr].append(hdrs[1])

        if write:
            buffer.write(line)

    if not requiredHeaders['from']:
        buffer.write('From: %s\r\n' % (o.sender,))
    if not requiredHeaders['to']:
        if not o.to:
            raise SystemExit("No recipients specified.")
        buffer.write('To: %s\r\n' % (', '.join(o.to),))
    if not requiredHeaders['date']:
        buffer.write('Date: %s\r\n' % (smtp.rfc822date(),))

    buffer.write(line)

    if o.recipientsFromHeaders:
        for a in o.excludeAddresses:
            try:
                o.to.remove(a)
            except:
                pass

    buffer.seek(0, 0)
    o.body = StringIO.StringIO(buffer.getvalue() + sys.stdin.read())
    return o

class Configuration:
    """
    @ivar allowUIDs: A list of UIDs which are allowed to send mail.
    @ivar allowGIDs: A list of GIDs which are allowed to send mail.
    @ivar denyUIDs: A list of UIDs which are not allowed to send mail.
    @ivar denyGIDs: A list of GIDs which are not allowed to send mail.

    @type defaultAccess: C{bool}
    @ivar defaultAccess: C{True} if access will be allowed when no other access
    control rule matches or C{False} if it will be denied in that case.

    @ivar useraccess: Either C{'allow'} to check C{allowUID} first
    or C{'deny'} to check C{denyUID} first.

    @ivar groupaccess: Either C{'allow'} to check C{allowGID} first or
    C{'deny'} to check C{denyGID} first.

    @ivar identities: A C{dict} mapping hostnames to credentials to use when
    sending mail to that host.

    @ivar smarthost: C{None} or a hostname through which all outgoing mail will
    be sent.

    @ivar domain: C{None} or the hostname with which to identify ourselves when
    connecting to an MTA.
    """
    def __init__(self):
        self.allowUIDs = []
        self.denyUIDs = []
        self.allowGIDs = []
        self.denyGIDs = []
        self.useraccess = 'deny'
        self.groupaccess= 'deny'

        self.identities = {}
        self.smarthost = None
        self.domain = None

        self.defaultAccess = True


def loadConfig(path):
    # [useraccess]
    # allow=uid1,uid2,...
    # deny=uid1,uid2,...
    # order=allow,deny
    # [groupaccess]
    # allow=gid1,gid2,...
    # deny=gid1,gid2,...
    # order=deny,allow
    # [identity]
    # host1=username:password
    # host2=username:password
    # [addresses]
    # smarthost=a.b.c.d
    # default_domain=x.y.z

    c = Configuration()

    if not os.access(path, os.R_OK):
        return c

    p = ConfigParser()
    p.read(path)

    au = c.allowUIDs
    du = c.denyUIDs
    ag = c.allowGIDs
    dg = c.denyGIDs
    for (section, a, d) in (('useraccess', au, du), ('groupaccess', ag, dg)):
        if p.has_section(section):
            for (mode, L) in (('allow', a), ('deny', d)):
                if p.has_option(section, mode) and p.get(section, mode):
                    for id in p.get(section, mode).split(','):
                        try:
                            id = int(id)
                        except ValueError:
                            log("Illegal %sID in [%s] section: %s", section[0].upper(), section, id)
                        else:
                            L.append(id)
            order = p.get(section, 'order')
            order = map(str.split, map(str.lower, order.split(',')))
            if order[0] == 'allow':
                setattr(c, section, 'allow')
            else:
                setattr(c, section, 'deny')

    if p.has_section('identity'):
        for (host, up) in p.items('identity'):
            parts = up.split(':', 1)
            if len(parts) != 2:
                log("Illegal entry in [identity] section: %s", up)
                continue
            p.identities[host] = parts

    if p.has_section('addresses'):
        if p.has_option('addresses', 'smarthost'):
            c.smarthost = p.get('addresses', 'smarthost')
        if p.has_option('addresses', 'default_domain'):
            c.domain = p.get('addresses', 'default_domain')

    return c

def success(result):
    reactor.stop()

failed = None
def failure(f):
    global failed
    reactor.stop()
    failed = f

def sendmail(host, options, ident):
    d = smtp.sendmail(host, options.sender, options.to, options.body)
    d.addCallbacks(success, failure)
    reactor.run()

def senderror(failure, options):
    recipient = [options.sender]
    sender = '"Internally Generated Message (%s)"<postmaster@%s>' % (sys.argv[0], smtp.DNSNAME)
    error = StringIO.StringIO()
    failure.printTraceback(file=error)
    body = StringIO.StringIO(ERROR_FMT % error.getvalue())

    d = smtp.sendmail('localhost', sender, recipient, body)
    d.addBoth(lambda _: reactor.stop())

def deny(conf):
    uid = os.getuid()
    gid = os.getgid()

    if conf.useraccess == 'deny':
        if uid in conf.denyUIDs:
            return True
        if uid in conf.allowUIDs:
            return False
    else:
        if uid in conf.allowUIDs:
            return False
        if uid in conf.denyUIDs:
            return True

    if conf.groupaccess == 'deny':
        if gid in conf.denyGIDs:
            return True
        if gid in conf.allowGIDs:
            return False
    else:
        if gid in conf.allowGIDs:
            return False
        if gid in conf.denyGIDs:
            return True

    return not conf.defaultAccess

def run():
    o = parseOptions(sys.argv[1:])
    gConf = loadConfig(GLOBAL_CFG)
    lConf = loadConfig(LOCAL_CFG)

    if deny(gConf) or deny(lConf):
        log("Permission denied")
        return

    host = lConf.smarthost or gConf.smarthost or SMARTHOST

    ident = gConf.identities.copy()
    ident.update(lConf.identities)

    if lConf.domain:
        smtp.DNSNAME = lConf.domain
    elif gConf.domain:
        smtp.DNSNAME = gConf.domain

    sendmail(host, o, ident)

    if failed:
        if o.printErrors:
            failed.printTraceback(file=sys.stderr)
            raise SystemExit(1)
        else:
            senderror(failed, o)
