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


"""Simple Mail Transfer Protocol implementation.
"""

from __future__ import generators

# Twisted imports
from twisted.copyright import longversion
from twisted.protocols import basic
from twisted.protocols import policies
from twisted.internet import protocol
from twisted.internet import defer
from twisted.internet import error
from twisted.internet import reactor
from twisted.internet.interfaces import ITLSTransport
from twisted.python import log
from twisted.python import components
from twisted.python import util
from twisted.python import reflect
from twisted.python import failure

from twisted import cred
import twisted.cred.checkers
import twisted.cred.credentials
from twisted.python.runtime import platform

# System imports
import time, string, re, base64, types, socket, os, random, hmac
import MimeWriter, tempfile, rfc822
import warnings
import binascii
import sys
from zope.interface import implements

try:
    from email.base64MIME import encode as encode_base64
except ImportError:
    def encode_base64(s, eol='\n'):
        return s.encode('base64').rstrip() + eol

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

# Cache the hostname (XXX Yes - this is broken)
if platform.isMacOSX():
    # On OS X, getfqdn() is ridiculously slow - use the
    # probably-identical-but-sometimes-not gethostname() there.
    DNSNAME = socket.gethostname()
else:
    DNSNAME = socket.getfqdn()

# Used for fast success code lookup
SUCCESS = dict(map(None, range(200, 300), []))

class IMessageDelivery(components.Interface):
    def receivedHeader(self, helo, origin, recipients):
        """
        Generate the Received header for a message

        @type helo: C{(str, str)}
        @param helo: The argument to the HELO command and the client's IP
        address.

        @type origin: C{Address}
        @param origin: The address the message is from

        @type recipients: C{list} of L{User}
        @param recipients: A list of the addresses for which this message
        is bound.

        @rtype: C{str}
        @return: The full \"Received\" header string.
        """

    def validateTo(self, user):
        """
        Validate the address for which the message is destined.

        @type user: C{User}
        @param user: The address to validate.

        @rtype: no-argument callable
        @return: A C{Deferred} which becomes, or a callable which
        takes no arguments and returns an object implementing C{IMessage}.
        This will be called and the returned object used to deliver the
        message when it arrives.

        @raise SMTPBadRcpt: Raised if messages to the address are
        not to be accepted.
        """

    def validateFrom(self, helo, origin):
        """
        Validate the address from which the message originates.

        @type helo: C{(str, str)}
        @param helo: The argument to the HELO command and the client's IP
        address.

        @type origin: C{Address}
        @param origin: The address the message is from

        @rtype: C{Deferred} or C{Address}
        @return: C{origin} or a C{Deferred} whose callback will be
        passed C{origin}.

        @raise SMTPBadSender: Raised of messages from this address are
        not to be accepted.
        """

class IMessageDeliveryFactory(components.Interface):
    """An alternate interface to implement for handling message delivery.

    It is useful to implement this interface instead of L{IMessageDelivery}
    directly because it allows the implementor to distinguish between
    different messages delivery over the same connection.  This can be
    used to optimize delivery of a single message to multiple recipients,
    something which cannot be done by L{IMessageDelivery} implementors
    due to their lack of information.
    """
    def getMessageDelivery(self):
        """Return an L{IMessageDelivery} object.

        This will be called once per message.
        """

class SMTPError(Exception):
    pass

class SMTPClientError(SMTPError):
    """Base class for SMTP client errors.
    """
    def __init__(self, code, resp, log=None, addresses=None, isFatal=False, retry=False):
        """
        @param code: The SMTP response code associated with this error.
        @param resp: The string response associated with this error.
        @param log: A string log of the exchange leading up to and including the error.

        @param isFatal: A boolean indicating whether this connection can proceed
        or not.  If True, the connection will be dropped.
        @param retry: A boolean indicating whether the delivery should be retried.
        If True and the factory indicates further retries are desirable, they will
        be attempted, otherwise the delivery will be failed.
        """
        self.code = code
        self.resp = resp
        self.log = log
        self.addresses = addresses
        self.isFatal = isFatal
        self.retry = retry

    def __str__(self):
        if self.code > 0:
            res = ["%.3d %s" % (self.code, self.resp)]
        else:
            res = [self.resp]
        if self.log:
            res.append('')
            res.append(self.log)
        return '\n'.join(res)


class ESMTPClientError(SMTPClientError):
    """Base class for ESMTP client errors.
    """

class EHLORequiredError(ESMTPClientError):
    """The server does not support EHLO.

    This is considered a non-fatal error (the connection will not be
    dropped).
    """

class AUTHRequiredError(ESMTPClientError):
    """Authentication was required but the server does not support it.

    This is considered a non-fatal error (the connection will not be
    dropped).
    """

class TLSRequiredError(ESMTPClientError):
    """Transport security was required but the server does not support it.

    This is considered a non-fatal error (the connection will not be
    dropped).
    """

class AUTHDeclinedError(ESMTPClientError):
    """The server rejected our credentials.

    Either the username, password, or challenge response
    given to the server was rejected.

    This is considered a non-fatal error (the connection will not be
    dropped).
    """

class AuthenticationError(ESMTPClientError):
    """An error ocurred while authenticating.

    Either the server rejected our request for authentication or the
    challenge received was malformed.

    This is considered a non-fatal error (the connection will not be
    dropped).
    """

class TLSError(ESMTPClientError):
    """An error occurred while negiotiating for transport security.

    This is considered a non-fatal error (the connection will not be
    dropped).
    """

class SMTPConnectError(SMTPClientError):
    """Failed to connect to the mail exchange host.

    This is considered a fatal error.  A retry will be made.
    """
    def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True):
        SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)

class SMTPTimeoutError(SMTPClientError):
    """Failed to receive a response from the server in the expected time period.

    This is considered a fatal error.  A retry will be made.
    """
    def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True):
        SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)

class SMTPProtocolError(SMTPClientError):
    """The server sent a mangled response.

    This is considered a fatal error.  A retry will not be made.
    """
    def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=False):
        SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)

class SMTPDeliveryError(SMTPClientError):
    """Indicates that a delivery attempt has had an error.
    """

class SMTPServerError(SMTPError):
    def __init__(self, code, resp):
        self.code = code
        self.resp = resp

    def __str__(self):
        return "%.3d %s" % (self.code, self.resp)

class SMTPAddressError(SMTPServerError):
    def __init__(self, addr, code, resp):
        SMTPServerError.__init__(self, code, resp)
        self.addr = Address(addr)

    def __str__(self):
        return "%.3d <%s>... %s" % (self.code, self.addr, self.resp)

class SMTPBadRcpt(SMTPAddressError):
    def __init__(self, addr, code=550,
                 resp='Cannot receive for specified address'):
        SMTPAddressError.__init__(self, addr, code, resp)

class SMTPBadSender(SMTPAddressError):
    def __init__(self, addr, code=550, resp='Sender not acceptable'):
        SMTPAddressError.__init__(self, addr, code, resp)

