root / tags / release-2.0.1 / twisted / mail / smtp.py

Revision 13020, 61.9 kB (checked in by exarkun, 4 years ago)

Be a little more lenient with the format of commands - resolves 865

Previously, leading whitespace would generate undefined behavior, as per the RFC. Change this to handle the input in a manner that is most likely what the client intended.

Line 
1 # -*- test-case-name: twisted.mail.test.test_smtp -*-
2 #
3 # Copyright (c) 2001-2004 Twisted Matrix Laboratories.
4 # See LICENSE for details.
5
6
7 """Simple Mail Transfer Protocol implementation.
8 """
9
10 from __future__ import generators
11
12 # Twisted imports
13 from twisted.protocols import basic
14 from twisted.protocols import policies
15 from twisted.internet import protocol
16 from twisted.internet import defer
17 from twisted.internet import error
18 from twisted.internet import reactor
19 from twisted.internet.interfaces import ITLSTransport
20 from twisted.python import log
21 from twisted.python import components
22 from twisted.python import util
23 from twisted.python import reflect
24 from twisted.python import failure
25
26 from twisted import cred
27 import twisted.cred.checkers
28 import twisted.cred.credentials
29
30 # System imports
31 import time, string, re, base64, types, socket, os, random, hmac
32 import MimeWriter, tempfile, rfc822
33 import warnings
34 import binascii
35 import sys
36 from zope.interface import implements
37
38 try:
39     from email.base64MIME import encode as encode_base64
40 except ImportError:
41     def encode_base64(s, eol='\n'):
42         return s.encode('base64').rstrip() + eol
43
44 try:
45     from cStringIO import StringIO
46 except ImportError:
47     from StringIO import StringIO
48
49 DNSNAME = socket.getfqdn() # Cache the hostname
50
51 # Used for fast success code lookup
52 SUCCESS = dict(map(None, range(200, 300), []))
53
54 class IMessageDelivery(components.Interface):
55     def receivedHeader(self, helo, origin, recipients):
56         """
57         Generate the Received header for a message
58
59         @type helo: C{(str, str)}
60         @param helo: The argument to the HELO command and the client's IP
61         address.
62
63         @type origin: C{Address}
64         @param origin: The address the message is from
65
66         @type recipients: C{list} of C{str}
67         @param recipients: A list of the addresses for which this message
68         is bound.
69
70         @rtype: C{str}
71         @return: The full "Received" header string.
72         """
73
74     def validateTo(self, user):
75         """
76         Validate the address for which the message is destined.
77
78         @type user: C{User}
79         @param user: The address to validate.
80
81         @rtype: no-argument callable
82         @return: A C{Deferred} which becomes, or a callable which
83         takes no arguments and returns an object implementing C{IMessage}.
84         This will be called and the returned object used to deliver the
85         message when it arrives.
86
87         @raise SMTPBadRcpt: Raised if messages to the address are
88         not to be accepted.
89         """
90
91     def validateFrom(self, helo, origin):
92         """
93         Validate the address from which the message originates.
94
95         @type helo: C{(str, str)}
96         @param helo: The argument to the HELO command and the client's IP
97         address.
98
99         @type origin: C{Address}
100         @param origin: The address the message is from
101
102         @rtype: C{Deferred} or C{Address}
103         @return: C{origin} or a C{Deferred} whose callback will be
104         passed C{origin}.
105
106         @raise SMTPBadSender: Raised of messages from this address are
107         not to be accepted.
108         """
109
110 class IMessageDeliveryFactory(components.Interface):
111     """An alternate interface to implement for handling message delivery.
112
113     It is useful to implement this interface instead of L{IMessageDelivery}
114     directly because it allows the implementor to distinguish between
115     different messages delivery over the same connection.  This can be
116     used to optimize delivery of a single message to multiple recipients,
117     something which cannot be done by L{IMessageDelivery} implementors
118     due to their lack of information.
119     """
120     def getMessageDelivery(self):
121         """Return an L{IMessageDelivery} object.
122
123         This will be called once per message.
124         """
125
126 class SMTPError(Exception):
127     pass
128
129 class SMTPClientError(SMTPError):
130     """Base class for SMTP client errors.
131     """
132     def __init__(self, code, resp, log=None, addresses=None, isFatal=False, retry=False):
133         """
134         @param code: The SMTP response code associated with this error.
135         @param resp: The string response associated with this error.
136         @param log: A string log of the exchange leading up to and including the error.
137
138         @param isFatal: A boolean indicating whether this connection can proceed
139         or not.  If True, the connection will be dropped.
140         @param retry: A boolean indicating whether the delivery should be retried.
141         If True and the factory indicates further retries are desirable, they will
142         be attempted, otherwise the delivery will be failed.
143         """
144         self.code = code
145         self.resp = resp
146         self.log = log
147         self.addresses = addresses
148         self.isFatal = isFatal
149         self.retry = retry
150
151     def __str__(self):
152         if self.code > 0:
153             res = ["%.3d %s" % (self.code, self.resp)]
154         else:
155             res = [self.resp]
156         if self.log:
157             res.append('')
158             res.append(self.log)
159         return '\n'.join(res)
160
161
162 class ESMTPClientError(SMTPClientError):
163     """Base class for ESMTP client errors.
164     """
165
166 class EHLORequiredError(ESMTPClientError):
167     """The server does not support EHLO.
168
169     This is considered a non-fatal error (the connection will not be
170     dropped).
171     """
172
173 class AUTHRequiredError(ESMTPClientError):
174     """Authentication was required but the server does not support it.
175
176     This is considered a non-fatal error (the connection will not be
177     dropped).
178     """
179
180 class TLSRequiredError(ESMTPClientError):
181     """Transport security was required but the server does not support it.
182
183     This is considered a non-fatal error (the connection will not be
184     dropped).
185     """
186
187 class AUTHDeclinedError(ESMTPClientError):
188     """The server rejected our credentials.
189
190     Either the username, password, or challenge response
191     given to the server was rejected.
192
193     This is considered a non-fatal error (the connection will not be
194     dropped).
195     """
196
197 class AuthenticationError(ESMTPClientError):
198     """An error ocurred while authenticating.
199
200     Either the server rejected our request for authentication or the
201     challenge received was malformed.
202
203     This is considered a non-fatal error (the connection will not be
204     dropped).
205     """
206
207 class TLSError(ESMTPClientError):
208     """An error occurred while negiotiating for transport security.
209
210     This is considered a non-fatal error (the connection will not be
211     dropped).
212     """
213
214 class SMTPConnectError(SMTPClientError):
215     """Failed to connect to the mail exchange host.
216
217     This is considered a fatal error.  A retry will be made.
218     """
219     def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True):
220         SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)
221
222 class SMTPTimeoutError(SMTPClientError):
223     """Failed to receive a response from the server in the expected time period.
224
225     This is considered a fatal error.  A retry will be made.
226     """
227     def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True):
228         SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)
229
230 class SMTPProtocolError(SMTPClientError):
231     """The server sent a mangled response.
232
233     This is considered a fatal error.  A retry will not be made.
234     """
235     def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=False):
236         SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)
237
238 class SMTPDeliveryError(SMTPClientError):
239     """Indicates that a delivery attempt has had an error.
240     """
241
242 class SMTPServerError(SMTPError):
243     def __init__(self, code, resp):
244         self.code = code
245         self.resp = resp
246
247     def __str__(self):
248         return "%.3d %s" % (self.code, self.resp)
249
250 class SMTPAddressError(SMTPServerError):
251     def __init__(self, addr, code, resp):
252         SMTPServerError.__init__(self, code, resp)
253         self.addr = Address(addr)
254
255     def __str__(self):
256         return "%.3d <%s>... %s" % (self.code, self.addr, self.resp)
257
258 class SMTPBadRcpt(SMTPAddressError):
259     def __init__(self, addr, code=550,
260                  resp='Cannot receive for specified address'):
261         SMTPAddressError.__init__(self, addr, code, resp)
262
263 class SMTPBadSender(SMTPAddressError):
264     def __init__(self, addr, code=550, resp='Sender not acceptable'):
265         SMTPAddressError.__init__(self, addr, code, resp)
266
267 def rfc822date(timeinfo=None,local=1):
268     """
269     Format an RFC-2822 compliant date string.
270
271     @param timeinfo: (optional) A sequence as returned by C{time.localtime()}
272         or C{time.gmtime()}. Default is now.
273     @param local: (optional) Indicates if the supplied time is local or
274         universal time, or if no time is given, whether now should be local or
275         universal time. Default is local, as suggested (SHOULD) by rfc-2822.
276
277     @returns: A string representing the time and date in RFC-2822 format.
278     """
279     if not timeinfo:
280         if local:
281             timeinfo = time.localtime()
282         else:
283             timeinfo = time.gmtime()
284     if local:
285         if timeinfo[8]:
286             # DST
287             tz = -time.altzone
288         else:
289             tz = -time.timezone
290
291         (tzhr, tzmin) = divmod(abs(tz), 3600)
292         if tz:
293             tzhr *= int(abs(tz)/tz)
294         (tzmin, tzsec) = divmod(tzmin, 60)
295     else:
296         (tzhr, tzmin) = (0,0)
297
298     return "%s, %02d %s %04d %02d:%02d:%02d %+03d%02d" % (
299         ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timeinfo[6]],
300         timeinfo[2],
301         ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
302          'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timeinfo[1] - 1],
303         timeinfo[0], timeinfo[3], timeinfo[4], timeinfo[5],
304         tzhr, tzmin)
305
306 def idGenerator():
307     i = 0
308     while True:
309         yield i
310         i += 1
311
312 def messageid(uniq=None, N=idGenerator().next):
313     """Return a globally unique random string in RFC 2822 Message-ID format
314
315     <datetime.pid.random@host.dom.ain>
316
317     Optional uniq string will be added to strenghten uniqueness if given.
318     """
319     datetime = time.strftime('%Y%m%d%H%M%S', time.gmtime())
320     pid = os.getpid()
321     rand = random.randrange(2**31L-1)
322     if uniq is None:
323         uniq = ''
324     else:
325         uniq = '.' + uniq
326
327     return '<%s.%s.%s%s.%s@%s>' % (datetime, pid, rand, uniq, N(), DNSNAME)
328
329 def quoteaddr(addr):
330     """Turn an email address, possibly with realname part etc, into
331     a form suitable for and SMTP envelope.
332     """
333
334     if isinstance(addr, Address):
335         return '<%s>' % str(addr)
336
337     res = rfc822.parseaddr(addr)
338
339     if res == (None, None):
340         # It didn't parse, use it as-is
341         return '<%s>' % str(addr)
342     else:
343         return '<%s>' % str(res[1])
344
345 COMMAND, DATA, AUTH = 'COMMAND', 'DATA', 'AUTH'
346
347 class AddressError(SMTPError):
348     "Parse error in address"
349
350 # Character classes for parsing addresses
351 atom = r"[-A-Za-z0-9!\#$%&'*+/=?^_`{|}~]"
352
353 class Address:
354     """Parse and hold an RFC 2821 address.
355
356     Source routes are stipped and ignored, UUCP-style bang-paths
357     and %-style routing are not parsed.
358
359     @type domain: C{str}
360     @ivar domain: The domain within which this address resides.
361
362     @type local: C{str}
363     @ivar local: The local (\"user\") portion of this address.
364     """
365
366     tstring = re.compile(r'''( # A string of
367                           (?:"[^"]*" # quoted string
368                           |\\. # backslash-escaped characted
369                           |''' + atom + r''' # atom character
370                           )+|.) # or any single character''',re.X)
371     atomre = re.compile(atom) # match any one atom character
372
373     def __init__(self, addr, defaultDomain=None):
374         if isinstance(addr, User):
375             addr = addr.dest
376         if isinstance(addr, Address):
377             self.__dict__ = addr.__dict__.copy()
378             return
379         elif not isinstance(addr, types.StringTypes):
380             addr = str(addr)
381         self.addrstr = addr
382
383         # Tokenize
384         atl = filter(None,self.tstring.split(addr))
385
386         local = []
387         domain = []
388
389         while atl:
390             if atl[0] == '<':
391                 if atl[-1] != '>':
392                     raise AddressError, "Unbalanced <>"
393                 atl = atl[1:-1]
394             elif atl[0] == '@':
395                 atl = atl[1:]
396                 if not local:
397                     # Source route
398                     while atl and atl[0] != ':':
399                         # remove it
400                         atl = atl[1:]
401                     if not atl:
402                         raise AddressError, "Malformed source route"
403                     atl = atl[1:] # remove :
404                 elif domain:
405                     raise AddressError, "Too many @"
406                 else:
407                     # Now in domain
408                     domain = ['']
409             elif len(atl[0]) == 1 and not self.atomre.match(atl[0]) and atl[0] !=  '.':
410                 raise AddressError, "Parse error at %r of %r" % (atl[0], (addr, atl))
411             else:
412                 if not domain:
413                     local.append(atl[0])
414                 else:
415                     domain.append(atl[0])
416                 atl = atl[1:]
417
418         self.local = ''.join(local)
419         self.domain = ''.join(domain)
420         if self.local != '' and self.domain == '':
421             if defaultDomain is None:
422                 defaultDomain = DNSNAME
423             self.domain = defaultDomain
424
425     dequotebs = re.compile(r'\\(.)')
426
427     def dequote(self,addr):
428         """Remove RFC-2821 quotes from address."""
429         res = []
430
431         atl = filter(None,self.tstring.split(str(addr)))
432
433         for t in atl:
434             if t[0] == '"' and t[-1] == '"':
435                 res.append(t[1:-1])
436             elif '\\' in t:
437                 res.append(self.dequotebs.sub(r'\1',t))
438             else:
439                 res.append(t)
440
441         return ''.join(res)
442
443     def __str__(self):
444         if self.local or self.domain:
445             return '@'.join((self.local, self.domain))
446         else:
447             return ''
448
449     def __repr__(self):
450         return "%s.%s(%s)" % (self.__module__, self.__class__.__name__,
451                               repr(str(self)))
452
453 class User:
454     """Hold information about and SMTP message recipient,
455     including information on where the message came from
456     """
457
458     def __init__(self, destination, helo, protocol, orig):
459         host = getattr(protocol, 'host', None)
460         self.dest = Address(destination, host)
461         self.helo = helo
462         self.protocol = protocol
463         if isinstance(orig, Address):
464             self.orig = orig
465         else:
466             self.orig = Address(orig, host)
467
468     def __getstate__(self):
469         """Helper for pickle.
470
471         protocol isn't picklabe, but we want User to be, so skip it in
472         the pickle.
473         """
474         return { 'dest' : self.dest,
475                  'helo' : self.helo,
476                  'protocol' : None,
477                  'orig' : self.orig }
478
479     def __str__(self):
480         return str(self.dest)
481
482 class IMessage(components.Interface):
483     """Interface definition for messages that can be sent via SMTP."""
484
485     def lineReceived(self, line):
486         """handle another line"""
487
488     def eomReceived(self):
489         """handle end of message
490
491         return a deferred. The deferred should be called with either:
492         callback(string) or errback(error)
493         """
494
495     def connectionLost(self):
496         """handle message truncated
497
498         semantics should be to discard the message
499         """
500
501 class SMTP(basic.LineReceiver, policies.TimeoutMixin):
502     """SMTP server-side protocol."""
503
504     timeout = 600
505     host = DNSNAME
506     portal = None
507
508     # Control whether we log SMTP events
509     noisy = True
510
511     # A factory for IMessageDelivery objects.  If an
512     # avatar implementing IMessageDeliveryFactory can
513     # be acquired from the portal, it will be used to
514     # create a new IMessageDelivery object for each
515     # message which is received.
516     deliveryFactory = None
517
518     # An IMessageDelivery object.  A new instance is
519     # used for each message received if we can get an
520     # IMessageDeliveryFactory from the portal.  Otherwise,
521     # a single instance is used throughout the lifetime
522     # of the connection.
523     delivery = None
524
525     # Cred cleanup function.
526     _onLogout = None
527
528     def __init__(self, delivery=None, deliveryFactory=None):
529         self.mode = COMMAND
530         self._from = None
531         self._helo = None
532         self._to = []
533         self.delivery = delivery
534         self.deliveryFactory = deliveryFactory
535
536     def timeoutConnection(self):
537         msg = '%s Timeout. Try talking faster next time!' % (self.host,)
538         self.sendCode(421, msg)
539         self.transport.loseConnection()
540
541     def greeting(self):
542         return '%s NO UCE NO UBE NO RELAY PROBES ESMTP' % (self.host,)
543
544     def connectionMade(self):
545         # Ensure user-code always gets something sane for _helo
546         peer = self.transport.getPeer()
547         try:
548             host = peer.host
549         except AttributeError: # not an IPv4Address
550             host = str(peer)
551         self._helo = (None, host)
552         self.sendCode(220, self.greeting())
553         self.setTimeout(self.timeout)
554
555     def sendCode(self, code, message=''):
556         "Send an SMTP code with a message."
557         lines = message.splitlines()
558         lastline = lines[-1:]
559         for line in lines[:-1]:
560             self.sendLine('%3.3d-%s' % (code, line))
561         self.sendLine('%3.3d %s' % (code,
562                                     lastline and lastline[0] or ''))
563
564     def lineReceived(self, line):
565         self.resetTimeout()
566         return getattr(self, 'state_' + self.mode)(line)
567
568     def state_COMMAND(self, line):
569         # Ignore leading and trailing whitespace, as well as an arbitrary
570         # amount of whitespace between the command and its argument, though
571         # it is not required by the protocol, for it is a nice thing to do.
572         line = line.strip()
573
574         parts = line.split(None, 1)
575         if parts:
576             method = self.lookupMethod(parts[0]) or self.do_UNKNOWN
577             if len(parts) == 2:
578                 method(parts[1])
579             else:
580                 method('')
581         else:
582             self.sendSyntaxError()
583
584     def sendSyntaxError(self):
585         self.sendCode(500, 'Error: bad syntax')
586
587     def lookupMethod(self, command):
588         return getattr(self, 'do_' + command.upper(), None)
589
590     def lineLengthExceeded(self, line):
591         if self.mode is DATA:
592             for message in self.__messages:
593                 message.connectionLost()
594             self.mode = COMMAND
595             del self.__messages
596         self.sendCode(500, 'Line too long')
597
598     def rawDataReceived(self, data):
599         """Throw away rest of long line"""
600         rest = string.split(data, '\r\n', 1)
601         if len(rest) == 2:
602             self.setLineMode(rest[1])
603
604     def do_UNKNOWN(self, rest):
605         self.sendCode(500, 'Command not implemented')
606
607     def do_HELO(self, rest):
608         peer = self.transport.getPeer()
609         try:
610             host = peer.host
611         except AttributeError:
612             host = str(peer)
613         self._helo = (rest, host)
614         self._from = None
615         self._to = []
616         self.sendCode(250, '%s Hello %s, nice to meet you' % (self.host, host))
617
618     def do_QUIT(self, rest):
619         self.sendCode(221, 'See you later')
620         self.transport.loseConnection()
621
622     # A string of quoted strings, backslash-escaped character or
623     # atom characters + '@.,:'
624     qstring = r'("[^"]*"|\\.|' + atom + r'|[@.,:])+'
625
626     mail_re = re.compile(r'''\s*FROM:\s*(?P<path><> # Empty <>
627                          |<''' + qstring + r'''> # <addr>
628                          |''' + qstring + r''' # addr
629                          )\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options
630                          $''',re.I|re.X)
631     rcpt_re = re.compile(r'\s*TO:\s*(?P<path><' + qstring + r'''> # <addr>
632                          |''' + qstring + r''' # addr
633                          )\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options
634                          $''',re.I|re.X)
635
636     def do_MAIL(self, rest):
637         if self._from:
638             self.sendCode(503,"Only one sender per message, please")
639             return
640         # Clear old recipient list
641         self._to = []
642         m = self.mail_re.match(rest)
643         if not m:
644             self.sendCode(501, "Syntax error")
645             return
646
647         try:
648             addr = Address(m.group('path'), self.host)
649         except AddressError, e:
650             self.sendCode(553, str(e))
651             return
652
653         defer.maybeDeferred(self.validateFrom, self._helo, addr
654             ).addCallbacks(self._cbFromValidate, self._ebFromValidate
655             )
656
657     def _cbFromValidate(self, from_, code=250, msg='Sender address accepted'):
658         self._from = from_
659         self.sendCode(code, msg)
660
661     def _ebFromValidate(self, failure):
662         if failure.check(SMTPBadSender):
663             self.sendCode(failure.value.code,
664                           'Cannot receive for specified address %s: %s'
665                           % (quoteaddr(failure.value.addr), failure.value.resp))
666         elif failure.check(SMTPServerError):
667             self.sendCode(failure.value.code, failure.value.resp)
668         else:
669             log.err(failure)
670             self.sendCode(
671                 451,
672                 'Requested action aborted: local error in processing'
673             )
674
675
676     def do_RCPT(self, rest):
677         if not self._from:
678             self.sendCode(503, "Must have sender before recipient")
679             return
680         m = self.rcpt_re.match(rest)
681         if not m:
682             self.sendCode(501, "Syntax error")
683             return
684
685         try:
686             user = User(m.group('path'), self._helo, self, self._from)
687         except AddressError, e:
688             self.sendCode(553, str(e))
689             return
690
691         d = defer.maybeDeferred(self.validateTo, user)
692         d.addCallbacks(
693             self._cbToValidate,
694             self._ebToValidate,
695             callbackArgs=(user,)
696         )
697
698     def _cbToValidate(self, to, user=None, code=250, msg='Recipient address accepted'):
699         if user is None:
700             user = to
701         self._to.append((user, to))
702         self.sendCode(code, msg)
703
704     def _ebToValidate(self, failure):
705         if failure.check(SMTPBadRcpt, SMTPServerError):
706             self.sendCode(failure.value.code, failure.value.resp)
707         else:
708             log.err(failure)
709             self.sendCode(
710                 451,
711                 'Requested action aborted: local error in processing'
712             )
713
714     def do_DATA(self, rest):
715         if self._from is None or (not self._to):
716             self.sendCode(503, 'Must have valid receiver and originator')
717             return
718         assert self.delivery
719         self.mode = DATA
720         helo, origin = self._helo, self._from
721         recipients = self._to
722
723         self._from = None
724         self._to = []
725         self.datafailed = None
726
727         try:
728             self.__messages = [f() for (u, f) in recipients]
729         except SMTPServerError, e:
730             self.sendCode(e.code, e.resp)
731             self.mode = COMMAND
732             return
733         except:
734             log.err()
735             self.sendCode(550, "Internal server error")
736             self.mode = COMMAND
737             return
738
739         rcvdhdr = self.delivery.receivedHeader(
740             helo, origin, [u for (u, f) in recipients])
741
742         self.__inheader = self.__inbody = 0
743         if rcvdhdr:
744             try:
745                 for message in self.__messages:
746                     message.lineReceived(rcvdhdr)
747             except SMTPServerError, e:
748                 self.sendCode(e.code, e.resp)
749                 self.mode = COMMAND
750                 return
751         self.sendCode(354, 'Continue')
752         if self.noisy:
753             fmt = 'Receiving message for delivery: from=%s to=%s'
754             log.msg(fmt % (origin, [str(u) for (u, f) in recipients]))
755
756     def connectionLost(self, reason):
757         # self.sendCode(421, 'Dropping connection.') # This does nothing...
758         # Ideally, if we (rather than the other side) lose the connection,
759         # we should be able to tell the other side that we are going away.
760         # RFC-2821 requires that we try.
761         if self.mode is DATA:
762             try:
763                 for message in self.__messages:
764                     try:
765                         message.connectionLost()
766                     except:
767                         log.err()
768                 del self.__messages
769             except AttributeError:
770                 pass
771         if self._onLogout:
772             self._onLogout()
773             self._onLogout = None
774         self.setTimeout(None)
775
776     def do_RSET(self, rest):
777         self._from = None
778         self._to = []
779         self.sendCode(250, 'I remember nothing.')
780
781     def dataLineReceived(self, line):
782         if line[:1] == '.':
783             if line == '.':
784                 self.mode = COMMAND
785                 if self.datafailed:
786                     self.sendCode(self.datafailed.code,
787                                   self.datafailed.resp)
788                     return
789                 if not self.__messages:
790                     self._messageHandled("thrown away")
791                     return
792                 defer.DeferredList([
793                     m.eomReceived() for m in self.__messages
794                 ], consumeErrors=True).addCallback(self._messageHandled
795                                                    )
796                 del self.__messages
797                 return
798             line = line[1:]
799
800         if self.datafailed:
801             return
802
803         try:
804             # Add a blank line between the generated Received:-header
805             # and the message body if the message comes in without any
806             # headers
807             if not self.__inheader and not self.__inbody:
808                 if ':' in line:
809                     self.__inheader = 1
810                 elif line:
811                     for message in self.__messages:
812                         message.lineReceived('')
813                     self.__inbody = 1
814
815             if not line:
816                 self.__inbody = 1
817
818             for message in self.__messages:
819                 message.lineReceived(line)
820         except SMTPServerError, e:
821             self.datafailed = e
822             for message in self.__messages:
823                 message.connectionLost()
824     state_DATA = dataLineReceived
825
826     def _messageHandled(self, resultList):
827         failures = 0
828         for (success, result) in resultList:
829             if not success:
830                 failures += 1
831                 log.err(result)
832         if failures:
833             msg = 'Could not send e-mail'
834             L = len(resultList)
835             if L > 1:
836                 msg += ' (%d failures out of %d recipients)' % (failures, L)
837             self.sendCode(550, msg)
838         else:
839             self.sendCode(250, 'Delivery in progress')
840
841     def _cbAuthenticated(self, (iface, avatar, logout)):
842         if issubclass(iface, IMessageDeliveryFactory):
843             self.deliveryFactory = avatar
844             self.delivery = None
845         elif issubclass(iface, IMessageDelivery):
846             self.deliveryFactory = None
847             self.delivery = avatar
848         else:
849             raise RuntimeError("%s is not a supported interface" % (iface.__name__,))
850         self._onLogout = logout
851         self.authenticated = 1
852         self.challenger = None
853
854     def _ebAuthenticated(self, reason):
855         self.challenge = None
856         if reason.check(cred.error.UnauthorizedLogin):
857             self.sendCode(535, 'Authentication failed')
858         elif reason.check(SMTPAddressError):
859             self.sendCode(reason.value.code, reason.value.resp)
860         else:
861             self.sendCode(451, 'Requested action aborted: local error in processing')
862             log.err(reason)
863
864     # overridable methods:
865     def validateFrom(self, helo, origin):
866         """
867         Validate the address from which the message originates.
868
869         @type helo: C{(str, str)}
870         @param helo: The argument to the HELO command and the client's IP
871         address.
872
873         @type origin: C{Address}
874         @param origin: The address the message is from
875
876         @rtype: C{Deferred} or C{Address}
877         @return: C{origin} or a C{Deferred} whose callback will be
878         passed C{origin}.
879
880         @raise SMTPBadSender: Raised of messages from this address are
881         not to be accepted.
882         """
883         if self.deliveryFactory is not None:
884             self.delivery = self.deliveryFactory.getMessageDelivery()
885
886         if self.delivery is not None:
887             return defer.maybeDeferred(self.delivery.validateFrom,
888                                        helo, origin)
889
890         # No login has been performed, no default delivery object has been
891         # provided: try to perform an anonymous login and then invoke this
892         # method again.
893         if self.portal:
894             return self.portal.login(cred.credentials.Anonymous(), None,
895                     IMessageDeliveryFactory, IMessageDelivery
896                 ).addCallback(self._cbAuthenticated
897                 ).addCallback(lambda _: self.validateFrom(helo, origin)
898                 ).addErrback(self._ebAuthenticated
899                 )
900         raise SMTPBadSender(origin)
901
902     def validateTo(self, user):
903         """
904         Validate the address for which the message is destined.
905
906         @type user: C{User}
907         @param user: The address to validate.
908
909         @rtype: no-argument callable
910         @return: A C{Deferred} which becomes, or a callable which
911         takes no arguments and returns an object implementing C{IMessage}.
912         This will be called and the returned object used to deliver the
913         message when it arrives.
914
915         @raise SMTPBadRcpt: Raised if messages to the address are
916         not to be accepted.
917         """
918         if self.delivery:
919             return self.delivery.validateTo(user)
920         raise SMTPBadRcpt(user)
921
922     def startMessage(self, recipients):
923         if self.delivery:
924             return self.delivery.startMessage(recipients)
925         return []
926
927
928 class SMTPFactory(protocol.ServerFactory):
929     """Factory for SMTP."""
930
931     # override in instances or subclasses
932     domain = DNSNAME
933     timeout = 600
934     protocol = SMTP
935
936     portal = None
937
938     def __init__(self, portal = None):
939         self.portal = portal
940
941     def buildProtocol(self, addr):
942         p = protocol.ServerFactory.buildProtocol(self, addr)
943         p.portal = self.portal
944         p.host = self.domain
945         return p
946
947 class SMTPClient(basic.LineReceiver, policies.TimeoutMixin):
948     """SMTP client for sending emails."""
949
950     # If enabled then log SMTP client server communication
951     debug = True
952
953     # Number of seconds to wait before timing out a connection.  If
954     # None, perform no timeout checking.
955     timeout = None
956
957     def __init__(self, identity, logsize=10):
958         self.identity = identity or ''
959         self.toAddressesResult = []
960         self.successAddresses = []
961         self._from = None
962         self.resp = []
963         self.code = -1
964         self.log = util.LineLog(logsize)
965
966     def sendLine(self, line):
967         # Log sendLine only if you are in debug mode for performance
968         if self.debug:
969             self.log.append('>>> ' + line)
970
971         basic.LineReceiver.sendLine(self,line)
972
973     def connectionMade(self):
974         self.setTimeout(self.timeout)
975
976         self._expected = [ 220 ]
977         self._okresponse = self.smtpState_helo
978         self._failresponse = self.smtpConnectionFailed
979
980     def connectionLost(self, reason=protocol.connectionDone):
981         """We are no longer connected"""
982         self.setTimeout(None)
983         self.mailFile = None
984
985     def timeoutConnection(self):
986         self.sendError(
987             SMTPTimeoutError(-1,
988                              "Timeout waiting for SMTP server response",
989                              self.log))
990
991     def lineReceived(self, line):
992         self.resetTimeout()
993
994         # Log lineReceived only if you are in debug mode for performance
995         if self.debug:
996             self.log.append('<<< ' + line)
997
998         why = None
999
1000         try:
1001             self.code = int(line[:3])
1002         except ValueError:
1003             # This is a fatal error and will disconnect the transport lineReceived will not be called again
1004             self.sendError(SMTPProtocolError(-1, "Invalid response from SMTP server: %s" % line, self.log.str()))
1005             return
1006
1007         if line[0] == '0':
1008             # Verbose informational message, ignore it
1009             return
1010
1011         self.resp.append(line[4:])
1012
1013         if line[3:4] == '-':
1014             # continuation
1015             return
1016
1017         if self.code in self._expected:
1018             why = self._okresponse(self.code,'\n'.join(self.resp))
1019         else:
1020             why = self._failresponse(self.code,'\n'.join(self.resp))
1021
1022         self.code = -1
1023         self.resp = []
1024         return why
1025
1026     def smtpConnectionFailed(self, code, resp):
1027         self.sendError(SMTPConnectError(code, resp, self.log.str()))
1028
1029     def smtpTransferFailed(self, code, resp):
1030         if code < 0:
1031             self.sendError(SMTPProtocolError(code, resp, self.log.str()))
1032         else:
1033             self.smtpState_msgSent(code, resp)
1034
1035     def smtpState_helo(self, code, resp):
1036         self.sendLine('HELO ' + self.identity)
1037         self._expected = SUCCESS
1038         self._okresponse = self.smtpState_from
1039
1040     def smtpState_from(self, code, resp):
1041         self._from = self.getMailFrom()
1042         self._failresponse = self.smtpTransferFailed
1043         if self._from is not None:
1044             self.sendLine('MAIL FROM:%s' % quoteaddr(self._from))
1045             self._expected = [250]
1046             self._okresponse = self.smtpState_to
1047         else:
1048             # All messages have been sent, disconnect
1049             self._disconnectFromServer()
1050
1051     def smtpState_disconnect(self, code, resp):
1052         self.transport.loseConnection()
1053
1054     def smtpState_to(self, code, resp):
1055         self.toAddresses = iter(self.getMailTo())
1056         self.toAddressesResult = []
1057         self.successAddresses = []
1058         self._okresponse = self.smtpState_toOrData
1059         self._expected = xrange(0,1000)
1060         self.lastAddress = None
1061         return self.smtpState_toOrData(0, '')
1062
1063     def smtpState_toOrData(self, code, resp):
1064         if self.lastAddress is not None:
1065             self.toAddressesResult.append((self.lastAddress, code, resp))
1066             if code in SUCCESS:
1067                 self.successAddresses.append(self.lastAddress)
1068         try:
1069             self.lastAddress = self.toAddresses.next()
1070         except StopIteration:
1071             if self.successAddresses:
1072                 self.sendLine('DATA')
1073                 self._expected = [ 354 ]
1074                 self._okresponse = self.smtpState_data
1075             else:
1076                 return self.smtpState_msgSent(code,'No recipients accepted')
1077         else:
1078             self.sendLine('RCPT TO:%s' % quoteaddr(self.lastAddress))
1079
1080     def smtpState_data(self, code, resp):
1081         s = basic.FileSender()
1082         s.beginFileTransfer(
1083             self.getMailData(), self.transport, self.transformChunk
1084         ).addCallback(self.finishedFileTransfer)
1085         self._expected = SUCCESS
1086         self._okresponse = self.smtpState_msgSent
1087
1088     def smtpState_msgSent(self, code, resp):
1089         if self._from is not None:
1090             self.sentMail(code, resp, len(self.successAddresses),
1091                           self.toAddressesResult, self.log)
1092
1093         self.toAddressesResult = []
1094         self._from = None
1095         self.sendLine('RSET')
1096         self._expected = SUCCESS
1097         self._okresponse = self.smtpState_from
1098
1099     ##
1100     ## Helpers for FileSender
1101     ##
1102     def transformChunk(self, chunk):
1103         return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..')
1104
1105     def finishedFileTransfer(self, lastsent):
1106         if lastsent != '\n':
1107             line = '\r\n.'
1108         else:
1109             line = '.'
1110         self.sendLine(line)
1111     ##
1112
1113     # these methods should be overriden in subclasses
1114     def getMailFrom(self):
1115         """Return the email address the mail is from."""
1116         raise NotImplementedError
1117
1118     def getMailTo(self):
1119         """Return a list of emails to send to."""
1120         raise NotImplementedError
1121
1122     def getMailData(self):
1123         """Return file-like object containing data of message to be sent.
1124
1125         Lines in the file should be delimited by '\\n'.
1126         """
1127         raise NotImplementedError
1128
1129     def sendError(self, exc):
1130         """If an error occurs before a mail message is sent sendError will be called.
1131            This base class method sends a QUIT if the error is non-fatal
1132            and disconnects the connection.
1133
1134            @param exc: The SMTPClientError (or child class) raised
1135            @type exc: C{SMTPClientError}
1136         """
1137         assert isinstance(exc, SMTPClientError)
1138
1139         if exc.isFatal:
1140             # If the error was fatal then the communication channel with the SMTP Server is
1141             # broken so just close the transport connection
1142             self.smtpState_disconnect(-1, None)
1143         else:
1144             self._disconnectFromServer()
1145
1146     def sentMail(self, code, resp, numOk, addresses, log):
1147         """Called when an attempt to send an email is completed.
1148
1149         If some addresses were accepted, code and resp are the response
1150         to the DATA command. If no addresses were accepted, code is -1
1151         and resp is an informative message.
1152
1153         @param code: the code returned by the SMTP Server
1154         @param resp: The string response returned from the SMTP Server
1155         @param numOK: the number of addresses accepted by the remote host.
1156         @param addresses: is a list of tuples (address, code, resp) listing
1157                           the response to each RCPT command.
1158         @param log: is the SMTP session log
1159         """
1160         raise NotImplementedError
1161
1162     def _disconnectFromServer(self):
1163         self._expected = xrange(0, 1000)
1164         self._okresponse = self.smtpState_disconnect
1165         self.sendLine('QUIT')
1166
1167 class ESMTPClient(SMTPClient):
1168     # Fall back to HELO if the server does not support EHLO
1169     heloFallback = True
1170
1171     # Refuse to proceed if authentication cannot be performed
1172     requireAuthentication = False
1173
1174     # Refuse to proceed if TLS is not available
1175     requireTransportSecurity = False
1176
1177     # Indicate whether or not our transport can be considered secure.
1178     tlsMode = False
1179
1180     # ClientContextFactory to use for STARTTLS
1181     context = None
1182
1183     def __init__(self, secret, contextFactory=None, *args, **kw):
1184         SMTPClient.__init__(self, *args, **kw)
1185         self.authenticators = []
1186         self.secret = secret
1187         self.context = contextFactory
1188         self.tlsMode = False
1189
1190     def esmtpEHLORequired(self, code=-1, resp=None):
1191         self.sendError(EHLORequiredError(502, "Server does not support ESMTP Authentication", self.log.str()))
1192
1193     def esmtpAUTHRequired(self, code=-1, resp=None):
1194         tmp = []
1195
1196         for a in self.authenticators:
1197             tmp.append(a.getName().upper())
1198
1199         auth = "[%s]" % ', '.join(tmp)
1200
1201         self.sendError(AUTHRequiredError(502, "Server does not support Client Authentication schemes %s" % auth,
1202                                          self.log.str()))
1203
1204     def esmtpTLSRequired(self, code=-1, resp=None):
1205         self.sendError(TLSRequiredError(502, "Server does not support secure communication via TLS / SSL",
1206                                         self.log.str()))
1207
1208     def esmtpTLSFailed(self, code=-1, resp=None):
1209         self.sendError(TLSError(code, "Could not complete the SSL/TLS handshake", self.log.str()))
1210
1211     def esmtpAUTHDeclined(self, code=-1, resp=None):
1212         self.sendError(AUTHDeclinedError(code, resp, self.log.str()))
1213
1214     def esmtpAUTHMalformedChallenge(self, code=-1, resp=None):
1215         str =  "Login failed because the SMTP Server returned a malformed Authentication Challenge"
1216         self.sendError(AuthenticationError(501, str, self.log.str()))
1217
1218     def esmtpAUTHServerError(self, code=-1, resp=None):
1219         self.sendError(AuthenticationError(code, resp, self.log.str()))
1220
1221     def registerAuthenticator(self, auth):
1222         """Registers an Authenticator with the ESMTPClient. The ESMTPClient
1223            will attempt to login to the SMTP Server in the order the
1224            Authenticators are registered. The most secure Authentication
1225            mechanism should be registered first.
1226
1227            @param auth: The Authentication mechanism to register
1228            @type auth: class implementing C{IClientAuthentication}
1229         """
1230
1231         self.authenticators.append(auth)
1232
1233     def connectionMade(self):
1234         SMTPClient.connectionMade(self)
1235         self._okresponse = self.esmtpState_ehlo
1236
1237     def esmtpState_ehlo(self, code, resp):
1238         self._expected = SUCCESS
1239
1240         self._okresponse = self.esmtpState_serverConfig
1241         self._failresponse = self.esmtpEHLORequired
1242
1243         if self.heloFallback:
1244             self._failresponse = self.smtpState_helo
1245
1246         self.sendLine('EHLO ' + self.identity)
1247
1248     def esmtpState_serverConfig(self, code, resp):
1249         items = {}
1250         for line in resp.splitlines():
1251             e = line.split(None, 1)
1252             if len(e) > 1:
1253                 items[e[0]] = e[1]
1254             else:
1255                 items[e[0]] = None
1256
1257         if self.tlsMode:
1258             self.authenticate(code, resp, items)
1259         else:
1260             self.tryTLS(code, resp, items)
1261
1262     def tryTLS(self, code, resp, items):
1263         if self.context and 'STARTTLS' in items:
1264             self._expected = [220]
1265             self._okresponse = self.esmtpState_starttls
1266             self._failresponse = self.esmtpTLSFailed
1267             self.sendLine('STARTTLS')
1268         elif self.requireTransportSecurity:
1269             self.tlsMode = False
1270             self.esmtpTLSRequired()
1271         else:
1272             self.tlsMode = False
1273             self.authenticate(code, resp, items)
1274
1275     def esmtpState_starttls(self, code, resp):
1276         try:
1277             self.transport.startTLS(self.context)
1278             self.tlsMode = True
1279         except:
1280             log.err()
1281             self.esmtpTLSFailed(451)
1282
1283         # Send another EHLO once TLS has been started to
1284         # get the TLS / AUTH schemes. Some servers only allow AUTH in TLS mode.
1285         self.esmtpState_ehlo(code, resp)
1286
1287     def authenticate(self, code, resp, items):
1288         if self.secret and items.get('AUTH'):
1289             schemes = items['AUTH'].split()
1290             tmpSchemes = {}
1291
1292             #XXX: May want to come up with a more efficient way to do this
1293             for s in schemes:
1294                 tmpSchemes[s.upper()] = 1
1295
1296             for a in self.authenticators:
1297                 auth = a.getName().upper()
1298
1299                 if auth in tmpSchemes:
1300                     self._authinfo = a
1301
1302                     # Special condition handled
1303                     if auth  == "PLAIN":
1304                         self._okresponse = self.smtpState_from
1305                         self._failresponse = self._esmtpState_plainAuth
1306                         self._expected = [235]
1307                         challenge = encode_base64(self._authinfo.challengeResponse(self.secret, 1), eol="")
1308                         self.sendLine('AUTH ' + auth + ' ' + challenge)
1309                     else:
1310                         self._expected = [334]
1311                         self._okresponse = self.esmtpState_challenge
1312                         # If some error occurs here, the server declined the AUTH
1313                         # before the user / password phase. This would be
1314                         # a very rare case
1315                         self._failresponse = self.esmtpAUTHServerError
1316                         self.sendLine('AUTH ' + auth)
1317                     return
1318
1319         if self.requireAuthentication:
1320             self.esmtpAUTHRequired()
1321         else:
1322             self.smtpState_from(code, resp)
1323
1324     def _esmtpState_plainAuth(self, code, resp):
1325         self._okresponse = self.smtpState_from
1326         self._failresponse = self.esmtpAUTHDeclined
1327         self._expected = [235]
1328         challenge = encode_base64(self._authinfo.challengeResponse(self.secret, 2), eol="")
1329         self.sendLine('AUTH PLAIN ' + challenge)
1330
1331     def esmtpState_challenge(self, code, resp):
1332         auth = self._authinfo
1333         del self._authinfo
1334         self._authResponse(auth, resp)
1335
1336     def _authResponse(self, auth, challenge):
1337         self._failresponse = self.esmtpAUTHDeclined
1338
1339         try:
1340             challenge = base64.decodestring(challenge)
1341         except binascii.Error, e:
1342             # Illegal challenge, give up, then quit
1343             self.sendLine('*')
1344             self._okresponse = self.esmtpAUTHMalformedChallenge
1345             self._failresponse = self.esmtpAUTHMalformedChallenge
1346         else:
1347             resp = auth.challengeResponse(self.secret, challenge)
1348             self._expected = [235]
1349             self._okresponse = self.smtpState_from
1350             self.sendLine(encode_base64(resp, eol=""))
1351
1352         if auth.getName() == "LOGIN" and challenge == "Username:":
1353             self._expected = [334]
1354             self._authinfo = auth
1355             self._okresponse = self.esmtpState_challenge
1356
1357
1358 class ESMTP(SMTP):
1359
1360     ctx = None
1361     canStartTLS = False
1362     startedTLS = False
1363
1364     authenticated = False
1365
1366     def __init__(self, chal = None, contextFactory = None):
1367         SMTP.__init__(self)
1368         if chal is None:
1369             chal = {}
1370         self.challengers = chal
1371         self.authenticated = False
1372         self.ctx = contextFactory
1373
1374     def connectionMade(self):
1375         SMTP.connectionMade(self)
1376         self.canStartTLS = ITLSTransport.providedBy(self.transport)
1377         self.canStartTLS = self.canStartTLS and (self.ctx is not None)
1378
1379     def extensions(self):
1380         ext = {'AUTH': self.challengers.keys()}
1381         if self.canStartTLS and not self.startedTLS:
1382             ext['STARTTLS'] = None
1383         return ext
1384
1385     def lookupMethod(self, command):
1386         m = SMTP.lookupMethod(self, command)
1387         if m is None:
1388             m = getattr(self, 'ext_' + command.upper(), None)
1389         return m
1390
1391     def listExtensions(self):
1392         r = []
1393         for (c, v) in self.extensions().iteritems():
1394             if v is not None:
1395                 if v:
1396                     # Intentionally omit extensions with empty argument lists
1397                     r.append('%s %s' % (c, ' '.join(v)))
1398             else:
1399                 r.append(c)
1400         return '\n'.join(r)
1401
1402     def do_EHLO(self, rest):
1403         peer = self.transport.getPeer().host
1404         self._helo = (rest, peer)
1405         self._from = None
1406         self._to = []
1407         self.sendCode(
1408             250,
1409             '%s Hello %s, nice to meet you\n%s' % (
1410                 self.host, peer,
1411                 self.listExtensions(),
1412             )
1413         )
1414
1415     def ext_STARTTLS(self, rest):
1416         if self.startedTLS:
1417             self.sendCode(503, 'TLS already negotiated')
1418         elif self.ctx and self.canStartTLS:
1419             self.sendCode(220, 'Begin TLS negotiation now')
1420             self.transport.startTLS(self.ctx)
1421             self.startedTLS = True
1422         else:
1423             self.sendCode(454, 'TLS not available')
1424
1425     def ext_AUTH(self, rest):
1426         if self.authenticated:
1427             self.sendCode(503, 'Already authenticated')
1428             return
1429         parts = rest.split(None, 1)
1430         chal = self.challengers.get(parts[0].upper(), lambda: None)()
1431         if not chal:
1432             self.sendCode(504, 'Unrecognized authentication type')
1433             return
1434         self.authenticate(chal)
1435
1436     def authenticate(self, challenger):
1437         if self.portal:
1438             challenge = challenger.getChallenge()
1439             coded = base64.encodestring(challenge)[:-1]
1440             self.sendCode(334, coded)
1441             self.mode = AUTH
1442             self.challenger = challenger
1443         else:
1444             self.sendCode(454, 'Temporary authentication failure')
1445
1446     def state_AUTH(self, rest):
1447         self.mode = COMMAND
1448
1449         if rest == '*':
1450             self.sendCode(501, 'Authentication aborted')
1451             self.challenger.abort()
1452             self.challenger = None
1453             return
1454
1455         try:
1456             uncoded = base64.decodestring(rest)
1457         except binascii.Error, e:
1458             self._ebAuthenticated(failure.Failure(e))
1459         else:
1460             self.challenger.setResponse(uncoded)
1461             if self.challenger.moreChallenges():
1462                 self.authenticate(self.challenger)
1463             else:
1464                 self.portal.login(self.challenger, None,
1465                         IMessageDeliveryFactory, IMessageDelivery
1466                     ).addCallback(self._cbAuthenticated
1467                     ).addCallback(lambda _: self.sendCode(235, 'Authentication successful.')
1468                     ).addErrback(self._ebAuthenticated
1469                     )
1470
1471 class SenderMixin:
1472     """Utility class for sending emails easily.
1473
1474     Use with SMTPSenderFactory or ESMTPSenderFactory.
1475     """
1476     done = 0
1477
1478     def getMailFrom(self):
1479         if not self.done:
1480             self.done = 1
1481             return str(self.factory.fromEmail)
1482         else:
1483             return None
1484
1485     def getMailTo(self):
1486         return self.factory.toEmail
1487
1488     def getMailData(self):
1489         return self.factory.file
1490
1491     def sendError(self, exc):
1492         # Call the base class to close the connection with the SMTP server
1493         SMTPClient.sendError(self, exc)
1494
1495         #  Do not retry to connect to SMTP Server if:
1496         #   1. No more retries left (This allows the correct error to be returned to the errorback)
1497         #   2. retry is false
1498         #   3. The error code is not in the 4xx range (Communication Errors)
1499
1500         if (self.factory.retries >= 0 or
1501             (not exc.retry and not (exc.code >= 400 and exc.code < 500))):
1502             self.factory.sendFinished = 1
1503             self.factory.result.errback(exc)
1504
1505     def sentMail(self, code, resp, numOk, addresses, log):
1506         # Do not retry, the SMTP server acknowledged the request
1507         self.factory.sendFinished = 1
1508         if code not in SUCCESS:
1509             errlog = []
1510             for addr, acode, aresp in addresses:
1511                 if code not in SUCCESS:
1512                     errlog.append("%s: %03d %s" % (addr, acode, aresp))
1513
1514             errlog.append(log.str())
1515
1516             exc = SMTPDeliveryError(code, resp, '\n'.join(errlog), addresses)
1517             self.factory.result.errback(exc)
1518         else:
1519             self.factory.result.callback((numOk, addresses))
1520
1521
1522 class SMTPSender(SenderMixin, SMTPClient):
1523     pass
1524
1525
1526 class SMTPSenderFactory(protocol.ClientFactory):
1527     """
1528     Utility factory for sending emails easily.
1529     """
1530
1531     domain = DNSNAME
1532     protocol = SMTPSender
1533
1534     def __init__(self, fromEmail, toEmail, file, deferred, retries=5,
1535                  timeout=None):
1536         """
1537         @param fromEmail: The RFC 2821 address from which to send this
1538         message.
1539
1540         @param toEmail: A sequence of RFC 2821 addresses to which to
1541         send this message.
1542
1543         @param file: A file-like object containing the message to send.
1544
1545         @param deferred: A Deferred to callback or errback when sending
1546         of this message completes.
1547
1548         @param retries: The number of times to retry delivery of this
1549         message.
1550
1551         @param timeout: Period, in seconds, for which to wait for
1552         server responses, or None to wait forever.
1553         """
1554         assert isinstance(retries, (int, long))
1555
1556         if isinstance(toEmail, types.StringTypes):
1557             toEmail = [toEmail]
1558         self.fromEmail = Address(fromEmail)
1559         self.nEmails = len(toEmail)
1560         self.toEmail = iter(toEmail)
1561         self.file = file
1562         self.result = deferred
1563         self.result.addBoth(self._removeDeferred)
1564         self.sendFinished = 0
1565
1566         self.retries = -retries
1567         self.timeout = timeout
1568
1569     def _removeDeferred(self, argh):
1570         del self.result
1571         return argh
1572
1573     def clientConnectionFailed(self, connector, err):
1574         self._processConnectionError(connector, err)
1575
1576     def clientConnectionLost(self, connector, err):
1577         self._processConnectionError(connector, err)
1578
1579     def _processConnectionError(self, connector, err):
1580         if self.retries < self.sendFinished <= 0:
1581             log.msg("SMTP Client retrying server. Retry: %s" % -self.retries)
1582
1583             connector.connect()
1584             self.retries += 1
1585         elif self.sendFinished <= 0:
1586             # If we were unable to communicate with the SMTP server a ConnectionDone will be
1587             # returned. We want a more clear error message for debugging
1588             if err.check(error.ConnectionDone):
1589                 err.value = SMTPConnectError(-1, "Unable to connect to server.")
1590             self.result.errback(err.value)
1591
1592     def buildProtocol(self, addr):
1593         p = self.protocol(self.domain, self.nEmails*2+2)
1594         p.factory = self
1595         p.timeout = self.timeout
1596         return p
1597
1598
1599 class IClientAuthentication(components.Interface):
1600     def getName(self):
1601         """Return an identifier associated with this authentication scheme.
1602
1603         @rtype: C{str}
1604         """
1605
1606     def challengeResponse(self, secret, challenge):
1607         """Generate a challenge response string"""
1608
1609
1610 class CramMD5ClientAuthenticator:
1611     implements(IClientAuthentication)
1612
1613     def __init__(self, user):
1614         self.user = user
1615
1616     def getName(self):
1617         return "CRAM-MD5"
1618
1619     def challengeResponse(self, secret, chal):
1620         response = hmac.HMAC(secret, chal).hexdigest()
1621         return '%s %s' % (self.user, response)
1622
1623 components.backwardsCompatImplements(CramMD5ClientAuthenticator)
1624
1625
1626 class LOGINAuthenticator:
1627     implements(IClientAuthentication)
1628
1629     def __init__(self, user):
1630         self.user = user
1631
1632     def getName(self):
1633         return "LOGIN"
1634
1635     def challengeResponse(self, secret, chal):
1636         if chal== "Username:":
1637             return self.user
1638         elif chal == 'Password:':
1639             return secret
1640
1641 components.backwardsCompatImplements(LOGINAuthenticator)
1642
1643 class PLAINAuthenticator:
1644     implements(IClientAuthentication)
1645
1646     def __init__(self, user):
1647         self.user = user
1648
1649     def getName(self):
1650         return "PLAIN"
1651
1652     def challengeResponse(self, secret, chal=1):
1653         if chal == 1:
1654            return "%s\0%s\0%s" % (self.user, self.user, secret)
1655         else:
1656            return "%s\0%s" % (self.user, secret)
1657
1658 components.backwardsCompatImplements(PLAINAuthenticator)
1659
1660
1661 class ESMTPSender(SenderMixin, ESMTPClient):
1662
1663     requireAuthentication = True
1664     requireTransportSecurity = True
1665
1666     def __init__(self, username, secret, contextFactory=None, *args, **kw):
1667         self.heloFallback = 0
1668         self.username = username
1669
1670         if contextFactory is None:
1671             contextFactory = self._getContextFactory()
1672
1673         ESMTPClient.__init__(self, secret, contextFactory, *args, **kw)
1674
1675         self._registerAuthenticators()
1676
1677     def _registerAuthenticators(self):
1678         # Register Authenticator in order from most secure to least secure
1679         self.registerAuthenticator(CramMD5ClientAuthenticator(self.username))
1680         self.registerAuthenticator(LOGINAuthenticator(self.username))
1681         self.registerAuthenticator(PLAINAuthenticator(self.username))
1682
1683     def _getContextFactory(self):
1684         if self.context is not None:
1685             return self.context
1686         try:
1687             from twisted.internet import ssl
1688         except ImportError:
1689             return None
1690         else:
1691             try:
1692                 context = ssl.ClientContextFactory()
1693                 context.method = ssl.SSL.TLSv1_METHOD
1694                 return context
1695             except AttributeError:
1696                 return None
1697
1698
1699 class ESMTPSenderFactory(SMTPSenderFactory):
1700     """
1701     Utility factory for sending emails easily.
1702     """
1703
1704     protocol = ESMTPSender
1705
1706     def __init__(self, username, password, fromEmail, toEmail, file,
1707                  deferred, retries=5, timeout=None,
1708                  contextFactory=None, heloFallback=False,
1709                  requireAuthentication=True,
1710                  requireTransportSecurity=True):
1711
1712         SMTPSenderFactory.__init__(self, fromEmail, toEmail, file, deferred, retries, timeout)
1713         self.username = username
1714         self.password = password
1715         self._contextFactory = contextFactory
1716         self._heloFallback = heloFallback
1717         self._requireAuthentication = requireAuthentication
1718         self._requireTransportSecurity = requireTransportSecurity
1719
1720     def buildProtocol(self, addr):
1721         p = self.protocol(self.username, self.password, self._contextFactory, self.domain, self.nEmails*2+2)
1722         p.heloFallback = self._heloFallback
1723         p.requireAuthentication = self._requireAuthentication
1724         p.requireTransportSecurity = self._requireTransportSecurity
1725         p.factory = self
1726         p.timeout = self.timeout
1727         return p
1728
1729 def sendmail(smtphost, from_addr, to_addrs, msg, senderDomainName=None, port=25):
1730     """Send an email
1731
1732     This interface is intended to be a direct replacement for
1733     smtplib.SMTP.sendmail() (with the obvious change that
1734     you specify the smtphost as well). Also, ESMTP options
1735     are not accepted, as we don't do ESMTP yet. I reserve the
1736     right to implement the ESMTP options differently.
1737
1738     @param smtphost: The host the message should be sent to
1739     @param from_addr: The (envelope) address sending this mail.
1740     @param to_addrs: A list of addresses to send this mail to.  A string will
1741         be treated as a list of one address
1742     @param msg: The message, including headers, either as a file or a string.
1743         File-like objects need to support read() and close(). Lines must be
1744         delimited by '\\n'. If you pass something that doesn't look like a
1745         file, we try to convert it to a string (so you should be able to
1746         pass an email.Message directly, but doing the conversion with
1747         email.Generator manually will give you more control over the
1748         process).
1749
1750     @param senderDomainName: Name by which to identify.  If None, try
1751     to pick something sane (but this depends on external configuration
1752     and may not succeed).
1753
1754     @param port: Remote port to which to connect.
1755
1756     @rtype: L{Deferred}
1757     @returns: A L{Deferred}, its callback will be called if a message is sent
1758         to ANY address, the errback if no message is sent.
1759
1760         The callback will be called with a tuple (numOk, addresses) where numOk
1761         is the number of successful recipient addresses and addresses is a list
1762         of tuples (address, code, resp) giving the response to the RCPT command
1763         for each address.
1764     """
1765     if not hasattr(msg,'read'):
1766         # It's not a file
1767         msg = StringIO(str(msg))
1768
1769     d = defer.Deferred()
1770     factory = SMTPSenderFactory(from_addr, to_addrs, msg, d)
1771
1772     if senderDomainName is not None:
1773         factory.domain = senderDomainName
1774
1775     reactor.connectTCP(smtphost, port, factory)
1776
1777     return d
1778
1779 def sendEmail(smtphost, fromEmail, toEmail, content, headers = None, attachments = None, multipartbody = "mixed"):
1780     """Send an email, optionally with attachments.
1781
1782     @type smtphost: str
1783     @param smtphost: hostname of SMTP server to which to connect
1784
1785     @type fromEmail: str
1786     @param fromEmail: email address to indicate this email is from
1787
1788     @type toEmail: str
1789     @param toEmail: email address to which to send this email
1790
1791     @type content: str
1792     @param content: The body if this email.
1793
1794     @type headers: dict
1795     @param headers: Dictionary of headers to include in the email
1796
1797     @type attachments: list of 3-tuples
1798     @param attachments: Each 3-tuple should consist of the name of the
1799       attachment, the mime-type of the attachment, and a string that is
1800       the attachment itself.
1801
1802     @type multipartbody: str
1803     @param multipartbody: The type of MIME multi-part body.  Generally
1804       either "mixed" (as in text and images) or "alternative" (html email
1805       with a fallback to text/plain).
1806
1807     @rtype: Deferred
1808     @return: The returned Deferred has its callback or errback invoked when
1809       the mail is successfully sent or when an error occurs, respectively.
1810     """
1811     warnings.warn("smtp.sendEmail may go away in the future.\n"
1812                   "  Consider revising your code to use the email module\n"
1813                   "  and smtp.sendmail.",
1814                   category=DeprecationWarning, stacklevel=2)
1815
1816     f = tempfile.TemporaryFile()
1817     writer = MimeWriter.MimeWriter(f)
1818
1819     writer.addheader("Mime-Version", "1.0")
1820     if headers:
1821         # Setup the mail headers
1822         for (header, value) in headers.items():
1823             writer.addheader(header, value)
1824
1825         headkeys = [k.lower() for k in headers.keys()]
1826     else:
1827         headkeys = ()
1828
1829     # Add required headers if not present
1830     if "message-id" not in headkeys:
1831         writer.addheader("Message-ID", messageid())
1832     if "date" not in headkeys:
1833         writer.addheader("Date", rfc822date())
1834     if "from" not in headkeys and "sender" not in headkeys:
1835         writer.addheader("From", fromEmail)
1836     if "to" not in headkeys and "cc" not in headkeys and "bcc" not in headkeys:
1837         writer.addheader("To", toEmail)
1838
1839     writer.startmultipartbody(multipartbody)
1840
1841     # message body
1842     part = writer.nextpart()
1843     body = part.startbody("text/plain")
1844     body.write(content)
1845
1846     if attachments is not None:
1847         # add attachments
1848         for (file, mime, attachment) in attachments:
1849             part = writer.nextpart()
1850             if mime.startswith('text'):
1851                 encoding = "7bit"
1852             else:
1853                 attachment = base64.encodestring(attachment)
1854                 encoding = "base64"
1855             part.addheader("Content-Transfer-Encoding", encoding)
1856             body = part.startbody("%s; name=%s" % (mime, file))
1857             body.write(attachment)
1858
1859     # finish
1860     writer.lastpart()
1861
1862     # send message
1863     f.seek(0, 0)
1864     d = defer.Deferred()
1865     factory = SMTPSenderFactory(fromEmail, toEmail, f, d)
1866     reactor.connectTCP(smtphost, 25, factory)
1867
1868     return d
1869
1870 ##
1871 ## Yerg.  Codecs!
1872 ##
1873 import codecs
1874 def xtext_encode(s):
1875     r = []
1876     for ch in s:
1877         o = ord(ch)
1878         if ch == '+' or ch == '=' or o < 33 or o > 126:
1879             r.append('+%02X' % o)
1880         else:
1881             r.append(ch)
1882     return (''.join(r), len(s))
1883
1884 try:
1885     from twisted.protocols._c_urlarg import unquote as _helper_unquote
1886 except ImportError:
1887     def xtext_decode(s):
1888         r = []
1889         i = 0
1890         while i < len(s):
1891             if s[i] == '+':
1892                 try:
1893                     r.append(chr(int(s[i + 1:i + 3], 16)))
1894                 except ValueError:
1895                     r.append(s[i:i + 3])
1896                 i += 3
1897             else:
1898                 r.append(s[i])
1899                 i += 1
1900         return (''.join(r), len(s))
1901 else:
1902     def xtext_decode(s):
1903         return (_helper_unquote(s, '+'), len(s))
1904
1905 class xtextStreamReader(codecs.StreamReader):
1906     def decode(self, s, errors='strict'):
1907         return xtext_decode(s)
1908
1909 class xtextStreamWriter(codecs.StreamWriter):
1910     def decode(self, s, errors='strict'):
1911         return xtext_encode(s)
1912
1913 def xtext_codec(name):
1914     if name == 'xtext':
1915         return (xtext_encode, xtext_decode, xtextStreamReader, xtextStreamWriter)
1916 codecs.register(xtext_codec)
Note: See TracBrowser for help on using the browser.