123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247 |
- # -*- test-case-name: twisted.mail.test.test_smtp -*-
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
- #
- # pylint: disable=I0011,C0103,C9302
- """
- Simple Mail Transfer Protocol implementation.
- """
- from __future__ import absolute_import, division
- import time
- import re
- import base64
- import socket
- import os
- import random
- import binascii
- import warnings
- from email.utils import parseaddr
- from zope.interface import implementer
- from twisted import cred
- 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, ISSLTransport
- from twisted.python import log
- from twisted.python import util
- from twisted.python.compat import (_PY3, range, long, unicode, networkString,
- nativeString, iteritems, _keys, _bytesChr,
- iterbytes)
- from twisted.python.runtime import platform
- from twisted.mail.interfaces import (IClientAuthentication,
- IMessageSMTP as IMessage,
- IMessageDeliveryFactory, IMessageDelivery)
- from twisted.mail._cred import (CramMD5ClientAuthenticator, LOGINAuthenticator,
- LOGINCredentials as _lcredentials)
- from twisted.mail._except import (
- AUTHDeclinedError, AUTHRequiredError, AddressError,
- AuthenticationError, EHLORequiredError, ESMTPClientError,
- SMTPAddressError, SMTPBadRcpt, SMTPBadSender, SMTPClientError,
- SMTPConnectError, SMTPDeliveryError, SMTPError, SMTPServerError,
- SMTPTimeoutError, SMTPTLSError as TLSError, TLSRequiredError,
- SMTPProtocolError)
- from io import BytesIO
- __all__ = [
- 'AUTHDeclinedError', 'AUTHRequiredError', 'AddressError',
- 'AuthenticationError', 'EHLORequiredError', 'ESMTPClientError',
- 'SMTPAddressError', 'SMTPBadRcpt', 'SMTPBadSender', 'SMTPClientError',
- 'SMTPConnectError', 'SMTPDeliveryError', 'SMTPError', 'SMTPServerError',
- 'SMTPTimeoutError', 'TLSError', 'TLSRequiredError', 'SMTPProtocolError',
- 'IClientAuthentication', 'IMessage', 'IMessageDelivery',
- 'IMessageDeliveryFactory',
- 'CramMD5ClientAuthenticator', 'LOGINAuthenticator', 'LOGINCredentials',
- 'PLAINAuthenticator',
- 'Address', 'User', 'sendmail', 'SenderMixin',
- 'ESMTP', 'ESMTPClient', 'ESMTPSender', 'ESMTPSenderFactory',
- 'SMTP', 'SMTPClient', 'SMTPFactory', 'SMTPSender', 'SMTPSenderFactory',
- 'idGenerator', 'messageid', 'quoteaddr', 'rfc822date', 'xtextStreamReader',
- 'xtextStreamWriter', 'xtext_codec', 'xtext_decode', 'xtext_encode'
- ]
- # Cache the hostname (XXX Yes - this is broken)
- if platform.isMacOSX():
- # On macOS, getfqdn() is ridiculously slow - use the
- # probably-identical-but-sometimes-not gethostname() there.
- DNSNAME = socket.gethostname()
- else:
- DNSNAME = socket.getfqdn()
- # Encode the DNS name into something we can send over the wire
- DNSNAME = DNSNAME.encode('ascii')
- # Used for fast success code lookup
- SUCCESS = dict.fromkeys(range(200, 300))
- 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 L{bytes} 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 networkString("%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
- _gen = idGenerator()
- def messageid(uniq=None, N=lambda: next(_gen)):
- """
- 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 strengthen uniqueness if given.
- """
- datetime = time.strftime('%Y%m%d%H%M%S', time.gmtime())
- pid = os.getpid()
- rand = random.randrange(2**31-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 b'<' + bytes(addr) + b'>'
- if isinstance(addr, bytes):
- addr = addr.decode('ascii')
- res = parseaddr(addr)
- if res == (None, None):
- # It didn't parse, use it as-is
- return b'<' + bytes(addr) + b'>'
- else:
- return b'<' + res[1].encode('ascii') + b'>'
- COMMAND, DATA, AUTH = 'COMMAND', 'DATA', 'AUTH'
- # Character classes for parsing addresses
- atom = br"[-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{bytes}
- @ivar domain: The domain within which this address resides.
- @type local: C{bytes}
- @ivar local: The local (\"user\") portion of this address.
- """
- tstring = re.compile(br'''( # A string of
- (?:"[^"]*" # quoted string
- |\\. # backslash-escaped characted
- |''' + atom + br''' # 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, bytes):
- addr = str(addr).encode('ascii')
- self.addrstr = addr
- # Tokenize
- atl = list(filter(None, self.tstring.split(addr)))
- local = []
- domain = []
- while atl:
- if atl[0] == b'<':
- if atl[-1] != b'>':
- raise AddressError("Unbalanced <>")
- atl = atl[1:-1]
- elif atl[0] == b'@':
- atl = atl[1:]
- if not local:
- # Source route
- while atl and atl[0] != b':':
- # 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 = [b'']
- elif (len(atl[0]) == 1 and
- not self.atomre.match(atl[0]) and
- atl[0] != b'.'):
- 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 = b''.join(local)
- self.domain = b''.join(domain)
- if self.local != b'' and self.domain == b'':
- if defaultDomain is None:
- defaultDomain = DNSNAME
- self.domain = defaultDomain
- dequotebs = re.compile(br'\\(.)')
- def dequote(self, addr):
- """
- Remove RFC-2821 quotes from address.
- """
- res = []
- if not isinstance(addr, bytes):
- addr = str(addr).encode('ascii')
- atl = filter(None, self.tstring.split(addr))
- for t in atl:
- if t[0] == b'"' and t[-1] == b'"':
- res.append(t[1:-1])
- elif '\\' in t:
- res.append(self.dequotebs.sub(br'\1', t))
- else:
- res.append(t)
- return b''.join(res)
- if _PY3:
- def __str__(self):
- return nativeString(bytes(self))
- else:
- def __str__(self):
- return self.__bytes__()
- def __bytes__(self):
- if self.local or self.domain:
- return b'@'.join((self.local, self.domain))
- else:
- return b''
- 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):
- try:
- host = protocol.host
- except AttributeError:
- 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 nativeString(bytes(self.dest))
- def __bytes__(self):
- return bytes(self.dest)
- class SMTP(basic.LineOnlyReceiver, policies.TimeoutMixin):
- """
- SMTP server-side protocol.
- @ivar host: The hostname of this mail server.
- @type host: L{bytes}
- """
- timeout = 600
- 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
- self.host = DNSNAME
- @property
- def host(self):
- return self._host
- @host.setter
- def host(self, toSet):
- if not isinstance(toSet, bytes):
- toSet = str(toSet).encode('ascii')
- self._host = toSet
- def timeoutConnection(self):
- msg = self.host + b' Timeout. Try talking faster next time!'
- self.sendCode(421, msg)
- self.transport.loseConnection()
- def greeting(self):
- return self.host + b' NO UCE NO UBE NO RELAY PROBES'
- 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=b''):
- """
- Send an SMTP code with a message.
- """
- lines = message.splitlines()
- lastline = lines[-1:]
- for line in lines[:-1]:
- self.sendLine(networkString('%3.3d-' % (code,)) + line)
- self.sendLine(networkString('%3.3d ' % (code,)) +
- (lastline and lastline[0] or b''))
- 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(b'')
- else:
- self.sendSyntaxError()
- def sendSyntaxError(self):
- self.sendCode(500, b'Error: bad syntax')
- def lookupMethod(self, command):
- """
- @param command: The command to get from this class.
- @type command: L{str}
- @return: The function which executes this command.
- """
- if not isinstance(command, str):
- command = nativeString(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, b'Line too long')
- def do_UNKNOWN(self, rest):
- self.sendCode(500, b'Command not implemented')
- def do_HELO(self, rest):
- peer = self.transport.getPeer()
- try:
- host = peer.host
- except AttributeError:
- host = str(peer)
- if not isinstance(host, bytes):
- host = host.encode('idna')
- self._helo = (rest, host)
- self._from = None
- self._to = []
- self.sendCode(250,
- self.host + b' Hello ' + host + b', nice to meet you')
- def do_QUIT(self, rest):
- self.sendCode(221, b'See you later')
- self.transport.loseConnection()
- # A string of quoted strings, backslash-escaped character or
- # atom characters + '@.,:'
- qstring = br'("[^"]*"|\\.|' + atom + br'|[@.,:])+'
- mail_re = re.compile(br'''\s*FROM:\s*(?P<path><> # Empty <>
- |<''' + qstring + br'''> # <addr>
- |''' + qstring + br''' # addr
- )\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options
- $''', re.I|re.X)
- rcpt_re = re.compile(br'\s*TO:\s*(?P<path><' + qstring + br'''> # <addr>
- |''' + qstring + br''' # addr
- )\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options
- $''', re.I|re.X)
- def do_MAIL(self, rest):
- if self._from:
- self.sendCode(503, b"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, b"Syntax error")
- return
- try:
- addr = Address(m.group('path'), self.host)
- except AddressError as e:
- self.sendCode(553, networkString(str(e)))
- return
- validated = defer.maybeDeferred(self.validateFrom, self._helo, addr)
- validated.addCallbacks(self._cbFromValidate, self._ebFromValidate)
- def _cbFromValidate(self, fromEmail, code=250,
- msg=b'Sender address accepted'):
- self._from = fromEmail
- self.sendCode(code, msg)
- def _ebFromValidate(self, failure):
- if failure.check(SMTPBadSender):
- self.sendCode(failure.value.code,
- (b'Cannot receive from specified address ' +
- quoteaddr(failure.value.addr) + b': ' +
- networkString(failure.value.resp)))
- elif failure.check(SMTPServerError):
- self.sendCode(failure.value.code,
- networkString(failure.value.resp))
- else:
- log.err(failure, "SMTP sender validation failure")
- self.sendCode(
- 451,
- b'Requested action aborted: local error in processing')
- def do_RCPT(self, rest):
- if not self._from:
- self.sendCode(503, b"Must have sender before recipient")
- return
- m = self.rcpt_re.match(rest)
- if not m:
- self.sendCode(501, b"Syntax error")
- return
- try:
- user = User(m.group('path'), self._helo, self, self._from)
- except AddressError as e:
- self.sendCode(553, networkString(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=b'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,
- networkString(failure.value.resp))
- else:
- log.err(failure)
- self.sendCode(
- 451,
- b'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, b'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 as e:
- self.sendCode(e.code, e.resp)
- self.mode = COMMAND
- self._disconnect(msgs)
- return
- except:
- log.err()
- self.sendCode(550, b"Internal server error")
- self.mode = COMMAND
- self._disconnect(msgs)
- return
- self.__messages = msgs
- self.__inheader = self.__inbody = 0
- self.sendCode(354, b'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, b'I remember nothing.')
- def dataLineReceived(self, line):
- if line[:1] == b'.':
- if line == b'.':
- 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 b':' in line:
- self.__inheader = 1
- elif line:
- for message in self.__messages:
- message.lineReceived(b'')
- self.__inbody = 1
- if not line:
- self.__inbody = 1
- for message in self.__messages:
- message.lineReceived(line)
- except SMTPServerError as 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'
- resultLen = len(resultList)
- if resultLen > 1:
- msg += ' (%d failures out of %d recipients)'.format(
- failures, resultLen)
- self.sendCode(550, networkString(msg))
- else:
- self.sendCode(250, b'Delivery in progress')
- def _cbAnonymousAuthentication(self, result):
- """
- Save the state resulting from a successful anonymous cred login.
- """
- (iface, avatar, logout) = result
- 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{(bytes, bytes)}
- @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: L{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 = b""
- if helo[0]:
- heloStr = b" helo=" + helo[0]
- domain = networkString(self.transport.getHost().host)
- from_ = b"from " + helo[0] + b" ([" + helo[1] + b"]" + heloStr + b")"
- by = b"by %s with %s (%s)" % (domain,
- self.__class__.__name__,
- longversion)
- for_ = b"for %s; %s" % (' '.join(map(str, recipients)),
- rfc822date())
- return b"Received: " + from_ + b"\n\t" + by + b"\n\t" + for_
- 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.
- After the client has connected to the SMTP server, it repeatedly calls
- L{SMTPClient.getMailFrom}, L{SMTPClient.getMailTo} and
- L{SMTPClient.getMailData} and uses this information to send an email.
- It then calls L{SMTPClient.getMailFrom} again; if it returns L{None}, the
- client will disconnect, otherwise it will continue as normal i.e. call
- L{SMTPClient.getMailTo} and L{SMTPClient.getMailData} and send a new email.
- """
- # 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):
- if isinstance(identity, unicode):
- identity = identity.encode('ascii')
- self.identity = identity or b''
- 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(b'>>> ' + 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, b"Timeout waiting for SMTP server response",
- self.log.str()))
- def lineReceived(self, line):
- self.resetTimeout()
- # Log lineReceived only if you are in debug mode for performance
- if self.debug:
- self.log.append(b'<<< ' + 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: {}".format(line),
- self.log.str()))
- return
- if line[0:1] == b'0':
- # Verbose informational message, ignore it
- return
- self.resp.append(line[4:])
- if line[3:4] == b'-':
- # Continuation
- return
- if self.code in self._expected:
- why = self._okresponse(self.code, b'\n'.join(self.resp))
- else:
- why = self._failresponse(self.code, b'\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(b'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(b'MAIL FROM:' + 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 = range(0, 1000)
- self.lastAddress = None
- return self.smtpState_toOrData(0, b'')
- 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 = next(self.toAddresses)
- except StopIteration:
- if self.successAddresses:
- self.sendLine(b'DATA')
- self._expected = [ 354 ]
- self._okresponse = self.smtpState_data
- else:
- return self.smtpState_msgSent(code,'No recipients accepted')
- else:
- self.sendLine(b'RCPT TO:' + quoteaddr(self.lastAddress))
- def smtpState_data(self, code, resp):
- s = basic.FileSender()
- d = s.beginFileTransfer(
- self.getMailData(), self.transport, self.transformChunk)
- def ebTransfer(err):
- self.sendError(err.value)
- d.addCallbacks(self.finishedFileTransfer, ebTransfer)
- 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(b'RSET')
- self._expected = SUCCESS
- self._okresponse = self.smtpState_from
- ##
- ## Helpers for FileSender
- ##
- def transformChunk(self, chunk):
- """
- Perform the necessary local to network newline conversion and escape
- leading periods.
- This method also resets the idle timeout so that as long as process is
- being made sending the message body, the client will not time out.
- """
- self.resetTimeout()
- return chunk.replace(b'\n', b'\r\n').replace(b'\r\n.', b'\r\n..')
- def finishedFileTransfer(self, lastsent):
- if lastsent != b'\n':
- line = b'\r\n.'
- else:
- line = b'.'
- self.sendLine(line)
- ##
- # these methods should be overridden 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}
- """
- if isinstance(exc, SMTPClientError) and not exc.isFatal:
- self._disconnectFromServer()
- else:
- # 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)
- 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 = range(0, 1000)
- self._okresponse = self.smtpState_disconnect
- self.sendLine(b'QUIT')
- class ESMTPClient(SMTPClient):
- """
- A client for sending emails over ESMTP.
- @ivar heloFallback: Whether or not to fall back to plain SMTP if the C{EHLO}
- command is not recognised by the server. If L{requireAuthentication} is
- C{True}, or L{requireTransportSecurity} is C{True} and the connection is
- not over TLS, this fallback flag will not be honored.
- @type heloFallback: L{bool}
- @ivar requireAuthentication: If C{True}, refuse to proceed if authentication
- cannot be performed. Overrides L{heloFallback}.
- @type requireAuthentication: L{bool}
- @ivar requireTransportSecurity: If C{True}, refuse to proceed if the
- transport cannot be secured. If the transport layer is not already
- secured via TLS, this will override L{heloFallback}.
- @type requireAuthentication: L{bool}
- @ivar context: The context factory to use for STARTTLS, if desired.
- @type context: L{ssl.ClientContextFactory}
- @ivar _tlsMode: Whether or not the connection is over TLS.
- @type _tlsMode: L{bool}
- """
- heloFallback = True
- requireAuthentication = False
- requireTransportSecurity = False
- context = None
- _tlsMode = False
- def __init__(self, secret, contextFactory=None, *args, **kw):
- SMTPClient.__init__(self, *args, **kw)
- self.authenticators = []
- self.secret = secret
- self.context = contextFactory
- def __getattr__(self, name):
- if name == "tlsMode":
- warnings.warn(
- "tlsMode attribute of twisted.mail.smtp.ESMTPClient "
- "is deprecated since Twisted 13.0",
- category=DeprecationWarning, stacklevel=2)
- return self._tlsMode
- else:
- raise AttributeError(
- '%s instance has no attribute %r' % (
- self.__class__.__name__, name,))
- def __setattr__(self, name, value):
- if name == "tlsMode":
- warnings.warn(
- "tlsMode attribute of twisted.mail.smtp.ESMTPClient "
- "is deprecated since Twisted 13.0",
- category=DeprecationWarning, stacklevel=2)
- self._tlsMode = value
- else:
- self.__dict__[name] = value
- def esmtpEHLORequired(self, code=-1, resp=None):
- """
- Fail because authentication is required, but the server does not support
- ESMTP, which is required for authentication.
- @param code: The server status code from the most recently received
- server message.
- @type code: L{int}
- @param resp: The server status response from the most recently received
- server message.
- @type resp: L{bytes}
- """
- self.sendError(EHLORequiredError(502, b"Server does not support ESMTP "
- b"Authentication", self.log.str()))
- def esmtpAUTHRequired(self, code=-1, resp=None):
- """
- Fail because authentication is required, but the server does not support
- any schemes we support.
- @param code: The server status code from the most recently received
- server message.
- @type code: L{int}
- @param resp: The server status response from the most recently received
- server message.
- @type resp: L{bytes}
- """
- tmp = []
- for a in self.authenticators:
- tmp.append(a.getName().upper())
- auth = b"[%s]" % b", ".join(tmp)
- self.sendError(AUTHRequiredError(502, b"Server does not support Client "
- b"Authentication schemes %s" % auth, self.log.str()))
- def esmtpTLSRequired(self, code=-1, resp=None):
- """
- Fail because TLS is required and the server does not support it.
- @param code: The server status code from the most recently received
- server message.
- @type code: L{int}
- @param resp: The server status response from the most recently received
- server message.
- @type resp: L{bytes}
- """
- self.sendError(TLSRequiredError(502, b"Server does not support secure "
- b"communication via TLS / SSL", self.log.str()))
- def esmtpTLSFailed(self, code=-1, resp=None):
- """
- Fail because the TLS handshake wasn't able to be completed.
- @param code: The server status code from the most recently received
- server message.
- @type code: L{int}
- @param resp: The server status response from the most recently received
- server message.
- @type resp: L{bytes}
- """
- self.sendError(TLSError(code, b"Could not complete the SSL/TLS "
- b"handshake", self.log.str()))
- def esmtpAUTHDeclined(self, code=-1, resp=None):
- """
- Fail because the authentication was rejected.
- @param code: The server status code from the most recently received
- server message.
- @type code: L{int}
- @param resp: The server status response from the most recently received
- server message.
- @type resp: L{bytes}
- """
- self.sendError(AUTHDeclinedError(code, resp, self.log.str()))
- def esmtpAUTHMalformedChallenge(self, code=-1, resp=None):
- """
- Fail because the server sent a malformed authentication challenge.
- @param code: The server status code from the most recently received
- server message.
- @type code: L{int}
- @param resp: The server status response from the most recently received
- server message.
- @type resp: L{bytes}
- """
- self.sendError(AuthenticationError(501, b"Login failed because the "
- b"SMTP Server returned a malformed Authentication Challenge",
- self.log.str()))
- def esmtpAUTHServerError(self, code=-1, resp=None):
- """
- Fail because of some other authentication error.
- @param code: The server status code from the most recently received
- server message.
- @type code: L{int}
- @param resp: The server status response from the most recently received
- server message.
- @type resp: L{bytes}
- """
- 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: L{IClientAuthentication} implementor
- @return: L{None}
- """
- self.authenticators.append(auth)
- def connectionMade(self):
- """
- Called when a connection has been made, and triggers sending an C{EHLO}
- to the server.
- """
- self._tlsMode = ISSLTransport.providedBy(self.transport)
- SMTPClient.connectionMade(self)
- self._okresponse = self.esmtpState_ehlo
- def esmtpState_ehlo(self, code, resp):
- """
- Send an C{EHLO} to the server.
- If L{heloFallback} is C{True}, and there is no requirement for TLS or
- authentication, the client will fall back to basic SMTP.
- @param code: The server status code from the most recently received
- server message.
- @type code: L{int}
- @param resp: The server status response from the most recently received
- server message.
- @type resp: L{bytes}
- @return: L{None}
- """
- self._expected = SUCCESS
- self._okresponse = self.esmtpState_serverConfig
- self._failresponse = self.esmtpEHLORequired
- if self._tlsMode:
- needTLS = False
- else:
- needTLS = self.requireTransportSecurity
- if self.heloFallback and not self.requireAuthentication and not needTLS:
- self._failresponse = self.smtpState_helo
- self.sendLine(b"EHLO " + self.identity)
- def esmtpState_serverConfig(self, code, resp):
- """
- Handle a positive response to the I{EHLO} command by parsing the
- capabilities in the server's response and then taking the most
- appropriate next step towards entering a mail transaction.
- """
- 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
- self.tryTLS(code, resp, items)
- def tryTLS(self, code, resp, items):
- """
- Take a necessary step towards being able to begin a mail transaction.
- The step may be to ask the server to being a TLS session. If TLS is
- already in use or not necessary and not available then the step may be
- to authenticate with the server. If TLS is necessary and not available,
- fail the mail transmission attempt.
- This is an internal helper method.
- @param code: The server status code from the most recently received
- server message.
- @type code: L{int}
- @param resp: The server status response from the most recently received
- server message.
- @type resp: L{bytes}
- @param items: A mapping of ESMTP extensions offered by the server. Keys
- are extension identifiers and values are the associated values.
- @type items: L{dict} mapping L{bytes} to L{bytes}
- @return: L{None}
- """
- # has tls can tls must tls result
- # t t t authenticate
- # t t f authenticate
- # t f t authenticate
- # t f f authenticate
- # f t t STARTTLS
- # f t f STARTTLS
- # f f t esmtpTLSRequired
- # f f f authenticate
- hasTLS = self._tlsMode
- canTLS = self.context and b"STARTTLS" in items
- mustTLS = self.requireTransportSecurity
- if hasTLS or not (canTLS or mustTLS):
- self.authenticate(code, resp, items)
- elif canTLS:
- self._expected = [220]
- self._okresponse = self.esmtpState_starttls
- self._failresponse = self.esmtpTLSFailed
- self.sendLine(b"STARTTLS")
- else:
- self.esmtpTLSRequired()
- def esmtpState_starttls(self, code, resp):
- """
- Handle a positive response to the I{STARTTLS} command by starting a new
- TLS session on C{self.transport}.
- Upon success, re-handshake with the server to discover what capabilities
- it has when TLS is in use.
- """
- 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(b'AUTH'):
- schemes = items[b'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 == b"PLAIN":
- self._okresponse = self.smtpState_from
- self._failresponse = self._esmtpState_plainAuth
- self._expected = [235]
- challenge = base64.b64encode(
- self._authinfo.challengeResponse(self.secret, 1))
- self.sendLine(b"AUTH %s %s" % (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(b'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 = base64.b64encode(
- self._authinfo.challengeResponse(self.secret, 2))
- self.sendLine(b'AUTH PLAIN ' + challenge)
- def esmtpState_challenge(self, code, resp):
- self._authResponse(self._authinfo, resp)
- def _authResponse(self, auth, challenge):
- self._failresponse = self.esmtpAUTHDeclined
- try:
- challenge = base64.b64decode(challenge)
- except binascii.Error:
- # Illegal challenge, give up, then quit
- self.sendLine(b'*')
- self._okresponse = self.esmtpAUTHMalformedChallenge
- self._failresponse = self.esmtpAUTHMalformedChallenge
- else:
- resp = auth.challengeResponse(self.secret, challenge)
- self._expected = [235, 334]
- self._okresponse = self.smtpState_maybeAuthenticated
- self.sendLine(base64.b64encode(resp))
- def smtpState_maybeAuthenticated(self, code, resp):
- """
- Called to handle the next message from the server after sending a
- response to a SASL challenge. The server response might be another
- challenge or it might indicate authentication has succeeded.
- """
- if code == 235:
- # Yes, authenticated!
- del self._authinfo
- self.smtpState_from(code, resp)
- else:
- # No, not authenticated yet. Keep trying.
- self._authResponse(self._authinfo, resp)
- 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) + b' ESMTP'
- def extensions(self):
- """
- SMTP service extensions
- @return: the SMTP service extensions that are supported.
- @rtype: L{dict} with L{bytes} keys and a value of either L{None} or a
- L{list} of L{bytes}.
- """
- ext = {b'AUTH': _keys(self.challengers)}
- if self.canStartTLS and not self.startedTLS:
- ext[b'STARTTLS'] = None
- return ext
- def lookupMethod(self, command):
- command = nativeString(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 iteritems(self.extensions()):
- if v is not None:
- if v:
- # Intentionally omit extensions with empty argument lists
- r.append(c + b' ' + b' '.join(v))
- else:
- r.append(c)
- return b'\n'.join(r)
- def do_EHLO(self, rest):
- peer = self.transport.getPeer().host
- if not isinstance(peer, bytes):
- peer = peer.encode('idna')
- self._helo = (rest, peer)
- self._from = None
- self._to = []
- self.sendCode(
- 250,
- (self.host + b' Hello ' + peer + b', nice to meet you\n' +
- self.listExtensions())
- )
- def ext_STARTTLS(self, rest):
- if self.startedTLS:
- self.sendCode(503, b'TLS already negotiated')
- elif self.ctx and self.canStartTLS:
- self.sendCode(220, b'Begin TLS negotiation now')
- self.transport.startTLS(self.ctx)
- self.startedTLS = True
- else:
- self.sendCode(454, b'TLS not available')
- def ext_AUTH(self, rest):
- if self.authenticated:
- self.sendCode(503, b'Already authenticated')
- return
- parts = rest.split(None, 1)
- chal = self.challengers.get(parts[0].upper(), lambda: None)()
- if not chal:
- self.sendCode(504, b'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.
- @param reason: Reason for failure.
- """
- self.challenge = None
- if reason.check(cred.error.UnauthorizedLogin):
- self.sendCode(535, b'Authentication failed')
- else:
- log.err(reason, "SMTP authentication failure")
- self.sendCode(
- 451,
- b'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, b'Temporary authentication failure')
- self.mode = COMMAND
- return
- if response is None:
- challenge = self.challenger.getChallenge()
- encoded = base64.b64encode(challenge)
- self.sendCode(334, encoded)
- return
- if response == b'*':
- self.sendCode(501, b'Authentication aborted')
- self.challenger = None
- self.mode = COMMAND
- return
- try:
- uncoded = base64.b64decode(response)
- except (TypeError, binascii.Error):
- self.sendCode(501, b'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 = base64.b64encode(challenge)
- 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,
- b'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 = True
- 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 = True
- if code not in SUCCESS:
- errlog = []
- for addr, acode, aresp in addresses:
- if acode not in SUCCESS:
- errlog.append((addr + b": " +
- networkString("%03d" % (acode,)) +
- b" " + aresp))
- errlog.append(log.str())
- exc = SMTPDeliveryError(code, resp, b'\n'.join(errlog), addresses)
- self.factory.result.errback(exc)
- else:
- self.factory.result.callback((numOk, addresses))
- class SMTPSender(SenderMixin, SMTPClient):
- """
- SMTP protocol that sends a single email based on information it
- gets from its factory, a L{SMTPSenderFactory}.
- """
- class SMTPSenderFactory(protocol.ClientFactory):
- """
- Utility factory for sending emails easily.
- @type currentProtocol: L{SMTPSender}
- @ivar currentProtocol: The current running protocol returned by
- L{buildProtocol}.
- @type sendFinished: C{bool}
- @ivar sendFinished: When the value is set to True, it means the message has
- been sent or there has been an unrecoverable error or the sending has
- been cancelled. The default value is False.
- """
- 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.
- @type deferred: L{defer.Deferred}
- @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, unicode):
- toEmail = [toEmail.encode('ascii')]
- elif isinstance(toEmail, bytes):
- toEmail = [toEmail]
- else:
- toEmailFinal = []
- for _email in toEmail:
- if not isinstance(_email, bytes):
- _email = _email.encode('ascii')
- toEmailFinal.append(_email)
- toEmail = toEmailFinal
- self.fromEmail = Address(fromEmail)
- self.nEmails = len(toEmail)
- self.toEmail = toEmail
- self.file = file
- self.result = deferred
- self.result.addBoth(self._removeDeferred)
- self.sendFinished = False
- self.currentProtocol = None
- self.retries = -retries
- self.timeout = timeout
- def _removeDeferred(self, result):
- del self.result
- return result
- def clientConnectionFailed(self, connector, err):
- self._processConnectionError(connector, err)
- def clientConnectionLost(self, connector, err):
- self._processConnectionError(connector, err)
- def _processConnectionError(self, connector, err):
- self.currentProtocol = None
- if (self.retries < 0) and (not self.sendFinished):
- log.msg("SMTP Client retrying server. Retry: %s" % -self.retries)
- # Rewind the file in case part of it was read while attempting to
- # send the message.
- self.file.seek(0, 0)
- connector.connect()
- self.retries += 1
- elif not self.sendFinished:
- # 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
- self.currentProtocol = p
- self.result.addBoth(self._removeProtocol)
- return p
- def _removeProtocol(self, result):
- """
- Remove the protocol created in C{buildProtocol}.
- @param result: The result/error passed to the callback/errback of
- L{defer.Deferred}.
- @return: The C{result} untouched.
- """
- if self.currentProtocol:
- self.currentProtocol = None
- return result
- class LOGINCredentials(_lcredentials):
- """
- L{LOGINCredentials} generates challenges for I{LOGIN} authentication.
- For interoperability with Outlook, the challenge generated does not exactly
- match the one defined in the
- U{draft specification<http://sepp.oetiker.ch/sasl-2.1.19-ds/draft-murchison-sasl-login-00.txt>}.
- """
- def __init__(self):
- _lcredentials.__init__(self)
- self.challenges = [b'Password:', b'Username:']
- @implementer(IClientAuthentication)
- class PLAINAuthenticator:
- def __init__(self, user):
- self.user = user
- def getName(self):
- return b"PLAIN"
- def challengeResponse(self, secret, chal=1):
- if chal == 1:
- return self.user + b'\0' + self.user + b'\0' + secret
- else:
- return b'\0' + self.user + b'\0' + 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.
- @type currentProtocol: L{ESMTPSender}
- @ivar currentProtocol: The current running protocol as made by
- L{buildProtocol}.
- """
- 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):
- """
- Build an L{ESMTPSender} protocol configured with C{heloFallback},
- C{requireAuthentication}, and C{requireTransportSecurity} as specified
- in L{__init__}.
- This sets L{currentProtocol} on the factory, as well as returning it.
- @rtype: L{ESMTPSender}
- """
- 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
- self.currentProtocol = p
- self.result.addBoth(self._removeProtocol)
- return p
- def sendmail(smtphost, from_addr, to_addrs, msg, senderDomainName=None, port=25,
- reactor=reactor, username=None, password=None,
- requireAuthentication=False, requireTransportSecurity=False):
- """
- Send an email.
- This interface is intended to be a replacement for L{smtplib.SMTP.sendmail}
- and related methods. To maintain backwards compatibility, it will fall back
- to plain SMTP, if ESMTP support is not available. If ESMTP support is
- available, it will attempt to provide encryption via STARTTLS and
- authentication if a secret is provided.
- @param smtphost: The host the message should be sent to.
- @type smtphost: L{bytes}
- @param from_addr: The (envelope) address sending this mail.
- @type from_addr: L{bytes}
- @param to_addrs: A list of addresses to send this mail to. A string will
- be treated as a list of one address.
- @type to_addr: L{list} of L{bytes} or L{bytes}
- @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
- L{email.message} directly, but doing the conversion with
- L{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).
- @type senderDomainName: L{bytes}
- @param port: Remote port to which to connect.
- @type port: L{int}
- @param username: The username to use, if wanting to authenticate.
- @type username: L{bytes} or L{unicode}
- @param password: The secret to use, if wanting to authenticate. If you do
- not specify this, SMTP authentication will not occur.
- @type password: L{bytes} or L{unicode}
- @param requireTransportSecurity: Whether or not STARTTLS is required.
- @type requireTransportSecurity: L{bool}
- @param requireAuthentication: Whether or not authentication is required.
- @type requireAuthentication: L{bool}
- @param reactor: The L{reactor} used to make the TCP connection.
- @rtype: L{Deferred}
- @returns: A cancellable L{Deferred}, its callback will be called if a
- message is sent to ANY address, the errback if no message is sent. When
- the C{cancel} method is called, it will stop retrying and disconnect
- the connection immediately.
- 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 = BytesIO(bytes(msg))
- def cancel(d):
- """
- Cancel the L{twisted.mail.smtp.sendmail} call, tell the factory not to
- retry and disconnect the connection.
- @param d: The L{defer.Deferred} to be cancelled.
- """
- factory.sendFinished = True
- if factory.currentProtocol:
- factory.currentProtocol.transport.abortConnection()
- else:
- # Connection hasn't been made yet
- connector.disconnect()
- d = defer.Deferred(cancel)
- if isinstance(username, unicode):
- username = username.encode("utf-8")
- if isinstance(password, unicode):
- password = password.encode("utf-8")
- factory = ESMTPSenderFactory(username, password, from_addr, to_addrs, msg,
- d, heloFallback=True, requireAuthentication=requireAuthentication,
- requireTransportSecurity=requireTransportSecurity)
- if senderDomainName is not None:
- factory.domain = networkString(senderDomainName)
- connector = reactor.connectTCP(smtphost, port, factory)
- return d
- import codecs
- def xtext_encode(s, errors=None):
- r = []
- for ch in iterbytes(s):
- o = ord(ch)
- if ch == '+' or ch == '=' or o < 33 or o > 126:
- r.append(networkString('+%02X' % (o,)))
- else:
- r.append(_bytesChr(o))
- return (b''.join(r), len(s))
- def xtext_decode(s, errors=None):
- """
- Decode the xtext-encoded string C{s}.
- @param s: String to decode.
- @param errors: codec error handling scheme.
- @return: The decoded string.
- """
- r = []
- i = 0
- while i < len(s):
- if s[i:i+1] == b'+':
- try:
- r.append(chr(int(bytes(s[i + 1:i + 3]), 16)))
- except ValueError:
- r.append(ord(s[i:i + 3]))
- i += 3
- else:
- r.append(bytes(s[i:i+1]).decode('ascii'))
- i += 1
- return (''.join(r), 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)
|