def rfc822date(timeinfo=None,local=1):
    """
    Format an RFC-2822 compliant date string.

    @param timeinfo: (optional) A sequence as returned by C{time.localtime()}
        or C{time.gmtime()}. Default is now.
    @param local: (optional) Indicates if the supplied time is local or
        universal time, or if no time is given, whether now should be local or
        universal time. Default is local, as suggested (SHOULD) by rfc-2822.

    @returns: A string representing the time and date in RFC-2822 format.
    """
    if not timeinfo:
        if local:
            timeinfo = time.localtime()
        else:
            timeinfo = time.gmtime()
    if local:
        if timeinfo[8]:
            # DST
            tz = -time.altzone
        else:
            tz = -time.timezone

        (tzhr, tzmin) = divmod(abs(tz), 3600)
        if tz:
            tzhr *= int(abs(tz)/tz)
        (tzmin, tzsec) = divmod(tzmin, 60)
    else:
        (tzhr, tzmin) = (0,0)

    return "%s, %02d %s %04d %02d:%02d:%02d %+03d%02d" % (
        ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timeinfo[6]],
        timeinfo[2],
        ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
         'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timeinfo[1] - 1],
        timeinfo[0], timeinfo[3], timeinfo[4], timeinfo[5],
        tzhr, tzmin)

def idGenerator():
    i = 0
    while True:
        yield i
        i += 1

def messageid(uniq=None, N=idGenerator().next):
    """Return a globally unique random string in RFC 2822 Message-ID format

    <datetime.pid.random@host.dom.ain>

    Optional uniq string will be added to strenghten uniqueness if given.
    """
    datetime = time.strftime('%Y%m%d%H%M%S', time.gmtime())
    pid = os.getpid()
    rand = random.randrange(2**31L-1)
    if uniq is None:
        uniq = ''
    else:
        uniq = '.' + uniq

    return '<%s.%s.%s%s.%s@%s>' % (datetime, pid, rand, uniq, N(), DNSNAME)

def quoteaddr(addr):
    """Turn an email address, possibly with realname part etc, into
    a form suitable for and SMTP envelope.
    """

    if isinstance(addr, Address):
        return '<%s>' % str(addr)

    res = rfc822.parseaddr(addr)

    if res == (None, None):
        # It didn't parse, use it as-is
        return '<%s>' % str(addr)
    else:
        return '<%s>' % str(res[1])

COMMAND, DATA, AUTH = 'COMMAND', 'DATA', 'AUTH'

class AddressError(SMTPError):
    "Parse error in address"

# Character classes for parsing addresses
atom = r"[-A-Za-z0-9!\#$%&'*+/=?^_`{|}~]"

class Address:
    """Parse and hold an RFC 2821 address.

    Source routes are stipped and ignored, UUCP-style bang-paths
    and %-style routing are not parsed.

    @type domain: C{str}
    @ivar domain: The domain within which this address resides.

    @type local: C{str}
    @ivar local: The local (\"user\") portion of this address.
    """

    tstring = re.compile(r'''( # A string of
                          (?:"[^"]*" # quoted string
                          |\\. # backslash-escaped characted
                          |''' + atom + r''' # atom character
                          )+|.) # or any single character''',re.X)
    atomre = re.compile(atom) # match any one atom character

    def __init__(self, addr, defaultDomain=None):
        if isinstance(addr, User):
            addr = addr.dest
        if isinstance(addr, Address):
            self.__dict__ = addr.__dict__.copy()
            return
        elif not isinstance(addr, types.StringTypes):
            addr = str(addr)
        self.addrstr = addr

        # Tokenize
        atl = filter(None,self.tstring.split(addr))

        local = []
        domain = []

        while atl:
            if atl[0] == '<':
                if atl[-1] != '>':
                    raise AddressError, "Unbalanced <>"
                atl = atl[1:-1]
            elif atl[0] == '@':
                atl = atl[1:]
                if not local:
                    # Source route
                    while atl and atl[0] != ':':
                        # remove it
                        atl = atl[1:]
                    if not atl:
                        raise AddressError, "Malformed source route"
                    atl = atl[1:] # remove :
                elif domain:
                    raise AddressError, "Too many @"
                else:
                    # Now in domain
                    domain = ['']
            elif len(atl[0]) == 1 and not self.atomre.match(atl[0]) and atl[0] !=  '.':
                raise AddressError, "Parse error at %r of %r" % (atl[0], (addr, atl))
            else:
                if not domain:
                    local.append(atl[0])
                else:
                    domain.append(atl[0])
                atl = atl[1:]

        self.local = ''.join(local)
        self.domain = ''.join(domain)
        if self.local != '' and self.domain == '':
            if defaultDomain is None:
                defaultDomain = DNSNAME
            self.domain = defaultDomain

    dequotebs = re.compile(r'\\(.)')

    def dequote(self,addr):
        """Remove RFC-2821 quotes from address."""
        res = []

        atl = filter(None,self.tstring.split(str(addr)))

        for t in atl:
            if t[0] == '"' and t[-1] == '"':
                res.append(t[1:-1])
            elif '\\' in t:
                res.append(self.dequotebs.sub(r'\1',t))
            else:
                res.append(t)

        return ''.join(res)

    def __str__(self):
        if self.local or self.domain:
            return '@'.join((self.local, self.domain))
        else:
            return ''

    def __repr__(self):
        return "%s.%s(%s)" % (self.__module__, self.__class__.__name__,
                              repr(str(self)))

class User:
    """Hold information about and SMTP message recipient,
    including information on where the message came from
    """

    def __init__(self, destination, helo, protocol, orig):
        host = getattr(protocol, 'host', None)
        self.dest = Address(destination, host)
        self.helo = helo
        self.protocol = protocol
        if isinstance(orig, Address):
            self.orig = orig
        else:
            self.orig = Address(orig, host)

    def __getstate__(self):
        """Helper for pickle.

        protocol isn't picklabe, but we want User to be, so skip it in
        the pickle.
        """
        return { 'dest' : self.dest,
                 'helo' : self.helo,
                 'protocol' : None,
                 'orig' : self.orig }

    def __str__(self):
        return str(self.dest)

class IMessage(components.Interface):
    """Interface definition for messages that can be sent via SMTP."""

    def lineReceived(self, line):
        """handle another line"""

    def eomReceived(self):
        """handle end of message

        return a deferred. The deferred should be called with either:
        callback(string) or errback(error)
        """

    def connectionLost(self):
        """handle message truncated

        semantics should be to discard the message
        """

