1
# -*- test-case-name: twisted.mail.test.test_smtp -*-
2
# Copyright (c) 2001-2009 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
Simple Mail Transfer Protocol implementation.
9
import time, re, base64, types, socket, os, random, hmac
10
import MimeWriter, tempfile, rfc822
13
from email.base64MIME import encode as encode_base64
15
from zope.interface import implements, Interface
17
from twisted.copyright import longversion
18
from twisted.protocols import basic
19
from twisted.protocols import policies
20
from twisted.internet import protocol
21
from twisted.internet import defer
22
from twisted.internet import error
23
from twisted.internet import reactor
24
from twisted.internet.interfaces import ITLSTransport
25
from twisted.python import log
26
from twisted.python import util
27
from twisted.python import failure
29
from twisted import cred
30
import twisted.cred.checkers
31
import twisted.cred.credentials
32
from twisted.python.runtime import platform
35
from cStringIO import StringIO
37
from StringIO import StringIO
39
# Cache the hostname (XXX Yes - this is broken)
40
if platform.isMacOSX():
41
# On OS X, getfqdn() is ridiculously slow - use the
42
# probably-identical-but-sometimes-not gethostname() there.
43
DNSNAME = socket.gethostname()
45
DNSNAME = socket.getfqdn()
47
# Used for fast success code lookup
48
SUCCESS = dict(map(None, range(200, 300), []))
50
class IMessageDelivery(Interface):
51
def receivedHeader(helo, origin, recipients):
53
Generate the Received header for a message
55
@type helo: C{(str, str)}
56
@param helo: The argument to the HELO command and the client's IP
59
@type origin: C{Address}
60
@param origin: The address the message is from
62
@type recipients: C{list} of L{User}
63
@param recipients: A list of the addresses for which this message
67
@return: The full \"Received\" header string.
72
Validate the address for which the message is destined.
75
@param user: The address to validate.
77
@rtype: no-argument callable
78
@return: A C{Deferred} which becomes, or a callable which
79
takes no arguments and returns an object implementing C{IMessage}.
80
This will be called and the returned object used to deliver the
81
message when it arrives.
83
@raise SMTPBadRcpt: Raised if messages to the address are
87
def validateFrom(helo, origin):
89
Validate the address from which the message originates.
91
@type helo: C{(str, str)}
92
@param helo: The argument to the HELO command and the client's IP
95
@type origin: C{Address}
96
@param origin: The address the message is from
98
@rtype: C{Deferred} or C{Address}
99
@return: C{origin} or a C{Deferred} whose callback will be
102
@raise SMTPBadSender: Raised of messages from this address are
106
class IMessageDeliveryFactory(Interface):
107
"""An alternate interface to implement for handling message delivery.
109
It is useful to implement this interface instead of L{IMessageDelivery}
110
directly because it allows the implementor to distinguish between
111
different messages delivery over the same connection. This can be
112
used to optimize delivery of a single message to multiple recipients,
113
something which cannot be done by L{IMessageDelivery} implementors
114
due to their lack of information.
116
def getMessageDelivery():
117
"""Return an L{IMessageDelivery} object.
119
This will be called once per message.
122
class SMTPError(Exception):
127
class SMTPClientError(SMTPError):
128
"""Base class for SMTP client errors.
130
def __init__(self, code, resp, log=None, addresses=None, isFatal=False, retry=False):
132
@param code: The SMTP response code associated with this error.
133
@param resp: The string response associated with this error.
135
@param log: A string log of the exchange leading up to and including
139
@param isFatal: A boolean indicating whether this connection can
140
proceed or not. If True, the connection will be dropped.
142
@param retry: A boolean indicating whether the delivery should be
143
retried. If True and the factory indicates further retries are
144
desirable, they will be attempted, otherwise the delivery will
150
self.addresses = addresses
151
self.isFatal = isFatal
157
res = ["%.3d %s" % (self.code, self.resp)]
163
return '\n'.join(res)
166
class ESMTPClientError(SMTPClientError):
167
"""Base class for ESMTP client errors.
170
class EHLORequiredError(ESMTPClientError):
171
"""The server does not support EHLO.
173
This is considered a non-fatal error (the connection will not be
177
class AUTHRequiredError(ESMTPClientError):
178
"""Authentication was required but the server does not support it.
180
This is considered a non-fatal error (the connection will not be
184
class TLSRequiredError(ESMTPClientError):
185
"""Transport security was required but the server does not support it.
187
This is considered a non-fatal error (the connection will not be
191
class AUTHDeclinedError(ESMTPClientError):
192
"""The server rejected our credentials.
194
Either the username, password, or challenge response
195
given to the server was rejected.
197
This is considered a non-fatal error (the connection will not be
201
class AuthenticationError(ESMTPClientError):
202
"""An error ocurred while authenticating.
204
Either the server rejected our request for authentication or the
205
challenge received was malformed.
207
This is considered a non-fatal error (the connection will not be
211
class TLSError(ESMTPClientError):
212
"""An error occurred while negiotiating for transport security.
214
This is considered a non-fatal error (the connection will not be
218
class SMTPConnectError(SMTPClientError):
219
"""Failed to connect to the mail exchange host.
221
This is considered a fatal error. A retry will be made.
223
def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True):
224
SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)
226
class SMTPTimeoutError(SMTPClientError):
227
"""Failed to receive a response from the server in the expected time period.
229
This is considered a fatal error. A retry will be made.
231
def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True):
232
SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)
234
class SMTPProtocolError(SMTPClientError):
235
"""The server sent a mangled response.
237
This is considered a fatal error. A retry will not be made.
239
def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=False):
240
SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)
242
class SMTPDeliveryError(SMTPClientError):
243
"""Indicates that a delivery attempt has had an error.
246
class SMTPServerError(SMTPError):
247
def __init__(self, code, resp):
252
return "%.3d %s" % (self.code, self.resp)
254
class SMTPAddressError(SMTPServerError):
255
def __init__(self, addr, code, resp):
256
SMTPServerError.__init__(self, code, resp)
257
self.addr = Address(addr)
260
return "%.3d <%s>... %s" % (self.code, self.addr, self.resp)
262
class SMTPBadRcpt(SMTPAddressError):
263
def __init__(self, addr, code=550,
264
resp='Cannot receive for specified address'):
265
SMTPAddressError.__init__(self, addr, code, resp)
267
class SMTPBadSender(SMTPAddressError):
268
def __init__(self, addr, code=550, resp='Sender not acceptable'):
269
SMTPAddressError.__init__(self, addr, code, resp)
271
def rfc822date(timeinfo=None,local=1):
273
Format an RFC-2822 compliant date string.
275
@param timeinfo: (optional) A sequence as returned by C{time.localtime()}
276
or C{time.gmtime()}. Default is now.
277
@param local: (optional) Indicates if the supplied time is local or
278
universal time, or if no time is given, whether now should be local or
279
universal time. Default is local, as suggested (SHOULD) by rfc-2822.
281
@returns: A string representing the time and date in RFC-2822 format.
285
timeinfo = time.localtime()
287
timeinfo = time.gmtime()
295
(tzhr, tzmin) = divmod(abs(tz), 3600)
297
tzhr *= int(abs(tz)/tz)
298
(tzmin, tzsec) = divmod(tzmin, 60)
300
(tzhr, tzmin) = (0,0)
302
return "%s, %02d %s %04d %02d:%02d:%02d %+03d%02d" % (
303
['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timeinfo[6]],
305
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
306
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timeinfo[1] - 1],
307
timeinfo[0], timeinfo[3], timeinfo[4], timeinfo[5],
316
def messageid(uniq=None, N=idGenerator().next):
317
"""Return a globally unique random string in RFC 2822 Message-ID format
319
<datetime.pid.random@host.dom.ain>
321
Optional uniq string will be added to strenghten uniqueness if given.
323
datetime = time.strftime('%Y%m%d%H%M%S', time.gmtime())
325
rand = random.randrange(2**31L-1)
331
return '<%s.%s.%s%s.%s@%s>' % (datetime, pid, rand, uniq, N(), DNSNAME)
334
"""Turn an email address, possibly with realname part etc, into
335
a form suitable for and SMTP envelope.
338
if isinstance(addr, Address):
339
return '<%s>' % str(addr)
341
res = rfc822.parseaddr(addr)
343
if res == (None, None):
344
# It didn't parse, use it as-is
345
return '<%s>' % str(addr)
347
return '<%s>' % str(res[1])
349
COMMAND, DATA, AUTH = 'COMMAND', 'DATA', 'AUTH'
351
class AddressError(SMTPError):
352
"Parse error in address"
354
# Character classes for parsing addresses
355
atom = r"[-A-Za-z0-9!\#$%&'*+/=?^_`{|}~]"
358
"""Parse and hold an RFC 2821 address.
360
Source routes are stipped and ignored, UUCP-style bang-paths
361
and %-style routing are not parsed.
364
@ivar domain: The domain within which this address resides.
367
@ivar local: The local (\"user\") portion of this address.
370
tstring = re.compile(r'''( # A string of
371
(?:"[^"]*" # quoted string
372
|\\. # backslash-escaped characted
373
|''' + atom + r''' # atom character
374
)+|.) # or any single character''',re.X)
375
atomre = re.compile(atom) # match any one atom character
377
def __init__(self, addr, defaultDomain=None):
378
if isinstance(addr, User):
380
if isinstance(addr, Address):
381
self.__dict__ = addr.__dict__.copy()
383
elif not isinstance(addr, types.StringTypes):
388
atl = filter(None,self.tstring.split(addr))
396
raise AddressError, "Unbalanced <>"
402
while atl and atl[0] != ':':
406
raise AddressError, "Malformed source route"
407
atl = atl[1:] # remove :
409
raise AddressError, "Too many @"
413
elif len(atl[0]) == 1 and not self.atomre.match(atl[0]) and atl[0] != '.':
414
raise AddressError, "Parse error at %r of %r" % (atl[0], (addr, atl))
419
domain.append(atl[0])
422
self.local = ''.join(local)
423
self.domain = ''.join(domain)
424
if self.local != '' and self.domain == '':
425
if defaultDomain is None:
426
defaultDomain = DNSNAME
427
self.domain = defaultDomain
429
dequotebs = re.compile(r'\\(.)')
431
def dequote(self,addr):
432
"""Remove RFC-2821 quotes from address."""
435
atl = filter(None,self.tstring.split(str(addr)))
438
if t[0] == '"' and t[-1] == '"':
441
res.append(self.dequotebs.sub(r'\1',t))
448
if self.local or self.domain:
449
return '@'.join((self.local, self.domain))
454
return "%s.%s(%s)" % (self.__module__, self.__class__.__name__,
458
"""Hold information about and SMTP message recipient,
459
including information on where the message came from
462
def __init__(self, destination, helo, protocol, orig):
463
host = getattr(protocol, 'host', None)
464
self.dest = Address(destination, host)
466
self.protocol = protocol
467
if isinstance(orig, Address):
470
self.orig = Address(orig, host)
472
def __getstate__(self):
473
"""Helper for pickle.
475
protocol isn't picklabe, but we want User to be, so skip it in
478
return { 'dest' : self.dest,
484
return str(self.dest)
486
class IMessage(Interface):
487
"""Interface definition for messages that can be sent via SMTP."""
489
def lineReceived(line):
490
"""handle another line"""
493
"""handle end of message
495
return a deferred. The deferred should be called with either:
496
callback(string) or errback(error)
499
def connectionLost():
500
"""handle message truncated
502
semantics should be to discard the message
505
class SMTP(basic.LineOnlyReceiver, policies.TimeoutMixin):
506
"""SMTP server-side protocol."""
512
# Control whether we log SMTP events
515
# A factory for IMessageDelivery objects. If an
516
# avatar implementing IMessageDeliveryFactory can
517
# be acquired from the portal, it will be used to
518
# create a new IMessageDelivery object for each
519
# message which is received.
520
deliveryFactory = None
522
# An IMessageDelivery object. A new instance is
523
# used for each message received if we can get an
524
# IMessageDeliveryFactory from the portal. Otherwise,
525
# a single instance is used throughout the lifetime
529
# Cred cleanup function.
532
def __init__(self, delivery=None, deliveryFactory=None):
537
self.delivery = delivery
538
self.deliveryFactory = deliveryFactory
540
def timeoutConnection(self):
541
msg = '%s Timeout. Try talking faster next time!' % (self.host,)
542
self.sendCode(421, msg)
543
self.transport.loseConnection()
546
return '%s NO UCE NO UBE NO RELAY PROBES' % (self.host,)
548
def connectionMade(self):
549
# Ensure user-code always gets something sane for _helo
550
peer = self.transport.getPeer()
553
except AttributeError: # not an IPv4Address
555
self._helo = (None, host)
556
self.sendCode(220, self.greeting())
557
self.setTimeout(self.timeout)
559
def sendCode(self, code, message=''):
560
"Send an SMTP code with a message."
561
lines = message.splitlines()
562
lastline = lines[-1:]
563
for line in lines[:-1]:
564
self.sendLine('%3.3d-%s' % (code, line))
565
self.sendLine('%3.3d %s' % (code,
566
lastline and lastline[0] or ''))
568
def lineReceived(self, line):
570
return getattr(self, 'state_' + self.mode)(line)
572
def state_COMMAND(self, line):
573
# Ignore leading and trailing whitespace, as well as an arbitrary
574
# amount of whitespace between the command and its argument, though
575
# it is not required by the protocol, for it is a nice thing to do.
578
parts = line.split(None, 1)
580
method = self.lookupMethod(parts[0]) or self.do_UNKNOWN
586
self.sendSyntaxError()
588
def sendSyntaxError(self):
589
self.sendCode(500, 'Error: bad syntax')
591
def lookupMethod(self, command):
592
return getattr(self, 'do_' + command.upper(), None)
594
def lineLengthExceeded(self, line):
595
if self.mode is DATA:
596
for message in self.__messages:
597
message.connectionLost()
600
self.sendCode(500, 'Line too long')
602
def do_UNKNOWN(self, rest):
603
self.sendCode(500, 'Command not implemented')
605
def do_HELO(self, rest):
606
peer = self.transport.getPeer()
609
except AttributeError:
611
self._helo = (rest, host)
614
self.sendCode(250, '%s Hello %s, nice to meet you' % (self.host, host))
616
def do_QUIT(self, rest):
617
self.sendCode(221, 'See you later')
618
self.transport.loseConnection()
620
# A string of quoted strings, backslash-escaped character or
621
# atom characters + '@.,:'
622
qstring = r'("[^"]*"|\\.|' + atom + r'|[@.,:])+'
624
mail_re = re.compile(r'''\s*FROM:\s*(?P<path><> # Empty <>
625
|<''' + qstring + r'''> # <addr>
626
|''' + qstring + r''' # addr
627
)\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options
629
rcpt_re = re.compile(r'\s*TO:\s*(?P<path><' + qstring + r'''> # <addr>
630
|''' + qstring + r''' # addr
631
)\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options
634
def do_MAIL(self, rest):
636
self.sendCode(503,"Only one sender per message, please")
638
# Clear old recipient list
640
m = self.mail_re.match(rest)
642
self.sendCode(501, "Syntax error")
646
addr = Address(m.group('path'), self.host)
647
except AddressError, e:
648
self.sendCode(553, str(e))
651
validated = defer.maybeDeferred(self.validateFrom, self._helo, addr)
652
validated.addCallbacks(self._cbFromValidate, self._ebFromValidate)
655
def _cbFromValidate(self, from_, code=250, msg='Sender address accepted'):
657
self.sendCode(code, msg)
660
def _ebFromValidate(self, failure):
661
if failure.check(SMTPBadSender):
662
self.sendCode(failure.value.code,
663
'Cannot receive from specified address %s: %s'
664
% (quoteaddr(failure.value.addr), failure.value.resp))
665
elif failure.check(SMTPServerError):
666
self.sendCode(failure.value.code, failure.value.resp)
668
log.err(failure, "SMTP sender validation failure")
671
'Requested action aborted: local error in processing')
674
def do_RCPT(self, rest):
676
self.sendCode(503, "Must have sender before recipient")
678
m = self.rcpt_re.match(rest)
680
self.sendCode(501, "Syntax error")
684
user = User(m.group('path'), self._helo, self, self._from)
685
except AddressError, e:
686
self.sendCode(553, str(e))
689
d = defer.maybeDeferred(self.validateTo, user)
696
def _cbToValidate(self, to, user=None, code=250, msg='Recipient address accepted'):
699
self._to.append((user, to))
700
self.sendCode(code, msg)
702
def _ebToValidate(self, failure):
703
if failure.check(SMTPBadRcpt, SMTPServerError):
704
self.sendCode(failure.value.code, failure.value.resp)
709
'Requested action aborted: local error in processing'
712
def _disconnect(self, msgs):
717
log.msg("msg raised exception from connectionLost")
720
def do_DATA(self, rest):
721
if self._from is None or (not self._to):
722
self.sendCode(503, 'Must have valid receiver and originator')
725
helo, origin = self._helo, self._from
726
recipients = self._to
730
self.datafailed = None
733
for (user, msgFunc) in recipients:
736
rcvdhdr = self.receivedHeader(helo, origin, [user])
738
msg.lineReceived(rcvdhdr)
740
except SMTPServerError, e:
741
self.sendCode(e.code, e.resp)
743
self._disconnect(msgs)
747
self.sendCode(550, "Internal server error")
749
self._disconnect(msgs)
751
self.__messages = msgs
753
self.__inheader = self.__inbody = 0
754
self.sendCode(354, 'Continue')
757
fmt = 'Receiving message for delivery: from=%s to=%s'
758
log.msg(fmt % (origin, [str(u) for (u, f) in recipients]))
760
def connectionLost(self, reason):
761
# self.sendCode(421, 'Dropping connection.') # This does nothing...
762
# Ideally, if we (rather than the other side) lose the connection,
763
# we should be able to tell the other side that we are going away.
764
# RFC-2821 requires that we try.
765
if self.mode is DATA:
767
for message in self.__messages:
769
message.connectionLost()
773
except AttributeError:
777
self._onLogout = None
778
self.setTimeout(None)
780
def do_RSET(self, rest):
783
self.sendCode(250, 'I remember nothing.')
785
def dataLineReceived(self, line):
790
self.sendCode(self.datafailed.code,
791
self.datafailed.resp)
793
if not self.__messages:
794
self._messageHandled("thrown away")
797
m.eomReceived() for m in self.__messages
798
], consumeErrors=True).addCallback(self._messageHandled
808
# Add a blank line between the generated Received:-header
809
# and the message body if the message comes in without any
811
if not self.__inheader and not self.__inbody:
815
for message in self.__messages:
816
message.lineReceived('')
822
for message in self.__messages:
823
message.lineReceived(line)
824
except SMTPServerError, e:
826
for message in self.__messages:
827
message.connectionLost()
828
state_DATA = dataLineReceived
830
def _messageHandled(self, resultList):
832
for (success, result) in resultList:
837
msg = 'Could not send e-mail'
840
msg += ' (%d failures out of %d recipients)' % (failures, L)
841
self.sendCode(550, msg)
843
self.sendCode(250, 'Delivery in progress')
846
def _cbAnonymousAuthentication(self, (iface, avatar, logout)):
848
Save the state resulting from a successful anonymous cred login.
850
if issubclass(iface, IMessageDeliveryFactory):
851
self.deliveryFactory = avatar
853
elif issubclass(iface, IMessageDelivery):
854
self.deliveryFactory = None
855
self.delivery = avatar
857
raise RuntimeError("%s is not a supported interface" % (iface.__name__,))
858
self._onLogout = logout
859
self.challenger = None
862
# overridable methods:
863
def validateFrom(self, helo, origin):
865
Validate the address from which the message originates.
867
@type helo: C{(str, str)}
868
@param helo: The argument to the HELO command and the client's IP
871
@type origin: C{Address}
872
@param origin: The address the message is from
874
@rtype: C{Deferred} or C{Address}
875
@return: C{origin} or a C{Deferred} whose callback will be
878
@raise SMTPBadSender: Raised of messages from this address are
881
if self.deliveryFactory is not None:
882
self.delivery = self.deliveryFactory.getMessageDelivery()
884
if self.delivery is not None:
885
return defer.maybeDeferred(self.delivery.validateFrom,
888
# No login has been performed, no default delivery object has been
889
# provided: try to perform an anonymous login and then invoke this
893
result = self.portal.login(
894
cred.credentials.Anonymous(),
896
IMessageDeliveryFactory, IMessageDelivery)
898
def ebAuthentication(err):
900
Translate cred exceptions into SMTP exceptions so that the
901
protocol code which invokes C{validateFrom} can properly report
904
if err.check(cred.error.UnauthorizedLogin):
905
exc = SMTPBadSender(origin)
906
elif err.check(cred.error.UnhandledCredentials):
908
origin, resp="Unauthenticated senders not allowed")
911
return defer.fail(exc)
914
self._cbAnonymousAuthentication, ebAuthentication)
916
def continueValidation(ignored):
918
Re-attempt from address validation.
920
return self.validateFrom(helo, origin)
922
result.addCallback(continueValidation)
925
raise SMTPBadSender(origin)
928
def validateTo(self, user):
930
Validate the address for which the message is destined.
933
@param user: The address to validate.
935
@rtype: no-argument callable
936
@return: A C{Deferred} which becomes, or a callable which
937
takes no arguments and returns an object implementing C{IMessage}.
938
This will be called and the returned object used to deliver the
939
message when it arrives.
941
@raise SMTPBadRcpt: Raised if messages to the address are
944
if self.delivery is not None:
945
return self.delivery.validateTo(user)
946
raise SMTPBadRcpt(user)
948
def receivedHeader(self, helo, origin, recipients):
949
if self.delivery is not None:
950
return self.delivery.receivedHeader(helo, origin, recipients)
954
heloStr = " helo=%s" % (helo[0],)
955
domain = self.transport.getHost().host
956
from_ = "from %s ([%s]%s)" % (helo[0], helo[1], heloStr)
957
by = "by %s with %s (%s)" % (domain,
958
self.__class__.__name__,
960
for_ = "for %s; %s" % (' '.join(map(str, recipients)),
962
return "Received: %s\n\t%s\n\t%s" % (from_, by, for_)
964
def startMessage(self, recipients):
966
return self.delivery.startMessage(recipients)
970
class SMTPFactory(protocol.ServerFactory):
971
"""Factory for SMTP."""
973
# override in instances or subclasses
980
def __init__(self, portal = None):
983
def buildProtocol(self, addr):
984
p = protocol.ServerFactory.buildProtocol(self, addr)
985
p.portal = self.portal
989
class SMTPClient(basic.LineReceiver, policies.TimeoutMixin):
991
SMTP client for sending emails.
993
After the client has connected to the SMTP server, it repeatedly calls
994
L{SMTPClient.getMailFrom}, L{SMTPClient.getMailTo} and
995
L{SMTPClient.getMailData} and uses this information to send an email.
996
It then calls L{SMTPClient.getMailFrom} again; if it returns C{None}, the
997
client will disconnect, otherwise it will continue as normal i.e. call
998
L{SMTPClient.getMailTo} and L{SMTPClient.getMailData} and send a new email.
1001
# If enabled then log SMTP client server communication
1004
# Number of seconds to wait before timing out a connection. If
1005
# None, perform no timeout checking.
1008
def __init__(self, identity, logsize=10):
1009
self.identity = identity or ''
1010
self.toAddressesResult = []
1011
self.successAddresses = []
1015
self.log = util.LineLog(logsize)
1017
def sendLine(self, line):
1018
# Log sendLine only if you are in debug mode for performance
1020
self.log.append('>>> ' + line)
1022
basic.LineReceiver.sendLine(self,line)
1024
def connectionMade(self):
1025
self.setTimeout(self.timeout)
1027
self._expected = [ 220 ]
1028
self._okresponse = self.smtpState_helo
1029
self._failresponse = self.smtpConnectionFailed
1031
def connectionLost(self, reason=protocol.connectionDone):
1032
"""We are no longer connected"""
1033
self.setTimeout(None)
1034
self.mailFile = None
1036
def timeoutConnection(self):
1039
-1, "Timeout waiting for SMTP server response",
1042
def lineReceived(self, line):
1045
# Log lineReceived only if you are in debug mode for performance
1047
self.log.append('<<< ' + line)
1052
self.code = int(line[:3])
1054
# This is a fatal error and will disconnect the transport lineReceived will not be called again
1055
self.sendError(SMTPProtocolError(-1, "Invalid response from SMTP server: %s" % line, self.log.str()))
1059
# Verbose informational message, ignore it
1062
self.resp.append(line[4:])
1064
if line[3:4] == '-':
1068
if self.code in self._expected:
1069
why = self._okresponse(self.code,'\n'.join(self.resp))
1071
why = self._failresponse(self.code,'\n'.join(self.resp))
1077
def smtpConnectionFailed(self, code, resp):
1078
self.sendError(SMTPConnectError(code, resp, self.log.str()))
1080
def smtpTransferFailed(self, code, resp):
1082
self.sendError(SMTPProtocolError(code, resp, self.log.str()))
1084
self.smtpState_msgSent(code, resp)
1086
def smtpState_helo(self, code, resp):
1087
self.sendLine('HELO ' + self.identity)
1088
self._expected = SUCCESS
1089
self._okresponse = self.smtpState_from
1091
def smtpState_from(self, code, resp):
1092
self._from = self.getMailFrom()
1093
self._failresponse = self.smtpTransferFailed
1094
if self._from is not None:
1095
self.sendLine('MAIL FROM:%s' % quoteaddr(self._from))
1096
self._expected = [250]
1097
self._okresponse = self.smtpState_to
1099
# All messages have been sent, disconnect
1100
self._disconnectFromServer()
1102
def smtpState_disconnect(self, code, resp):
1103
self.transport.loseConnection()
1105
def smtpState_to(self, code, resp):
1106
self.toAddresses = iter(self.getMailTo())
1107
self.toAddressesResult = []
1108
self.successAddresses = []
1109
self._okresponse = self.smtpState_toOrData
1110
self._expected = xrange(0,1000)
1111
self.lastAddress = None
1112
return self.smtpState_toOrData(0, '')
1114
def smtpState_toOrData(self, code, resp):
1115
if self.lastAddress is not None:
1116
self.toAddressesResult.append((self.lastAddress, code, resp))
1118
self.successAddresses.append(self.lastAddress)
1120
self.lastAddress = self.toAddresses.next()
1121
except StopIteration:
1122
if self.successAddresses:
1123
self.sendLine('DATA')
1124
self._expected = [ 354 ]
1125
self._okresponse = self.smtpState_data
1127
return self.smtpState_msgSent(code,'No recipients accepted')
1129
self.sendLine('RCPT TO:%s' % quoteaddr(self.lastAddress))
1131
def smtpState_data(self, code, resp):
1132
s = basic.FileSender()
1133
d = s.beginFileTransfer(
1134
self.getMailData(), self.transport, self.transformChunk)
1135
def ebTransfer(err):
1136
self.sendError(err.value)
1137
d.addCallbacks(self.finishedFileTransfer, ebTransfer)
1138
self._expected = SUCCESS
1139
self._okresponse = self.smtpState_msgSent
1142
def smtpState_msgSent(self, code, resp):
1143
if self._from is not None:
1144
self.sentMail(code, resp, len(self.successAddresses),
1145
self.toAddressesResult, self.log)
1147
self.toAddressesResult = []
1149
self.sendLine('RSET')
1150
self._expected = SUCCESS
1151
self._okresponse = self.smtpState_from
1154
## Helpers for FileSender
1156
def transformChunk(self, chunk):
1158
Perform the necessary local to network newline conversion and escape
1161
This method also resets the idle timeout so that as long as process is
1162
being made sending the message body, the client will not time out.
1165
return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..')
1167
def finishedFileTransfer(self, lastsent):
1168
if lastsent != '\n':
1175
# these methods should be overriden in subclasses
1176
def getMailFrom(self):
1177
"""Return the email address the mail is from."""
1178
raise NotImplementedError
1180
def getMailTo(self):
1181
"""Return a list of emails to send to."""
1182
raise NotImplementedError
1184
def getMailData(self):
1185
"""Return file-like object containing data of message to be sent.
1187
Lines in the file should be delimited by '\\n'.
1189
raise NotImplementedError
1191
def sendError(self, exc):
1193
If an error occurs before a mail message is sent sendError will be
1194
called. This base class method sends a QUIT if the error is
1195
non-fatal and disconnects the connection.
1197
@param exc: The SMTPClientError (or child class) raised
1198
@type exc: C{SMTPClientError}
1200
if isinstance(exc, SMTPClientError) and not exc.isFatal:
1201
self._disconnectFromServer()
1203
# If the error was fatal then the communication channel with the
1204
# SMTP Server is broken so just close the transport connection
1205
self.smtpState_disconnect(-1, None)
1208
def sentMail(self, code, resp, numOk, addresses, log):
1209
"""Called when an attempt to send an email is completed.
1211
If some addresses were accepted, code and resp are the response
1212
to the DATA command. If no addresses were accepted, code is -1
1213
and resp is an informative message.
1215
@param code: the code returned by the SMTP Server
1216
@param resp: The string response returned from the SMTP Server
1217
@param numOK: the number of addresses accepted by the remote host.
1218
@param addresses: is a list of tuples (address, code, resp) listing
1219
the response to each RCPT command.
1220
@param log: is the SMTP session log
1222
raise NotImplementedError
1224
def _disconnectFromServer(self):
1225
self._expected = xrange(0, 1000)
1226
self._okresponse = self.smtpState_disconnect
1227
self.sendLine('QUIT')
1231
class ESMTPClient(SMTPClient):
1232
# Fall back to HELO if the server does not support EHLO
1235
# Refuse to proceed if authentication cannot be performed
1236
requireAuthentication = False
1238
# Refuse to proceed if TLS is not available
1239
requireTransportSecurity = False
1241
# Indicate whether or not our transport can be considered secure.
1244
# ClientContextFactory to use for STARTTLS
1247
def __init__(self, secret, contextFactory=None, *args, **kw):
1248
SMTPClient.__init__(self, *args, **kw)
1249
self.authenticators = []
1250
self.secret = secret
1251
self.context = contextFactory
1252
self.tlsMode = False
1255
def esmtpEHLORequired(self, code=-1, resp=None):
1256
self.sendError(EHLORequiredError(502, "Server does not support ESMTP Authentication", self.log.str()))
1259
def esmtpAUTHRequired(self, code=-1, resp=None):
1262
for a in self.authenticators:
1263
tmp.append(a.getName().upper())
1265
auth = "[%s]" % ', '.join(tmp)
1267
self.sendError(AUTHRequiredError(502, "Server does not support Client Authentication schemes %s" % auth,
1271
def esmtpTLSRequired(self, code=-1, resp=None):
1272
self.sendError(TLSRequiredError(502, "Server does not support secure communication via TLS / SSL",
1275
def esmtpTLSFailed(self, code=-1, resp=None):
1276
self.sendError(TLSError(code, "Could not complete the SSL/TLS handshake", self.log.str()))
1278
def esmtpAUTHDeclined(self, code=-1, resp=None):
1279
self.sendError(AUTHDeclinedError(code, resp, self.log.str()))
1281
def esmtpAUTHMalformedChallenge(self, code=-1, resp=None):
1282
str = "Login failed because the SMTP Server returned a malformed Authentication Challenge"
1283
self.sendError(AuthenticationError(501, str, self.log.str()))
1285
def esmtpAUTHServerError(self, code=-1, resp=None):
1286
self.sendError(AuthenticationError(code, resp, self.log.str()))
1288
def registerAuthenticator(self, auth):
1289
"""Registers an Authenticator with the ESMTPClient. The ESMTPClient
1290
will attempt to login to the SMTP Server in the order the
1291
Authenticators are registered. The most secure Authentication
1292
mechanism should be registered first.
1294
@param auth: The Authentication mechanism to register
1295
@type auth: class implementing C{IClientAuthentication}
1298
self.authenticators.append(auth)
1300
def connectionMade(self):
1301
SMTPClient.connectionMade(self)
1302
self._okresponse = self.esmtpState_ehlo
1304
def esmtpState_ehlo(self, code, resp):
1305
self._expected = SUCCESS
1307
self._okresponse = self.esmtpState_serverConfig
1308
self._failresponse = self.esmtpEHLORequired
1310
if self.heloFallback:
1311
self._failresponse = self.smtpState_helo
1313
self.sendLine('EHLO ' + self.identity)
1315
def esmtpState_serverConfig(self, code, resp):
1317
for line in resp.splitlines():
1318
e = line.split(None, 1)
1325
self.authenticate(code, resp, items)
1327
self.tryTLS(code, resp, items)
1329
def tryTLS(self, code, resp, items):
1330
if self.context and 'STARTTLS' in items:
1331
self._expected = [220]
1332
self._okresponse = self.esmtpState_starttls
1333
self._failresponse = self.esmtpTLSFailed
1334
self.sendLine('STARTTLS')
1335
elif self.requireTransportSecurity:
1336
self.tlsMode = False
1337
self.esmtpTLSRequired()
1339
self.tlsMode = False
1340
self.authenticate(code, resp, items)
1342
def esmtpState_starttls(self, code, resp):
1344
self.transport.startTLS(self.context)
1348
self.esmtpTLSFailed(451)
1350
# Send another EHLO once TLS has been started to
1351
# get the TLS / AUTH schemes. Some servers only allow AUTH in TLS mode.
1352
self.esmtpState_ehlo(code, resp)
1354
def authenticate(self, code, resp, items):
1355
if self.secret and items.get('AUTH'):
1356
schemes = items['AUTH'].split()
1359
#XXX: May want to come up with a more efficient way to do this
1361
tmpSchemes[s.upper()] = 1
1363
for a in self.authenticators:
1364
auth = a.getName().upper()
1366
if auth in tmpSchemes:
1369
# Special condition handled
1371
self._okresponse = self.smtpState_from
1372
self._failresponse = self._esmtpState_plainAuth
1373
self._expected = [235]
1374
challenge = encode_base64(self._authinfo.challengeResponse(self.secret, 1), eol="")
1375
self.sendLine('AUTH ' + auth + ' ' + challenge)
1377
self._expected = [334]
1378
self._okresponse = self.esmtpState_challenge
1379
# If some error occurs here, the server declined the AUTH
1380
# before the user / password phase. This would be
1382
self._failresponse = self.esmtpAUTHServerError
1383
self.sendLine('AUTH ' + auth)
1386
if self.requireAuthentication:
1387
self.esmtpAUTHRequired()
1389
self.smtpState_from(code, resp)
1391
def _esmtpState_plainAuth(self, code, resp):
1392
self._okresponse = self.smtpState_from
1393
self._failresponse = self.esmtpAUTHDeclined
1394
self._expected = [235]
1395
challenge = encode_base64(self._authinfo.challengeResponse(self.secret, 2), eol="")
1396
self.sendLine('AUTH PLAIN ' + challenge)
1398
def esmtpState_challenge(self, code, resp):
1399
self._authResponse(self._authinfo, resp)
1401
def _authResponse(self, auth, challenge):
1402
self._failresponse = self.esmtpAUTHDeclined
1404
challenge = base64.decodestring(challenge)
1405
except binascii.Error, e:
1406
# Illegal challenge, give up, then quit
1408
self._okresponse = self.esmtpAUTHMalformedChallenge
1409
self._failresponse = self.esmtpAUTHMalformedChallenge
1411
resp = auth.challengeResponse(self.secret, challenge)
1412
self._expected = [235, 334]
1413
self._okresponse = self.smtpState_maybeAuthenticated
1414
self.sendLine(encode_base64(resp, eol=""))
1417
def smtpState_maybeAuthenticated(self, code, resp):
1419
Called to handle the next message from the server after sending a
1420
response to a SASL challenge. The server response might be another
1421
challenge or it might indicate authentication has succeeded.
1424
# Yes, authenticated!
1426
self.smtpState_from(code, resp)
1428
# No, not authenticated yet. Keep trying.
1429
self._authResponse(self._authinfo, resp)
1439
authenticated = False
1441
def __init__(self, chal = None, contextFactory = None):
1445
self.challengers = chal
1446
self.authenticated = False
1447
self.ctx = contextFactory
1449
def connectionMade(self):
1450
SMTP.connectionMade(self)
1451
self.canStartTLS = ITLSTransport.providedBy(self.transport)
1452
self.canStartTLS = self.canStartTLS and (self.ctx is not None)
1456
return SMTP.greeting(self) + ' ESMTP'
1459
def extensions(self):
1460
ext = {'AUTH': self.challengers.keys()}
1461
if self.canStartTLS and not self.startedTLS:
1462
ext['STARTTLS'] = None
1465
def lookupMethod(self, command):
1466
m = SMTP.lookupMethod(self, command)
1468
m = getattr(self, 'ext_' + command.upper(), None)
1471
def listExtensions(self):
1473
for (c, v) in self.extensions().iteritems():
1476
# Intentionally omit extensions with empty argument lists
1477
r.append('%s %s' % (c, ' '.join(v)))
1482
def do_EHLO(self, rest):
1483
peer = self.transport.getPeer().host
1484
self._helo = (rest, peer)
1489
'%s Hello %s, nice to meet you\n%s' % (
1491
self.listExtensions(),
1495
def ext_STARTTLS(self, rest):
1497
self.sendCode(503, 'TLS already negotiated')
1498
elif self.ctx and self.canStartTLS:
1499
self.sendCode(220, 'Begin TLS negotiation now')
1500
self.transport.startTLS(self.ctx)
1501
self.startedTLS = True
1503
self.sendCode(454, 'TLS not available')
1505
def ext_AUTH(self, rest):
1506
if self.authenticated:
1507
self.sendCode(503, 'Already authenticated')
1509
parts = rest.split(None, 1)
1510
chal = self.challengers.get(parts[0].upper(), lambda: None)()
1512
self.sendCode(504, 'Unrecognized authentication type')
1516
self.challenger = chal
1519
chal.getChallenge() # Discard it, apparently the client does not
1524
self.state_AUTH(rest)
1527
def _cbAuthenticated(self, loginInfo):
1529
Save the state resulting from a successful cred login and mark this
1530
connection as authenticated.
1532
result = SMTP._cbAnonymousAuthentication(self, loginInfo)
1533
self.authenticated = True
1537
def _ebAuthenticated(self, reason):
1539
Handle cred login errors by translating them to the SMTP authenticate
1540
failed. Translate all other errors into a generic SMTP error code and
1541
log the failure for inspection. Stop all errors from propagating.
1543
self.challenge = None
1544
if reason.check(cred.error.UnauthorizedLogin):
1545
self.sendCode(535, 'Authentication failed')
1547
log.err(reason, "SMTP authentication failure")
1550
'Requested action aborted: local error in processing')
1553
def state_AUTH(self, response):
1555
Handle one step of challenge/response authentication.
1557
@param response: The text of a response. If None, this
1558
function has been called as a result of an AUTH command with
1559
no initial response. A response of '*' aborts authentication,
1562
if self.portal is None:
1563
self.sendCode(454, 'Temporary authentication failure')
1567
if response is None:
1568
challenge = self.challenger.getChallenge()
1569
encoded = challenge.encode('base64')
1570
self.sendCode(334, encoded)
1574
self.sendCode(501, 'Authentication aborted')
1575
self.challenger = None
1580
uncoded = response.decode('base64')
1581
except binascii.Error:
1582
self.sendCode(501, 'Syntax error in parameters or arguments')
1583
self.challenger = None
1587
self.challenger.setResponse(uncoded)
1588
if self.challenger.moreChallenges():
1589
challenge = self.challenger.getChallenge()
1590
coded = challenge.encode('base64')[:-1]
1591
self.sendCode(334, coded)
1595
result = self.portal.login(
1596
self.challenger, None,
1597
IMessageDeliveryFactory, IMessageDelivery)
1598
result.addCallback(self._cbAuthenticated)
1599
result.addCallback(lambda ign: self.sendCode(235, 'Authentication successful.'))
1600
result.addErrback(self._ebAuthenticated)
1605
"""Utility class for sending emails easily.
1607
Use with SMTPSenderFactory or ESMTPSenderFactory.
1611
def getMailFrom(self):
1614
return str(self.factory.fromEmail)
1618
def getMailTo(self):
1619
return self.factory.toEmail
1621
def getMailData(self):
1622
return self.factory.file
1624
def sendError(self, exc):
1625
# Call the base class to close the connection with the SMTP server
1626
SMTPClient.sendError(self, exc)
1628
# Do not retry to connect to SMTP Server if:
1629
# 1. No more retries left (This allows the correct error to be returned to the errorback)
1631
# 3. The error code is not in the 4xx range (Communication Errors)
1633
if (self.factory.retries >= 0 or
1634
(not exc.retry and not (exc.code >= 400 and exc.code < 500))):
1635
self.factory.sendFinished = 1
1636
self.factory.result.errback(exc)
1638
def sentMail(self, code, resp, numOk, addresses, log):
1639
# Do not retry, the SMTP server acknowledged the request
1640
self.factory.sendFinished = 1
1641
if code not in SUCCESS:
1643
for addr, acode, aresp in addresses:
1644
if acode not in SUCCESS:
1645
errlog.append("%s: %03d %s" % (addr, acode, aresp))
1647
errlog.append(log.str())
1649
exc = SMTPDeliveryError(code, resp, '\n'.join(errlog), addresses)
1650
self.factory.result.errback(exc)
1652
self.factory.result.callback((numOk, addresses))
1655
class SMTPSender(SenderMixin, SMTPClient):
1657
SMTP protocol that sends a single email based on information it
1658
gets from its factory, a L{SMTPSenderFactory}.
1662
class SMTPSenderFactory(protocol.ClientFactory):
1664
Utility factory for sending emails easily.
1668
protocol = SMTPSender
1670
def __init__(self, fromEmail, toEmail, file, deferred, retries=5,
1673
@param fromEmail: The RFC 2821 address from which to send this
1676
@param toEmail: A sequence of RFC 2821 addresses to which to
1679
@param file: A file-like object containing the message to send.
1681
@param deferred: A Deferred to callback or errback when sending
1682
of this message completes.
1684
@param retries: The number of times to retry delivery of this
1687
@param timeout: Period, in seconds, for which to wait for
1688
server responses, or None to wait forever.
1690
assert isinstance(retries, (int, long))
1692
if isinstance(toEmail, types.StringTypes):
1694
self.fromEmail = Address(fromEmail)
1695
self.nEmails = len(toEmail)
1696
self.toEmail = toEmail
1698
self.result = deferred
1699
self.result.addBoth(self._removeDeferred)
1700
self.sendFinished = 0
1702
self.retries = -retries
1703
self.timeout = timeout
1705
def _removeDeferred(self, argh):
1709
def clientConnectionFailed(self, connector, err):
1710
self._processConnectionError(connector, err)
1712
def clientConnectionLost(self, connector, err):
1713
self._processConnectionError(connector, err)
1715
def _processConnectionError(self, connector, err):
1716
if self.retries < self.sendFinished <= 0:
1717
log.msg("SMTP Client retrying server. Retry: %s" % -self.retries)
1719
# Rewind the file in case part of it was read while attempting to
1721
self.file.seek(0, 0)
1724
elif self.sendFinished <= 0:
1725
# If we were unable to communicate with the SMTP server a ConnectionDone will be
1726
# returned. We want a more clear error message for debugging
1727
if err.check(error.ConnectionDone):
1728
err.value = SMTPConnectError(-1, "Unable to connect to server.")
1729
self.result.errback(err.value)
1731
def buildProtocol(self, addr):
1732
p = self.protocol(self.domain, self.nEmails*2+2)
1734
p.timeout = self.timeout
1739
from twisted.mail.imap4 import IClientAuthentication
1740
from twisted.mail.imap4 import CramMD5ClientAuthenticator, LOGINAuthenticator
1742
class PLAINAuthenticator:
1743
implements(IClientAuthentication)
1745
def __init__(self, user):
1751
def challengeResponse(self, secret, chal=1):
1753
return "%s\0%s\0%s" % (self.user, self.user, secret)
1755
return "%s\0%s" % (self.user, secret)
1759
class ESMTPSender(SenderMixin, ESMTPClient):
1761
requireAuthentication = True
1762
requireTransportSecurity = True
1764
def __init__(self, username, secret, contextFactory=None, *args, **kw):
1765
self.heloFallback = 0
1766
self.username = username
1768
if contextFactory is None:
1769
contextFactory = self._getContextFactory()
1771
ESMTPClient.__init__(self, secret, contextFactory, *args, **kw)
1773
self._registerAuthenticators()
1775
def _registerAuthenticators(self):
1776
# Register Authenticator in order from most secure to least secure
1777
self.registerAuthenticator(CramMD5ClientAuthenticator(self.username))
1778
self.registerAuthenticator(LOGINAuthenticator(self.username))
1779
self.registerAuthenticator(PLAINAuthenticator(self.username))
1781
def _getContextFactory(self):
1782
if self.context is not None:
1785
from twisted.internet import ssl
1790
context = ssl.ClientContextFactory()
1791
context.method = ssl.SSL.TLSv1_METHOD
1793
except AttributeError:
1797
class ESMTPSenderFactory(SMTPSenderFactory):
1799
Utility factory for sending emails easily.
1802
protocol = ESMTPSender
1804
def __init__(self, username, password, fromEmail, toEmail, file,
1805
deferred, retries=5, timeout=None,
1806
contextFactory=None, heloFallback=False,
1807
requireAuthentication=True,
1808
requireTransportSecurity=True):
1810
SMTPSenderFactory.__init__(self, fromEmail, toEmail, file, deferred, retries, timeout)
1811
self.username = username
1812
self.password = password
1813
self._contextFactory = contextFactory
1814
self._heloFallback = heloFallback
1815
self._requireAuthentication = requireAuthentication
1816
self._requireTransportSecurity = requireTransportSecurity
1818
def buildProtocol(self, addr):
1819
p = self.protocol(self.username, self.password, self._contextFactory, self.domain, self.nEmails*2+2)
1820
p.heloFallback = self._heloFallback
1821
p.requireAuthentication = self._requireAuthentication
1822
p.requireTransportSecurity = self._requireTransportSecurity
1824
p.timeout = self.timeout
1827
def sendmail(smtphost, from_addr, to_addrs, msg, senderDomainName=None, port=25):
1830
This interface is intended to be a direct replacement for
1831
smtplib.SMTP.sendmail() (with the obvious change that
1832
you specify the smtphost as well). Also, ESMTP options
1833
are not accepted, as we don't do ESMTP yet. I reserve the
1834
right to implement the ESMTP options differently.
1836
@param smtphost: The host the message should be sent to
1837
@param from_addr: The (envelope) address sending this mail.
1838
@param to_addrs: A list of addresses to send this mail to. A string will
1839
be treated as a list of one address
1840
@param msg: The message, including headers, either as a file or a string.
1841
File-like objects need to support read() and close(). Lines must be
1842
delimited by '\\n'. If you pass something that doesn't look like a
1843
file, we try to convert it to a string (so you should be able to
1844
pass an email.Message directly, but doing the conversion with
1845
email.Generator manually will give you more control over the
1848
@param senderDomainName: Name by which to identify. If None, try
1849
to pick something sane (but this depends on external configuration
1850
and may not succeed).
1852
@param port: Remote port to which to connect.
1855
@returns: A L{Deferred}, its callback will be called if a message is sent
1856
to ANY address, the errback if no message is sent.
1858
The callback will be called with a tuple (numOk, addresses) where numOk
1859
is the number of successful recipient addresses and addresses is a list
1860
of tuples (address, code, resp) giving the response to the RCPT command
1863
if not hasattr(msg,'read'):
1865
msg = StringIO(str(msg))
1867
d = defer.Deferred()
1868
factory = SMTPSenderFactory(from_addr, to_addrs, msg, d)
1870
if senderDomainName is not None:
1871
factory.domain = senderDomainName
1873
reactor.connectTCP(smtphost, port, factory)
1877
def sendEmail(smtphost, fromEmail, toEmail, content, headers = None, attachments = None, multipartbody = "mixed"):
1878
"""Send an email, optionally with attachments.
1881
@param smtphost: hostname of SMTP server to which to connect
1883
@type fromEmail: str
1884
@param fromEmail: email address to indicate this email is from
1887
@param toEmail: email address to which to send this email
1890
@param content: The body if this email.
1893
@param headers: Dictionary of headers to include in the email
1895
@type attachments: list of 3-tuples
1896
@param attachments: Each 3-tuple should consist of the name of the
1897
attachment, the mime-type of the attachment, and a string that is
1898
the attachment itself.
1900
@type multipartbody: str
1901
@param multipartbody: The type of MIME multi-part body. Generally
1902
either "mixed" (as in text and images) or "alternative" (html email
1903
with a fallback to text/plain).
1906
@return: The returned Deferred has its callback or errback invoked when
1907
the mail is successfully sent or when an error occurs, respectively.
1909
warnings.warn("smtp.sendEmail may go away in the future.\n"
1910
" Consider revising your code to use the email module\n"
1911
" and smtp.sendmail.",
1912
category=DeprecationWarning, stacklevel=2)
1914
f = tempfile.TemporaryFile()
1915
writer = MimeWriter.MimeWriter(f)
1917
writer.addheader("Mime-Version", "1.0")
1919
# Setup the mail headers
1920
for (header, value) in headers.items():
1921
writer.addheader(header, value)
1923
headkeys = [k.lower() for k in headers.keys()]
1927
# Add required headers if not present
1928
if "message-id" not in headkeys:
1929
writer.addheader("Message-ID", messageid())
1930
if "date" not in headkeys:
1931
writer.addheader("Date", rfc822date())
1932
if "from" not in headkeys and "sender" not in headkeys:
1933
writer.addheader("From", fromEmail)
1934
if "to" not in headkeys and "cc" not in headkeys and "bcc" not in headkeys:
1935
writer.addheader("To", toEmail)
1937
writer.startmultipartbody(multipartbody)
1940
part = writer.nextpart()
1941
body = part.startbody("text/plain")
1944
if attachments is not None:
1946
for (file, mime, attachment) in attachments:
1947
part = writer.nextpart()
1948
if mime.startswith('text'):
1951
attachment = base64.encodestring(attachment)
1953
part.addheader("Content-Transfer-Encoding", encoding)
1954
body = part.startbody("%s; name=%s" % (mime, file))
1955
body.write(attachment)
1962
d = defer.Deferred()
1963
factory = SMTPSenderFactory(fromEmail, toEmail, f, d)
1964
reactor.connectTCP(smtphost, 25, factory)
1972
def xtext_encode(s, errors=None):
1976
if ch == '+' or ch == '=' or o < 33 or o > 126:
1977
r.append('+%02X' % o)
1980
return (''.join(r), len(s))
1983
def _slowXTextDecode(s, errors=None):
1985
Decode the xtext-encoded string C{s}.
1992
r.append(chr(int(s[i + 1:i + 3], 16)))
1994
r.append(s[i:i + 3])
1999
return (''.join(r), len(s))
2002
from twisted.protocols._c_urlarg import unquote as _helper_unquote
2004
xtext_decode = _slowXTextDecode
2006
def xtext_decode(s, errors=None):
2008
Decode the xtext-encoded string C{s} using a fast extension function.
2010
return (_helper_unquote(s, '+'), len(s))
2012
class xtextStreamReader(codecs.StreamReader):
2013
def decode(self, s, errors='strict'):
2014
return xtext_decode(s)
2016
class xtextStreamWriter(codecs.StreamWriter):
2017
def decode(self, s, errors='strict'):
2018
return xtext_encode(s)
2020
def xtext_codec(name):
2022
return (xtext_encode, xtext_decode, xtextStreamReader, xtextStreamWriter)
2023
codecs.register(xtext_codec)