OLD | NEW |
| (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) | |
OLD | NEW |