class SMTP(basic.LineOnlyReceiver, policies.TimeoutMixin):
    """SMTP server-side protocol."""

    timeout = 600
    host = DNSNAME
    portal = None

    # Control whether we log SMTP events
    noisy = True

    # A factory for IMessageDelivery objects.  If an
    # avatar implementing IMessageDeliveryFactory can
    # be acquired from the portal, it will be used to
    # create a new IMessageDelivery object for each
    # message which is received.
    deliveryFactory = None

    # An IMessageDelivery object.  A new instance is
    # used for each message received if we can get an
    # IMessageDeliveryFactory from the portal.  Otherwise,
    # a single instance is used throughout the lifetime
    # of the connection.
    delivery = None

    # Cred cleanup function.
    _onLogout = None

    def __init__(self, delivery=None, deliveryFactory=None):
        self.mode = COMMAND
        self._from = None
        self._helo = None
        self._to = []
        self.delivery = delivery
        self.deliveryFactory = deliveryFactory

    def timeoutConnection(self):
        msg = '%s Timeout. Try talking faster next time!' % (self.host,)
        self.sendCode(421, msg)
        self.transport.loseConnection()

    def greeting(self):
        return '%s NO UCE NO UBE NO RELAY PROBES ESMTP' % (self.host,)

    def connectionMade(self):
        # Ensure user-code always gets something sane for _helo
        peer = self.transport.getPeer()
        try:
            host = peer.host
        except AttributeError: # not an IPv4Address
            host = str(peer)
        self._helo = (None, host)
        self.sendCode(220, self.greeting())
        self.setTimeout(self.timeout)

    def sendCode(self, code, message=''):
        "Send an SMTP code with a message."
        lines = message.splitlines()
        lastline = lines[-1:]
        for line in lines[:-1]:
            self.sendLine('%3.3d-%s' % (code, line))
        self.sendLine('%3.3d %s' % (code,
                                    lastline and lastline[0] or ''))

    def lineReceived(self, line):
        self.resetTimeout()
        return getattr(self, 'state_' + self.mode)(line)

    def state_COMMAND(self, line):
        # Ignore leading and trailing whitespace, as well as an arbitrary
        # amount of whitespace between the command and its argument, though
        # it is not required by the protocol, for it is a nice thing to do.
        line = line.strip()

        parts = line.split(None, 1)
        if parts:
            method = self.lookupMethod(parts[0]) or self.do_UNKNOWN
            if len(parts) == 2:
                method(parts[1])
            else:
                method('')
        else:
            self.sendSyntaxError()

    def sendSyntaxError(self):
        self.sendCode(500, 'Error: bad syntax')

    def lookupMethod(self, command):
        return getattr(self, 'do_' + command.upper(), None)

    def lineLengthExceeded(self, line):
        if self.mode is DATA:
            for message in self.__messages:
                message.connectionLost()
            self.mode = COMMAND
            del self.__messages
        self.sendCode(500, 'Line too long')

    def do_UNKNOWN(self, rest):
        self.sendCode(500, 'Command not implemented')

    def do_HELO(self, rest):
        peer = self.transport.getPeer()
        try:
            host = peer.host
        except AttributeError:
            host = str(peer)
        self._helo = (rest, host)
        self._from = None
        self._to = []
        self.sendCode(250, '%s Hello %s, nice to meet you' % (self.host, host))

    def do_QUIT(self, rest):
        self.sendCode(221, 'See you later')
        self.transport.loseConnection()

    # A string of quoted strings, backslash-escaped character or
    # atom characters + '@.,:'
    qstring = r'("[^"]*"|\\.|' + atom + r'|[@.,:])+'

    mail_re = re.compile(r'''\s*FROM:\s*(?P<path><> # Empty <>
                         |<''' + qstring + r'''> # <addr>
                         |''' + qstring + r''' # addr
                         )\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options
                         $''',re.I|re.X)
    rcpt_re = re.compile(r'\s*TO:\s*(?P<path><' + qstring + r'''> # <addr>
                         |''' + qstring + r''' # addr
                         )\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options
                         $''',re.I|re.X)

    def do_MAIL(self, rest):
        if self._from:
            self.sendCode(503,"Only one sender per message, please")
            return
        # Clear old recipient list
        self._to = []
        m = self.mail_re.match(rest)
        if not m:
            self.sendCode(501, "Syntax error")
            return

        try:
            addr = Address(m.group('path'), self.host)
        except AddressError, e:
            self.sendCode(553, str(e))
            return

        defer.maybeDeferred(self.validateFrom, self._helo, addr
            ).addCallbacks(self._cbFromValidate, self._ebFromValidate
            )

    def _cbFromValidate(self, from_, code=250, msg='Sender address accepted'):
        self._from = from_
        self.sendCode(code, msg)

    def _ebFromValidate(self, failure):
        if failure.check(SMTPBadSender):
            self.sendCode(failure.value.code,
                          'Cannot receive for specified address %s: %s'
                          % (quoteaddr(failure.value.addr), failure.value.resp))
        elif failure.check(SMTPServerError):
            self.sendCode(failure.value.code, failure.value.resp)
        else:
            log.err(failure)
            self.sendCode(
                451,
                'Requested action aborted: local error in processing'
            )


    def do_RCPT(self, rest):
        if not self._from:
            self.sendCode(503, "Must have sender before recipient")
            return
        m = self.rcpt_re.match(rest)
        if not m:
            self.sendCode(501, "Syntax error")
            return

        try:
            user = User(m.group('path'), self._helo, self, self._from)
        except AddressError, e:
            self.sendCode(553, str(e))
            return

        d = defer.maybeDeferred(self.validateTo, user)
        d.addCallbacks(
            self._cbToValidate,
            self._ebToValidate,
            callbackArgs=(user,)
        )

    def _cbToValidate(self, to, user=None, code=250, msg='Recipient address accepted'):
        if user is None:
            user = to
        self._to.append((user, to))
        self.sendCode(code, msg)

    def _ebToValidate(self, failure):
        if failure.check(SMTPBadRcpt, SMTPServerError):
            self.sendCode(failure.value.code, failure.value.resp)
        else:
            log.err(failure)
            self.sendCode(
                451,
                'Requested action aborted: local error in processing'
            )

    def _disconnect(self, msgs):
        for msg in msgs:
            try:
                msg.connectionLost()
            except:
                log.msg("msg raised exception from connectionLost")
                log.err()

    def do_DATA(self, rest):
        if self._from is None or (not self._to):
            self.sendCode(503, 'Must have valid receiver and originator')
            return
        self.mode = DATA
        helo, origin = self._helo, self._from
        recipients = self._to

        self._from = None
        self._to = []
        self.datafailed = None

        msgs = []
        for (user, msgFunc) in recipients:
            try:
                msg = msgFunc()
                rcvdhdr = self.receivedHeader(helo, origin, [user])
                if rcvdhdr:
                    msg.lineReceived(rcvdhdr)
                msgs.append(msg)
            except SMTPServerError, e:
                self.sendCode(e.code, e.resp)
                self.mode = COMMAND
                self._disconnect(msgs)
                return
            except:
                log.err()
                self.sendCode(550, "Internal server error")
                self.mode = COMMAND
                self._disconnect(msgs)
                return
        self.__messages = msgs

        self.__inheader = self.__inbody = 0
        self.sendCode(354, 'Continue')

        if self.noisy:
            fmt = 'Receiving message for delivery: from=%s to=%s'
            log.msg(fmt % (origin, [str(u) for (u, f) in recipients]))

    def connectionLost(self, reason):
        # self.sendCode(421, 'Dropping connection.') # This does nothing...
        # Ideally, if we (rather than the other side) lose the connection,
        # we should be able to tell the other side that we are going away.
        # RFC-2821 requires that we try.
        if self.mode is DATA:
            try:
                for message in self.__messages:
                    try:
                        message.connectionLost()
                    except:
                        log.err()
                del self.__messages
            except AttributeError:
                pass
        if self._onLogout:
            self._onLogout()
            self._onLogout = None
        self.setTimeout(None)

    def do_RSET(self, rest):
        self._from = None
        self._to = []
        self.sendCode(250, 'I remember nothing.')

    def dataLineReceived(self, line):
        if line[:1] == '.':
            if line == '.':
                self.mode = COMMAND
                if self.datafailed:
                    self.sendCode(self.datafailed.code,
                                  self.datafailed.resp)
                    return
                if not self.__messages:
                    self._messageHandled("thrown away")
                    return
                defer.DeferredList([
                    m.eomReceived() for m in self.__messages
                ], consumeErrors=True).addCallback(self._messageHandled
                                                   )
                del self.__messages
                return
            line = line[1:]

        if self.datafailed:
            return

        try:
            # Add a blank line between the generated Received:-header
            # and the message body if the message comes in without any
            # headers
            if not self.__inheader and not self.__inbody:
                if ':' in line:
                    self.__inheader = 1
                elif line:
                    for message in self.__messages:
                        message.lineReceived('')
                    self.__inbody = 1

            if not line:
                self.__inbody = 1

            for message in self.__messages:
                message.lineReceived(line)
        except SMTPServerError, e:
            self.datafailed = e
            for message in self.__messages:
                message.connectionLost()
    state_DATA = dataLineReceived

    def _messageHandled(self, resultList):
        failures = 0
        for (success, result) in resultList:
            if not success:
                failures += 1
                log.err(result)
        if failures:
            msg = 'Could not send e-mail'
            L = len(resultList)
            if L > 1:
                msg += ' (%d failures out of %d recipients)' % (failures, L)
            self.sendCode(550, msg)
        else:
            self.sendCode(250, 'Delivery in progress')

    def _cbAuthenticated(self, (iface, avatar, logout)):
        if issubclass(iface, IMessageDeliveryFactory):
            self.deliveryFactory = avatar
            self.delivery = None
        elif issubclass(iface, IMessageDelivery):
            self.deliveryFactory = None
            self.delivery = avatar
        else:
            raise RuntimeError("%s is not a supported interface" % (iface.__name__,))
        self._onLogout = logout
        self.authenticated = 1
        self.challenger = None

    def _ebAuthenticated(self, reason):
        self.challenge = None
        if reason.check(cred.error.UnauthorizedLogin):
            self.sendCode(535, 'Authentication failed')
        elif reason.check(SMTPAddressError):
            self.sendCode(reason.value.code, reason.value.resp)
        else:
            self.sendCode(451, 'Requested action aborted: local error in processing')
            log.err(reason)

    # overridable methods:
    def validateFrom(self, helo, origin):
        """
        Validate the address from which the message originates.

        @type helo: C{(str, str)}
        @param helo: The argument to the HELO command and the client's IP
        address.

        @type origin: C{Address}
        @param origin: The address the message is from

        @rtype: C{Deferred} or C{Address}
        @return: C{origin} or a C{Deferred} whose callback will be
        passed C{origin}.

        @raise SMTPBadSender: Raised of messages from this address are
        not to be accepted.
        """
        if self.deliveryFactory is not None:
            self.delivery = self.deliveryFactory.getMessageDelivery()

        if self.delivery is not None:
            return defer.maybeDeferred(self.delivery.validateFrom,
                                       helo, origin)

        # No login has been performed, no default delivery object has been
        # provided: try to perform an anonymous login and then invoke this
        # method again.
        if self.portal:
            return self.portal.login(cred.credentials.Anonymous(), None,
                    IMessageDeliveryFactory, IMessageDelivery
                ).addCallback(self._cbAuthenticated
                ).addCallback(lambda _: self.validateFrom(helo, origin)
                ).addErrback(self._ebAuthenticated
                )
        raise SMTPBadSender(origin)

    def validateTo(self, user):
        """
        Validate the address for which the message is destined.

        @type user: C{User}
        @param user: The address to validate.

        @rtype: no-argument callable
        @return: A C{Deferred} which becomes, or a callable which
        takes no arguments and returns an object implementing C{IMessage}.
        This will be called and the returned object used to deliver the
        message when it arrives.

        @raise SMTPBadRcpt: Raised if messages to the address are
        not to be accepted.
        """
        if self.delivery is not None:
            return self.delivery.validateTo(user)
        raise SMTPBadRcpt(user)

    def receivedHeader(self, helo, origin, recipients):
        if self.delivery is not None:
            return self.delivery.receivedHeader(helo, origin, recipients)

        heloStr = ""
        if helo[0]:
            heloStr = " helo=%s" % (helo[0],)
        domain = self.transport.getHost().host
        from_ = "from %s ([%s]%s)" % (helo[0], helo[1], heloStr)
        by = "by %s with ESMTP (%s)" % (domain, longversion)
        for_ = "for %s; %s" % (' '.join(map(str, recipients)),
                               rfc822date())
        return "Received: %s\n\t%s\n\t%s" % (from_, by, for_)

    def startMessage(self, recipients):
        if self.delivery:
            return self.delivery.startMessage(recipients)
        return []


