Mini Shell
# -*- 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)
Zerion Mini Shell 1.0