| 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 |