class SMTPFactory(protocol.ServerFactory):
    """Factory for SMTP."""

    # override in instances or subclasses
    domain = DNSNAME
    timeout = 600
    protocol = SMTP

    portal = None

    def __init__(self, portal = None):
        self.portal = portal

    def buildProtocol(self, addr):
        p = protocol.ServerFactory.buildProtocol(self, addr)
        p.portal = self.portal
        p.host = self.domain
        return p

class SMTPClient(basic.LineReceiver, policies.TimeoutMixin):
    """SMTP client for sending emails."""

    # If enabled then log SMTP client server communication
    debug = True

    # Number of seconds to wait before timing out a connection.  If
    # None, perform no timeout checking.
    timeout = None

    def __init__(self, identity, logsize=10):
        self.identity = identity or ''
        self.toAddressesResult = []
        self.successAddresses = []
        self._from = None
        self.resp = []
        self.code = -1
        self.log = util.LineLog(logsize)

    def sendLine(self, line):
        # Log sendLine only if you are in debug mode for performance
        if self.debug:
            self.log.append('>>> ' + line)

        basic.LineReceiver.sendLine(self,line)

    def connectionMade(self):
        self.setTimeout(self.timeout)

        self._expected = [ 220 ]
        self._okresponse = self.smtpState_helo
        self._failresponse = self.smtpConnectionFailed

    def connectionLost(self, reason=protocol.connectionDone):
        """We are no longer connected"""
        self.setTimeout(None)
        self.mailFile = None

    def timeoutConnection(self):
        self.sendError(
            SMTPTimeoutError(-1,
                             "Timeout waiting for SMTP server response",
                             self.log))

    def lineReceived(self, line):
        self.resetTimeout()

        # Log lineReceived only if you are in debug mode for performance
        if self.debug:
            self.log.append('<<< ' + line)

        why = None

        try:
            self.code = int(line[:3])
        except ValueError:
            # This is a fatal error and will disconnect the transport lineReceived will not be called again
            self.sendError(SMTPProtocolError(-1, "Invalid response from SMTP server: %s" % line, self.log.str()))
            return

        if line[0] == '0':
            # Verbose informational message, ignore it
            return

        self.resp.append(line[4:])

        if line[3:4] == '-':
            # continuation
            return

        if self.code in self._expected:
            why = self._okresponse(self.code,'\n'.join(self.resp))
        else:
            why = self._failresponse(self.code,'\n'.join(self.resp))

        self.code = -1
        self.resp = []
        return why

    def smtpConnectionFailed(self, code, resp):
        self.sendError(SMTPConnectError(code, resp, self.log.str()))

    def smtpTransferFailed(self, code, resp):
        if code < 0:
            self.sendError(SMTPProtocolError(code, resp, self.log.str()))
        else:
            self.smtpState_msgSent(code, resp)

    def smtpState_helo(self, code, resp):
        self.sendLine('HELO ' + self.identity)
        self._expected = SUCCESS
        self._okresponse = self.smtpState_from

    def smtpState_from(self, code, resp):
        self._from = self.getMailFrom()
        self._failresponse = self.smtpTransferFailed
        if self._from is not None:
            self.sendLine('MAIL FROM:%s' % quoteaddr(self._from))
            self._expected = [250]
            self._okresponse = self.smtpState_to
        else:
            # All messages have been sent, disconnect
            self._disconnectFromServer()

    def smtpState_disconnect(self, code, resp):
        self.transport.loseConnection()

    def smtpState_to(self, code, resp):
        self.toAddresses = iter(self.getMailTo())
        self.toAddressesResult = []
        self.successAddresses = []
        self._okresponse = self.smtpState_toOrData
        self._expected = xrange(0,1000)
        self.lastAddress = None
        return self.smtpState_toOrData(0, '')

    def smtpState_toOrData(self, code, resp):
        if self.lastAddress is not None:
            self.toAddressesResult.append((self.lastAddress, code, resp))
            if code in SUCCESS:
                self.successAddresses.append(self.lastAddress)
        try:
            self.lastAddress = self.toAddresses.next()
        except StopIteration:
            if self.successAddresses:
                self.sendLine('DATA')
                self._expected = [ 354 ]
                self._okresponse = self.smtpState_data
            else:
                return self.smtpState_msgSent(code,'No recipients accepted')
        else:
            self.sendLine('RCPT TO:%s' % quoteaddr(self.lastAddress))

    def smtpState_data(self, code, resp):
        s = basic.FileSender()
        s.beginFileTransfer(
            self.getMailData(), self.transport, self.transformChunk
        ).addCallback(self.finishedFileTransfer)
        self._expected = SUCCESS
        self._okresponse = self.smtpState_msgSent

    def smtpState_msgSent(self, code, resp):
        if self._from is not None:
            self.sentMail(code, resp, len(self.successAddresses),
                          self.toAddressesResult, self.log)

        self.toAddressesResult = []
        self._from = None
        self.sendLine('RSET')
        self._expected = SUCCESS
        self._okresponse = self.smtpState_from

    ##
    ## Helpers for FileSender
    ##
    def transformChunk(self, chunk):
        return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..')

    def finishedFileTransfer(self, lastsent):
        if lastsent != '\n':
            line = '\r\n.'
        else:
            line = '.'
        self.sendLine(line)
    ##

    # these methods should be overriden in subclasses
    def getMailFrom(self):
        """Return the email address the mail is from."""
        raise NotImplementedError

    def getMailTo(self):
        """Return a list of emails to send to."""
        raise NotImplementedError

    def getMailData(self):
        """Return file-like object containing data of message to be sent.

        Lines in the file should be delimited by '\\n'.
        """
        raise NotImplementedError

    def sendError(self, exc):
        """
        If an error occurs before a mail message is sent sendError will be
        called.  This base class method sends a QUIT if the error is
        non-fatal and disconnects the connection.

        @param exc: The SMTPClientError (or child class) raised
        @type exc: C{SMTPClientError}
        """
        assert isinstance(exc, SMTPClientError)

        if exc.isFatal:
            # If the error was fatal then the communication channel with the SMTP Server is
            # broken so just close the transport connection
            self.smtpState_disconnect(-1, None)
        else:
            self._disconnectFromServer()

    def sentMail(self, code, resp, numOk, addresses, log):
        """Called when an attempt to send an email is completed.

        If some addresses were accepted, code and resp are the response
        to the DATA command. If no addresses were accepted, code is -1
        and resp is an informative message.

        @param code: the code returned by the SMTP Server
        @param resp: The string response returned from the SMTP Server
        @param numOK: the number of addresses accepted by the remote host.
        @param addresses: is a list of tuples (address, code, resp) listing
                          the response to each RCPT command.
        @param log: is the SMTP session log
        """
        raise NotImplementedError

    def _disconnectFromServer(self):
        self._expected = xrange(0, 1000)
        self._okresponse = self.smtpState_disconnect
        self.sendLine('QUIT')

