~ntt-pf-lab/nova/monkey_patch_notification

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/mail/smtp.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

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