Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(948)

Side by Side Diff: third_party/twisted_8_1/twisted/mail/smtp.py

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

Powered by Google App Engine
This is Rietveld 408576698