# relay.py
#
#
#   Copyright (C) 2005-2006 Daniel Burrows
#
#   This program 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 2 of the
#   License, or (at your option) any later version.
#
#   This program 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 this program; see the file COPYING.  If not, write to
#   the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
#   Boston, MA 02111-1307, USA.
#
# Code to relay a single message off the system.

import os
import sys

from cStringIO import StringIO
import defaults
import email.Generator
import email.Utils
import lock
import smtplib
import random
import re
import socket # For socket.error

class RelayError(Exception):
    pass

def relay(m, to_addrs, configenv, substvars, getpass):
    """Perform SMTP relaying on the Message object m with the given
    configuration in effect.  This expects that all configuration
    variables have defaults specified in configenv.  The 'getpass'
    parameter is a callable object to be used to retrieve the user's
    password; it will be invoked as getpass(prompt).

    The following configuration variables are used:

    default_from_address    - the address used to replace missing From:
                              addresses, or '' to require a From address.

    default_envelope_from   - the address used to replace missing
                              envelope-from addresses.  If set to '',
                              default_from_address is used.

    rewrite_unqualified     - if set, unqualified From and envelope-from
                              addresses are replaced with the default
                              address (or an error is signalled if
                              there is no default address).

    ssl                     - if True, put the connection into SSL mode
                              via StartTLS.

    smtp_relay              - the SMTP server to which email should be sent.
                              You may append ':port' to the message to
                              set the port number (which otherwise
                              defaults to 25).

    login                   - if set and nonempty, this string value is used
                              as the user's login name on the SMTP server.
                              If unset or empty, no authentication is performed.

    password                - if nonempty, this string value is used as the user's
                              password in authentication; otherwise, the
                              program attempts to query the user using getpass().

    debug_smtp              - if true, log all communications with the
                              SMTP server.

    mda                     - if nonempty, unqualified To addresses will be
                              passed as arguments to this string value.
                              In addition to the standard expansion
                              variables, the variable $RECIPIENT is bound
                              to the user receiving the email.  If empty,
                              delivery to unqualified addresses will simply fail.

                              NOTE: if you are not root, this had
                              better be a suid mail program, or
                              delivery to unqualified local addresses
                              will FAIL."""


    def prepare_message(m):
        """Prepare a message to be sent via SMTP by flattening it without mangling the
        From address."""
        tmpf = StringIO()
        g = email.Generator.Generator(tmpf, mangle_from_ = False)
        g.flatten(m)
        return tmpf.getvalue()

    def deliver_unqualified(m, recipient):
        """Deliver a message to an unqualified address (i.e., a local
        user) by passing it off to an MDA.  Throws RelayError if the
        MDA can't be found or if it doesn't exit successfully."""

        if recipient == '$USER' or recipient == '$LOGNAME':
            recipient = substvars.get('USER', recipient)

        tmpsubst = substvars.copy()
        tmpsubst['TO'] = re.escape(recipient)
        mda = configenv.get_value(identity, 'mda', tmpsubst)

        # Could default here, but that risks losing mail if the MDA is
        # badly configured.
        if mda == '':
            raise RelayError('Cannot deliver to %s: mda not set in the configuration file.' % recipient)

        if debug_smtp:
            print 'Executing "%s" to deliver mail'%mda

        try:
            fp = os.popen(mda, 'w')
            # Speed up delivery by only converting the message to a
            # string once.
            fp.write(msgtext)
            rval = fp.close()
        except (OSError, IOError),e:
            raise RelayError('Error delivering message via MDA: %s'%e)

        if rval <> None:
            if not os.WIFEXITED(rval):
                coredumpstr = ''
                if os.WCOREDUMP(rval):
                    coredumpstr = ' (core dumped)'

                if os.WIFSIGNALED(rval):
                    raise RelayError('MDA terminated by signal %d%s'%(os.WTERMSIG(rval), coredumpstr))
                else:
                    raise RelayError('The MDA stopped and I don\'t know why; exit status was 0x%x'%rval)
            elif os.WEXITSTATUS(rval) <> 0:
                raise RelayError('MDA exited abnormally (status %d)'%os.WEXITSTATUS(rval))

    if len(to_addrs) == 0:
        raise RelayError('No recipient addresses; unable to relay message')

    identity = m.get('From', None)
    # If the From header is missing, set it from the envelope header.
    if identity == None and m.get_unixfrom() <> None:
        identity  = m.get_unixfrom()
        m['From'] = identity

    substvars = substvars.copy()
    if identity <> None:
        substvars['FROM']    = identity
    substvars['TO']      = m.get('To', '')
    substvars['SUBJECT'] = m.get('Subject', '')


    if identity <> None:
        safeidentity = identity
    else:
        safeidentity = ''
    # Maybe rewrite a missing or unqualified From: address.
    rewrite_unqualified = configenv.get_value(safeidentity, 'rewrite_unqualified', substvars)


    # Maybe rewrite a missing or unqualified From address
    if identity == None or identity == '' or (rewrite_unqualified and '@' not in identity):
        default_from = configenv.get_value(safeidentity, 'default_from_address', substvars)

        if default_from == '':
            if identity == None or identity == '':
                raise RelayError('Message lacks a From address and default_from_address is unset; unable to proceed.')
            else:
                raise RelayError('Need to rewrite the unqualified address "%s", but default_from_address is unset; unable to proceed.'%identity)

        if identity == None:
            m['From'] = default_from
        else:
            m.replace_header('From', default_from)

        identity = default_from
        substvars['FROM'] = identity

        rewrite_unqualified = configenv.get_value(safeidentity, 'rewrite_unqualified', substvars)

    


    # Maybe rewrite a missing or unqualified envelope-from address
    envelope_from = m.get_unixfrom()

    if envelope_from == None or envelope_from == '' or (rewrite_unqualified and '@' not in envelope_from):
        envelope_from = configenv.get_value(identity, 'default_envelope_from', substvars)

        if envelope_from == '':
            envelope_from = configenv.get_value(identity, 'default_from_address', substvars)

            if envelope_from == '':
                raise RelayError('Need to rewrite the envelope-from, but no default address is specified.')


        m.set_unixfrom(envelope_from)


    # Find the relaying server.
    ssl          = configenv.get_value(identity, 'ssl', substvars)
    smtp_relay   = configenv.get_value(identity, 'smtp_relay', substvars)
    login        = configenv.get_value(identity, 'login', substvars)
    debug_smtp   = configenv.get_value(identity, 'debug_smtp', substvars)

    msgtext = prepare_message(m)

    unqualified = []
    qualified = []

    # Deliver to unqualified addresses first, so at least we don't
    # pollute the Internet with a partial delivery ;-).
    for addr in to_addrs:
        realname, realaddr = email.Utils.parseaddr(addr)

        if '@' not in realaddr:
            unqualified.append(realaddr)
        else:
            qualified.append(addr)


    for realaddr in unqualified:
        deliver_unqualified(m, realaddr)

    if qualified == []:
        return


    # SMTP relaying follows:
    if ':' in smtp_relay:
        smtp_relay_host = smtp_relay[:smtp_relay.find(':')]
    else:
        smtp_relay_host = smtp_relay

    if login <> '':
        password = configenv.get_value(identity, 'password', substvars)

        if password == '':
            password = getpass('Enter the password for %s@%s'%(login, smtp_relay_host))

    try:
        conn = smtplib.SMTP()
        conn.set_debuglevel(debug_smtp)
        conn.connect(smtp_relay)

        if ssl:
            if not conn.has_extn('STARTTLS'):
                raise RelayError('SSL was requested, but the relay server does not support STARTTLS.')
            rval, msg = conn.starttls()
            if rval <> 220:
                raise RelayError('Unable to enable secure data transmission; server said "%s"'%msg)

        if login <> '':
            conn.login(login, password)

        conn.sendmail(envelope_from, qualified, msgtext)
    except smtplib.SMTPException, e:
        raise RelayError('SMTP error: %s'%e)
    except socket.error, e:
        raise RelayError('Network error: %s'%e[1])
