| Index: third_party/twisted_8_1/twisted/mail/smtp.py
|
| diff --git a/third_party/twisted_8_1/twisted/mail/smtp.py b/third_party/twisted_8_1/twisted/mail/smtp.py
|
| deleted file mode 100644
|
| index 54b561e237d7e60c4c61168c7157da8a4b12be0b..0000000000000000000000000000000000000000
|
| --- a/third_party/twisted_8_1/twisted/mail/smtp.py
|
| +++ /dev/null
|
| @@ -1,2016 +0,0 @@
|
| -# -*- 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 util
|
| -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, re, base64, types, socket, os, random, hmac
|
| -import MimeWriter, tempfile, rfc822
|
| -import warnings
|
| -import binascii
|
| -
|
| -from zope.interface import implements, Interface
|
| -
|
| -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(Interface):
|
| - def receivedHeader(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(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(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(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():
|
| - """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(Interface):
|
| - """Interface definition for messages that can be sent via SMTP."""
|
| -
|
| - def lineReceived(line):
|
| - """handle another line"""
|
| -
|
| - def eomReceived():
|
| - """handle end of message
|
| -
|
| - return a deferred. The deferred should be called with either:
|
| - callback(string) or errback(error)
|
| - """
|
| -
|
| - def connectionLost():
|
| - """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' % (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
|
| -
|
| - validated = defer.maybeDeferred(self.validateFrom, self._helo, addr)
|
| - validated.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 from 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, "SMTP sender validation 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 _cbAnonymousAuthentication(self, (iface, avatar, logout)):
|
| - """
|
| - Save the state resulting from a successful anonymous cred login.
|
| - """
|
| - 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.challenger = None
|
| -
|
| -
|
| - # 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:
|
| -
|
| - result = self.portal.login(
|
| - cred.credentials.Anonymous(),
|
| - None,
|
| - IMessageDeliveryFactory, IMessageDelivery)
|
| -
|
| - def ebAuthentication(err):
|
| - """
|
| - Translate cred exceptions into SMTP exceptions so that the
|
| - protocol code which invokes C{validateFrom} can properly report
|
| - the failure.
|
| - """
|
| - if err.check(cred.error.UnauthorizedLogin):
|
| - exc = SMTPBadSender(origin)
|
| - elif err.check(cred.error.UnhandledCredentials):
|
| - exc = SMTPBadSender(
|
| - origin, resp="Unauthenticated senders not allowed")
|
| - else:
|
| - return err
|
| - return defer.fail(exc)
|
| -
|
| - result.addCallbacks(
|
| - self._cbAnonymousAuthentication, ebAuthentication)
|
| -
|
| - def continueValidation(ignored):
|
| - """
|
| - Re-attempt from address validation.
|
| - """
|
| - return self.validateFrom(helo, origin)
|
| -
|
| - result.addCallback(continueValidation)
|
| - return result
|
| -
|
| - 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 %s (%s)" % (domain,
|
| - self.__class__.__name__,
|
| - 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 greeting(self):
|
| - return SMTP.greeting(self) + ' ESMTP'
|
| -
|
| -
|
| - 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.mode = AUTH
|
| - self.challenger = chal
|
| -
|
| - if len(parts) > 1:
|
| - chal.getChallenge() # Discard it, apparently the client does not
|
| - # care about it.
|
| - rest = parts[1]
|
| - else:
|
| - rest = None
|
| - self.state_AUTH(rest)
|
| -
|
| -
|
| - def _cbAuthenticated(self, loginInfo):
|
| - """
|
| - Save the state resulting from a successful cred login and mark this
|
| - connection as authenticated.
|
| - """
|
| - result = SMTP._cbAnonymousAuthentication(self, loginInfo)
|
| - self.authenticated = True
|
| - return result
|
| -
|
| -
|
| - def _ebAuthenticated(self, reason):
|
| - """
|
| - Handle cred login errors by translating them to the SMTP authenticate
|
| - failed. Translate all other errors into a generic SMTP error code and
|
| - log the failure for inspection. Stop all errors from propagating.
|
| - """
|
| - self.challenge = None
|
| - if reason.check(cred.error.UnauthorizedLogin):
|
| - self.sendCode(535, 'Authentication failed')
|
| - else:
|
| - log.err(failure, "SMTP authentication failure")
|
| - self.sendCode(
|
| - 451,
|
| - 'Requested action aborted: local error in processing')
|
| -
|
| -
|
| - def state_AUTH(self, response):
|
| - """
|
| - Handle one step of challenge/response authentication.
|
| -
|
| - @param response: The text of a response. If None, this
|
| - function has been called as a result of an AUTH command with
|
| - no initial response. A response of '*' aborts authentication,
|
| - as per RFC 2554.
|
| - """
|
| - if self.portal is None:
|
| - self.sendCode(454, 'Temporary authentication failure')
|
| - self.mode = COMMAND
|
| - return
|
| -
|
| - if response is None:
|
| - challenge = self.challenger.getChallenge()
|
| - encoded = challenge.encode('base64')
|
| - self.sendCode(334, encoded)
|
| - return
|
| -
|
| - if response == '*':
|
| - self.sendCode(501, 'Authentication aborted')
|
| - self.challenger = None
|
| - self.mode = COMMAND
|
| - return
|
| -
|
| - try:
|
| - uncoded = response.decode('base64')
|
| - except binascii.Error:
|
| - self.sendCode(501, 'Syntax error in parameters or arguments')
|
| - self.challenger = None
|
| - self.mode = COMMAND
|
| - return
|
| -
|
| - self.challenger.setResponse(uncoded)
|
| - if self.challenger.moreChallenges():
|
| - challenge = self.challenger.getChallenge()
|
| - coded = challenge.encode('base64')[:-1]
|
| - self.sendCode(334, coded)
|
| - return
|
| -
|
| - self.mode = COMMAND
|
| - result = self.portal.login(
|
| - self.challenger, None,
|
| - IMessageDeliveryFactory, IMessageDelivery)
|
| - result.addCallback(self._cbAuthenticated)
|
| - result.addCallback(lambda ign: self.sendCode(235, 'Authentication successful.'))
|
| - result.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(Interface):
|
| - def getName():
|
| - """Return an identifier associated with this authentication scheme.
|
| -
|
| - @rtype: C{str}
|
| - """
|
| -
|
| - def challengeResponse(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)
|
|
|