class ESMTPClient(SMTPClient):
    # Fall back to HELO if the server does not support EHLO
    heloFallback = True

    # Refuse to proceed if authentication cannot be performed
    requireAuthentication = False

    # Refuse to proceed if TLS is not available
    requireTransportSecurity = False

    # Indicate whether or not our transport can be considered secure.
    tlsMode = False

    # ClientContextFactory to use for STARTTLS
    context = None

    def __init__(self, secret, contextFactory=None, *args, **kw):
        SMTPClient.__init__(self, *args, **kw)
        self.authenticators = []
        self.secret = secret
        self.context = contextFactory
        self.tlsMode = False

    def esmtpEHLORequired(self, code=-1, resp=None):
        self.sendError(EHLORequiredError(502, "Server does not support ESMTP Authentication", self.log.str()))

    def esmtpAUTHRequired(self, code=-1, resp=None):
        tmp = []

        for a in self.authenticators:
            tmp.append(a.getName().upper())

        auth = "[%s]" % ', '.join(tmp)

        self.sendError(AUTHRequiredError(502, "Server does not support Client Authentication schemes %s" % auth,
                                         self.log.str()))

    def esmtpTLSRequired(self, code=-1, resp=None):
        self.sendError(TLSRequiredError(502, "Server does not support secure communication via TLS / SSL",
                                        self.log.str()))

    def esmtpTLSFailed(self, code=-1, resp=None):
        self.sendError(TLSError(code, "Could not complete the SSL/TLS handshake", self.log.str()))

    def esmtpAUTHDeclined(self, code=-1, resp=None):
        self.sendError(AUTHDeclinedError(code, resp, self.log.str()))

    def esmtpAUTHMalformedChallenge(self, code=-1, resp=None):
        str =  "Login failed because the SMTP Server returned a malformed Authentication Challenge"
        self.sendError(AuthenticationError(501, str, self.log.str()))

    def esmtpAUTHServerError(self, code=-1, resp=None):
        self.sendError(AuthenticationError(code, resp, self.log.str()))

    def registerAuthenticator(self, auth):
        """Registers an Authenticator with the ESMTPClient. The ESMTPClient
           will attempt to login to the SMTP Server in the order the
           Authenticators are registered. The most secure Authentication
           mechanism should be registered first.

           @param auth: The Authentication mechanism to register
           @type auth: class implementing C{IClientAuthentication}
        """

        self.authenticators.append(auth)

    def connectionMade(self):
        SMTPClient.connectionMade(self)
        self._okresponse = self.esmtpState_ehlo

    def esmtpState_ehlo(self, code, resp):
        self._expected = SUCCESS

        self._okresponse = self.esmtpState_serverConfig
        self._failresponse = self.esmtpEHLORequired

        if self.heloFallback:
            self._failresponse = self.smtpState_helo

        self.sendLine('EHLO ' + self.identity)

    def esmtpState_serverConfig(self, code, resp):
        items = {}
        for line in resp.splitlines():
            e = line.split(None, 1)
            if len(e) > 1:
                items[e[0]] = e[1]
            else:
                items[e[0]] = None

        if self.tlsMode:
            self.authenticate(code, resp, items)
        else:
            self.tryTLS(code, resp, items)

    def tryTLS(self, code, resp, items):
        if self.context and 'STARTTLS' in items:
            self._expected = [220]
            self._okresponse = self.esmtpState_starttls
            self._failresponse = self.esmtpTLSFailed
            self.sendLine('STARTTLS')
        elif self.requireTransportSecurity:
            self.tlsMode = False
            self.esmtpTLSRequired()
        else:
            self.tlsMode = False
            self.authenticate(code, resp, items)

    def esmtpState_starttls(self, code, resp):
        try:
            self.transport.startTLS(self.context)
            self.tlsMode = True
        except:
            log.err()
            self.esmtpTLSFailed(451)

        # Send another EHLO once TLS has been started to
        # get the TLS / AUTH schemes. Some servers only allow AUTH in TLS mode.
        self.esmtpState_ehlo(code, resp)

    def authenticate(self, code, resp, items):
        if self.secret and items.get('AUTH'):
            schemes = items['AUTH'].split()
            tmpSchemes = {}

            #XXX: May want to come up with a more efficient way to do this
            for s in schemes:
                tmpSchemes[s.upper()] = 1

            for a in self.authenticators:
                auth = a.getName().upper()

                if auth in tmpSchemes:
                    self._authinfo = a

                    # Special condition handled
                    if auth  == "PLAIN":
                        self._okresponse = self.smtpState_from
                        self._failresponse = self._esmtpState_plainAuth
                        self._expected = [235]
                        challenge = encode_base64(self._authinfo.challengeResponse(self.secret, 1), eol="")
                        self.sendLine('AUTH ' + auth + ' ' + challenge)
                    else:
                        self._expected = [334]
                        self._okresponse = self.esmtpState_challenge
                        # If some error occurs here, the server declined the AUTH
                        # before the user / password phase. This would be
                        # a very rare case
                        self._failresponse = self.esmtpAUTHServerError
                        self.sendLine('AUTH ' + auth)
                    return

        if self.requireAuthentication:
            self.esmtpAUTHRequired()
        else:
            self.smtpState_from(code, resp)

    def _esmtpState_plainAuth(self, code, resp):
        self._okresponse = self.smtpState_from
        self._failresponse = self.esmtpAUTHDeclined
        self._expected = [235]
        challenge = encode_base64(self._authinfo.challengeResponse(self.secret, 2), eol="")
        self.sendLine('AUTH PLAIN ' + challenge)

    def esmtpState_challenge(self, code, resp):
        auth = self._authinfo
        del self._authinfo
        self._authResponse(auth, resp)

    def _authResponse(self, auth, challenge):
        self._failresponse = self.esmtpAUTHDeclined

        try:
            challenge = base64.decodestring(challenge)
        except binascii.Error, e:
            # Illegal challenge, give up, then quit
            self.sendLine('*')
            self._okresponse = self.esmtpAUTHMalformedChallenge
            self._failresponse = self.esmtpAUTHMalformedChallenge
        else:
            resp = auth.challengeResponse(self.secret, challenge)
            self._expected = [235]
            self._okresponse = self.smtpState_from
            self.sendLine(encode_base64(resp, eol=""))

        if auth.getName() == "LOGIN" and challenge == "Username:":
            self._expected = [334]
            self._authinfo = auth
            self._okresponse = self.esmtpState_challenge


