root/trunk/twisted/mail/smtp.py

Revision 32426, 62.5 KB (checked in by exarkun, 10 months ago)

Merge smtp-login-4692

Author: retenodus
Reviewer: exarkun
Fixes: #4692

Change twisted.mail.smtp.LOGINCredentials to emit Outlook-compatible
challenge strings.

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