12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273 |
- # -*- 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.
- """
-
-
- import base64
- import binascii
- import os
- import random
- import re
- import socket
- import time
- import warnings
- from email.utils import parseaddr
- from io import BytesIO
- from typing import Type
-
- from zope.interface import implementer
-
- from twisted import cred
- from twisted.copyright import longversion
- from twisted.internet import defer, error, protocol, reactor
- from twisted.internet._idna import _idnaText
- from twisted.internet.interfaces import ISSLTransport, ITLSTransport
- from twisted.mail._cred import (
- CramMD5ClientAuthenticator,
- LOGINAuthenticator,
- LOGINCredentials as _lcredentials,
- )
- from twisted.mail._except import (
- AddressError,
- AUTHDeclinedError,
- AuthenticationError,
- AUTHRequiredError,
- EHLORequiredError,
- ESMTPClientError,
- SMTPAddressError,
- SMTPBadRcpt,
- SMTPBadSender,
- SMTPClientError,
- SMTPConnectError,
- SMTPDeliveryError,
- SMTPError,
- SMTPProtocolError,
- SMTPServerError,
- SMTPTimeoutError,
- SMTPTLSError as TLSError,
- TLSRequiredError,
- )
- from twisted.mail.interfaces import (
- IClientAuthentication,
- IMessageDelivery,
- IMessageDeliveryFactory,
- IMessageSMTP as IMessage,
- )
- from twisted.protocols import basic, policies
- from twisted.python import log, util
- from twisted.python.compat import iterbytes, nativeString, networkString
- from twisted.python.runtime import platform
-
- __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)
- # Encode the DNS name into something we can send over the wire
- if platform.isMacOSX():
- # On macOS, getfqdn() is ridiculously slow - use the
- # probably-identical-but-sometimes-not gethostname() there.
- DNSNAME = socket.gethostname().encode("ascii")
- else:
- DNSNAME = socket.getfqdn().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 "<{}.{}.{}{}.{}@{}>".format(
- datetime, pid, rand, uniq, N(), DNSNAME.decode()
- ).encode()
-
-
- 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(f"Parse error at {atl[0]!r} of {(addr, atl)!r}")
- 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)
-
- def __str__(self) -> str:
- return self.__bytes__().decode("ascii")
-
- def __bytes__(self) -> bytes:
- if self.local or self.domain:
- return b"@".join((self.local, self.domain))
- else:
- return b""
-
- def __repr__(self) -> str:
- return "{}.{}({})".format(
- 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) -> str:
- return self.__bytes__().decode("ascii")
-
- def __bytes__(self) -> bytes:
- 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 BaseException:
- 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 BaseException:
- 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 BaseException:
- 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 += f" ({failures} failures out of {resultLen} recipients)"
- 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(f"{iface.__name__} is not a supported interface")
- 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, str):
- 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,
- f"Invalid response from SMTP server: {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{IOpenSSLClientConnectionCreator}
-
- @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 BaseException:
- 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": list(self.challengers.keys())}
- 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 self.extensions().items():
- 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: Type[SMTPClient] = 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)
-
- if isinstance(toEmail, str):
- 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
-
- self._hostname = kw.pop("hostname", None)
-
- 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
- if self._hostname is None:
- return None
- try:
- from twisted.internet.ssl import optionsForClientTLS
- except ImportError:
- return None
- else:
- context = optionsForClientTLS(self._hostname)
- return context
-
-
- 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,
- hostname=None,
- ):
-
- 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
- self._hostname = hostname
-
- 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,
- hostname=self._hostname,
- )
- 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_addrs: 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, str):
- username = username.encode("utf-8")
- if isinstance(password, str):
- password = password.encode("utf-8")
-
- tlsHostname = smtphost
- if not isinstance(tlsHostname, str):
- tlsHostname = _idnaText(tlsHostname)
-
- factory = ESMTPSenderFactory(
- username,
- password,
- from_addr,
- to_addrs,
- msg,
- d,
- heloFallback=True,
- requireAuthentication=requireAuthentication,
- requireTransportSecurity=requireTransportSecurity,
- hostname=tlsHostname,
- )
-
- 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(f"+{o:02X}"))
- else:
- r.append(bytes((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)
|