class ESMTP(SMTP):

    ctx = None
    canStartTLS = False
    startedTLS = False

    authenticated = False

    def __init__(self, chal = None, contextFactory = None):
        SMTP.__init__(self)
        if chal is None:
            chal = {}
        self.challengers = chal
        self.authenticated = False
        self.ctx = contextFactory

    def connectionMade(self):
        SMTP.connectionMade(self)
        self.canStartTLS = ITLSTransport.providedBy(self.transport)
        self.canStartTLS = self.canStartTLS and (self.ctx is not None)

    def extensions(self):
        ext = {'AUTH': self.challengers.keys()}
        if self.canStartTLS and not self.startedTLS:
            ext['STARTTLS'] = None
        return ext

    def lookupMethod(self, command):
        m = SMTP.lookupMethod(self, command)
        if m is None:
            m = getattr(self, 'ext_' + command.upper(), None)
        return m

    def listExtensions(self):
        r = []
        for (c, v) in self.extensions().iteritems():
            if v is not None:
                if v:
                    # Intentionally omit extensions with empty argument lists
                    r.append('%s %s' % (c, ' '.join(v)))
            else:
                r.append(c)
        return '\n'.join(r)

    def do_EHLO(self, rest):
        peer = self.transport.getPeer().host
        self._helo = (rest, peer)
        self._from = None
        self._to = []
        self.sendCode(
            250,
            '%s Hello %s, nice to meet you\n%s' % (
                self.host, peer,
                self.listExtensions(),
            )
        )

    def ext_STARTTLS(self, rest):
        if self.startedTLS:
            self.sendCode(503, 'TLS already negotiated')
        elif self.ctx and self.canStartTLS:
            self.sendCode(220, 'Begin TLS negotiation now')
            self.transport.startTLS(self.ctx)
            self.startedTLS = True
        else:
            self.sendCode(454, 'TLS not available')

    def ext_AUTH(self, rest):
        if self.authenticated:
            self.sendCode(503, 'Already authenticated')
            return
        parts = rest.split(None, 1)
        chal = self.challengers.get(parts[0].upper(), lambda: None)()
        if not chal:
            self.sendCode(504, 'Unrecognized authentication type')
            return
        self.authenticate(chal)

    def authenticate(self, challenger):
        if self.portal:
            challenge = challenger.getChallenge()
            coded = base64.encodestring(challenge)[:-1]
            self.sendCode(334, coded)
            self.mode = AUTH
            self.challenger = challenger
        else:
            self.sendCode(454, 'Temporary authentication failure')

    def state_AUTH(self, rest):
        self.mode = COMMAND

        if rest == '*':
            self.sendCode(501, 'Authentication aborted')
            self.challenger.abort()
            self.challenger = None
            return

        try:
            uncoded = base64.decodestring(rest)
        except binascii.Error, e:
            self._ebAuthenticated(failure.Failure(e))
        else:
            self.challenger.setResponse(uncoded)
            if self.challenger.moreChallenges():
                self.authenticate(self.challenger)
            else:
                self.portal.login(self.challenger, None,
                        IMessageDeliveryFactory, IMessageDelivery
                    ).addCallback(self._cbAuthenticated
                    ).addCallback(lambda _: self.sendCode(235, 'Authentication successful.')
                    ).addErrback(self._ebAuthenticated
                    )

class SenderMixin:
    """Utility class for sending emails easily.

    Use with SMTPSenderFactory or ESMTPSenderFactory.
    """
    done = 0

    def getMailFrom(self):
        if not self.done:
            self.done = 1
            return str(self.factory.fromEmail)
        else:
            return None

    def getMailTo(self):
        return self.factory.toEmail

    def getMailData(self):
        return self.factory.file

    def sendError(self, exc):
        # Call the base class to close the connection with the SMTP server
        SMTPClient.sendError(self, exc)

        #  Do not retry to connect to SMTP Server if:
        #   1. No more retries left (This allows the correct error to be returned to the errorback)
        #   2. retry is false
        #   3. The error code is not in the 4xx range (Communication Errors)

        if (self.factory.retries >= 0 or
            (not exc.retry and not (exc.code >= 400 and exc.code < 500))):
            self.factory.sendFinished = 1
            self.factory.result.errback(exc)

    def sentMail(self, code, resp, numOk, addresses, log):
        # Do not retry, the SMTP server acknowledged the request
        self.factory.sendFinished = 1
        if code not in SUCCESS:
            errlog = []
            for addr, acode, aresp in addresses:
                if code not in SUCCESS:
                    errlog.append("%s: %03d %s" % (addr, acode, aresp))

            errlog.append(log.str())

            exc = SMTPDeliveryError(code, resp, '\n'.join(errlog), addresses)
            self.factory.result.errback(exc)
        else:
            self.factory.result.callback((numOk, addresses))


class SMTPSender(SenderMixin, SMTPClient):
    pass


class SMTPSenderFactory(protocol.ClientFactory):
    """
    Utility factory for sending emails easily.
    """

    domain = DNSNAME
    protocol = SMTPSender

    def __init__(self, fromEmail, toEmail, file, deferred, retries=5,
                 timeout=None):
        """
        @param fromEmail: The RFC 2821 address from which to send this
        message.

        @param toEmail: A sequence of RFC 2821 addresses to which to
        send this message.

        @param file: A file-like object containing the message to send.

        @param deferred: A Deferred to callback or errback when sending
        of this message completes.

        @param retries: The number of times to retry delivery of this
        message.

        @param timeout: Period, in seconds, for which to wait for
        server responses, or None to wait forever.
        """
        assert isinstance(retries, (int, long))

        if isinstance(toEmail, types.StringTypes):
            toEmail = [toEmail]
        self.fromEmail = Address(fromEmail)
        self.nEmails = len(toEmail)
        self.toEmail = iter(toEmail)
        self.file = file
        self.result = deferred
        self.result.addBoth(self._removeDeferred)
        self.sendFinished = 0

        self.retries = -retries
        self.timeout = timeout

    def _removeDeferred(self, argh):
        del self.result
        return argh

    def clientConnectionFailed(self, connector, err):
        self._processConnectionError(connector, err)

    def clientConnectionLost(self, connector, err):
        self._processConnectionError(connector, err)

    def _processConnectionError(self, connector, err):
        if self.retries < self.sendFinished <= 0:
            log.msg("SMTP Client retrying server. Retry: %s" % -self.retries)

            connector.connect()
            self.retries += 1
        elif self.sendFinished <= 0:
            # If we were unable to communicate with the SMTP server a ConnectionDone will be
            # returned. We want a more clear error message for debugging
            if err.check(error.ConnectionDone):
                err.value = SMTPConnectError(-1, "Unable to connect to server.")
            self.result.errback(err.value)

    def buildProtocol(self, addr):
        p = self.protocol(self.domain, self.nEmails*2+2)
        p.factory = self
        p.timeout = self.timeout
        return p


class IClientAuthentication(components.Interface):
    def getName(self):
        """Return an identifier associated with this authentication scheme.

        @rtype: C{str}
        """

    def challengeResponse(self, secret, challenge):
        """Generate a challenge response string"""


class CramMD5ClientAuthenticator:
    implements(IClientAuthentication)

    def __init__(self, user):
        self.user = user

    def getName(self):
        return "CRAM-MD5"

    def challengeResponse(self, secret, chal):
        response = hmac.HMAC(secret, chal).hexdigest()
        return '%s %s' % (self.user, response)


class LOGINAuthenticator:
    implements(IClientAuthentication)

    def __init__(self, user):
        self.user = user

    def getName(self):
        return "LOGIN"

    def challengeResponse(self, secret, chal):
        if chal== "Username:":
            return self.user
        elif chal == 'Password:':
            return secret

class PLAINAuthenticator:
    implements(IClientAuthentication)

    def __init__(self, user):
        self.user = user

    def getName(self):
        return "PLAIN"

    def challengeResponse(self, secret, chal=1):
        if chal == 1:
           return "%s\0%s\0%s" % (self.user, self.user, secret)
        else:
           return "%s\0%s" % (self.user, secret)


class ESMTPSender(SenderMixin, ESMTPClient):

    requireAuthentication = True
    requireTransportSecurity = True

    def __init__(self, username, secret, contextFactory=None, *args, **kw):
        self.heloFallback = 0
        self.username = username

        if contextFactory is None:
            contextFactory = self._getContextFactory()

        ESMTPClient.__init__(self, secret, contextFactory, *args, **kw)

        self._registerAuthenticators()

    def _registerAuthenticators(self):
        # Register Authenticator in order from most secure to least secure
        self.registerAuthenticator(CramMD5ClientAuthenticator(self.username))
        self.registerAuthenticator(LOGINAuthenticator(self.username))
        self.registerAuthenticator(PLAINAuthenticator(self.username))

    def _getContextFactory(self):
        if self.context is not None:
            return self.context
        try:
            from twisted.internet import ssl
        except ImportError:
            return None
        else:
            try:
                context = ssl.ClientContextFactory()
                context.method = ssl.SSL.TLSv1_METHOD
                return context
            except AttributeError:
                return None


class ESMTPSenderFactory(SMTPSenderFactory):
    """
    Utility factory for sending emails easily.
    """

    protocol = ESMTPSender

    def __init__(self, username, password, fromEmail, toEmail, file,
                 deferred, retries=5, timeout=None,
                 contextFactory=None, heloFallback=False,
                 requireAuthentication=True,
                 requireTransportSecurity=True):

        SMTPSenderFactory.__init__(self, fromEmail, toEmail, file, deferred, retries, timeout)
        self.username = username
        self.password = password
        self._contextFactory = contextFactory
        self._heloFallback = heloFallback
        self._requireAuthentication = requireAuthentication
        self._requireTransportSecurity = requireTransportSecurity

    def buildProtocol(self, addr):
        p = self.protocol(self.username, self.password, self._contextFactory, self.domain, self.nEmails*2+2)
        p.heloFallback = self._heloFallback
        p.requireAuthentication = self._requireAuthentication
        p.requireTransportSecurity = self._requireTransportSecurity
        p.factory = self
        p.timeout = self.timeout
        return p

def sendmail(smtphost, from_addr, to_addrs, msg, senderDomainName=None, port=25):
    """Send an email

    This interface is intended to be a direct replacement for
    smtplib.SMTP.sendmail() (with the obvious change that
    you specify the smtphost as well). Also, ESMTP options
    are not accepted, as we don't do ESMTP yet. I reserve the
    right to implement the ESMTP options differently.

    @param smtphost: The host the message should be sent to
    @param from_addr: The (envelope) address sending this mail.
    @param to_addrs: A list of addresses to send this mail to.  A string will
        be treated as a list of one address
    @param msg: The message, including headers, either as a file or a string.
        File-like objects need to support read() and close(). Lines must be
        delimited by '\\n'. If you pass something that doesn't look like a
        file, we try to convert it to a string (so you should be able to
        pass an email.Message directly, but doing the conversion with
        email.Generator manually will give you more control over the
        process).

    @param senderDomainName: Name by which to identify.  If None, try
    to pick something sane (but this depends on external configuration
    and may not succeed).

    @param port: Remote port to which to connect.

    @rtype: L{Deferred}
    @returns: A L{Deferred}, its callback will be called if a message is sent
        to ANY address, the errback if no message is sent.

        The callback will be called with a tuple (numOk, addresses) where numOk
        is the number of successful recipient addresses and addresses is a list
        of tuples (address, code, resp) giving the response to the RCPT command
        for each address.
    """
    if not hasattr(msg,'read'):
        # It's not a file
        msg = StringIO(str(msg))

    d = defer.Deferred()
    factory = SMTPSenderFactory(from_addr, to_addrs, msg, d)

    if senderDomainName is not None:
        factory.domain = senderDomainName

    reactor.connectTCP(smtphost, port, factory)

    return d

def sendEmail(smtphost, fromEmail, toEmail, content, headers = None, attachments = None, multipartbody = "mixed"):
    """Send an email, optionally with attachments.

    @type smtphost: str
    @param smtphost: hostname of SMTP server to which to connect

    @type fromEmail: str
    @param fromEmail: email address to indicate this email is from

    @type toEmail: str
    @param toEmail: email address to which to send this email

    @type content: str
    @param content: The body if this email.

    @type headers: dict
    @param headers: Dictionary of headers to include in the email

    @type attachments: list of 3-tuples
    @param attachments: Each 3-tuple should consist of the name of the
      attachment, the mime-type of the attachment, and a string that is
      the attachment itself.

    @type multipartbody: str
    @param multipartbody: The type of MIME multi-part body.  Generally
      either "mixed" (as in text and images) or "alternative" (html email
      with a fallback to text/plain).

    @rtype: Deferred
    @return: The returned Deferred has its callback or errback invoked when
      the mail is successfully sent or when an error occurs, respectively.
    """
    warnings.warn("smtp.sendEmail may go away in the future.\n"
                  "  Consider revising your code to use the email module\n"
                  "  and smtp.sendmail.",
                  category=DeprecationWarning, stacklevel=2)

    f = tempfile.TemporaryFile()
    writer = MimeWriter.MimeWriter(f)

    writer.addheader("Mime-Version", "1.0")
    if headers:
        # Setup the mail headers
        for (header, value) in headers.items():
            writer.addheader(header, value)

        headkeys = [k.lower() for k in headers.keys()]
    else:
        headkeys = ()

    # Add required headers if not present
    if "message-id" not in headkeys:
        writer.addheader("Message-ID", messageid())
    if "date" not in headkeys:
        writer.addheader("Date", rfc822date())
    if "from" not in headkeys and "sender" not in headkeys:
        writer.addheader("From", fromEmail)
    if "to" not in headkeys and "cc" not in headkeys and "bcc" not in headkeys:
        writer.addheader("To", toEmail)

    writer.startmultipartbody(multipartbody)

    # message body
    part = writer.nextpart()
    body = part.startbody("text/plain")
    body.write(content)

    if attachments is not None:
        # add attachments
        for (file, mime, attachment) in attachments:
            part = writer.nextpart()
            if mime.startswith('text'):
                encoding = "7bit"
            else:
                attachment = base64.encodestring(attachment)
                encoding = "base64"
            part.addheader("Content-Transfer-Encoding", encoding)
            body = part.startbody("%s; name=%s" % (mime, file))
            body.write(attachment)

    # finish
    writer.lastpart()

    # send message
    f.seek(0, 0)
    d = defer.Deferred()
    factory = SMTPSenderFactory(fromEmail, toEmail, f, d)
    reactor.connectTCP(smtphost, 25, factory)

    return d

##
## Yerg.  Codecs!
##
import codecs
def xtext_encode(s):
    r = []
    for ch in s:
        o = ord(ch)
        if ch == '+' or ch == '=' or o < 33 or o > 126:
            r.append('+%02X' % o)
        else:
            r.append(ch)
    return (''.join(r), len(s))

try:
    from twisted.protocols._c_urlarg import unquote as _helper_unquote
except ImportError:
    def xtext_decode(s):
        r = []
        i = 0
        while i < len(s):
            if s[i] == '+':
                try:
                    r.append(chr(int(s[i + 1:i + 3], 16)))
                except ValueError:
                    r.append(s[i:i + 3])
                i += 3
            else:
                r.append(s[i])
                i += 1
        return (''.join(r), len(s))
else:
    def xtext_decode(s):
        return (_helper_unquote(s, '+'), len(s))

class xtextStreamReader(codecs.StreamReader):
    def decode(self, s, errors='strict'):
        return xtext_decode(s)

class xtextStreamWriter(codecs.StreamWriter):
    def decode(self, s, errors='strict'):
        return xtext_encode(s)

def xtext_codec(name):
    if name == 'xtext':
        return (xtext_encode, xtext_decode, xtextStreamReader, xtextStreamWriter)
codecs.register(xtext_codec)
