1# Copyright (c) Twisted Matrix Laboratories. 2# See LICENSE for details. 3 4""" 5Test cases for twisted.mail.smtp module. 6""" 7import inspect 8 9from zope.interface import implements, directlyProvides 10 11from twisted.python.util import LineLog 12from twisted.trial import unittest, util 13from twisted.protocols import basic, loopback 14from twisted.mail import smtp 15from twisted.internet import defer, protocol, reactor, interfaces 16from twisted.internet import address, error, task 17from twisted.test.proto_helpers import MemoryReactor, StringTransport 18 19from twisted import cred 20import twisted.cred.error 21import twisted.cred.portal 22import twisted.cred.checkers 23import twisted.cred.credentials 24 25from twisted.cred.portal import IRealm, Portal 26from twisted.cred.checkers import ICredentialsChecker, AllowAnonymousAccess 27from twisted.cred.credentials import IAnonymous 28from twisted.cred.error import UnauthorizedLogin 29 30from twisted.mail import imap4 31 32 33try: 34 from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext 35except ImportError: 36 sslSkip = "OpenSSL not present" 37else: 38 sslSkip = None 39 40import re 41 42try: 43 from cStringIO import StringIO 44except ImportError: 45 from StringIO import StringIO 46 47 48def spameater(*spam, **eggs): 49 return None 50 51 52 53class BrokenMessage(object): 54 """ 55 L{BrokenMessage} is an L{IMessage} which raises an unexpected exception 56 from its C{eomReceived} method. This is useful for creating a server which 57 can be used to test client retry behavior. 58 """ 59 implements(smtp.IMessage) 60 61 def __init__(self, user): 62 pass 63 64 65 def lineReceived(self, line): 66 pass 67 68 69 def eomReceived(self): 70 raise RuntimeError("Some problem, delivery is failing.") 71 72 73 def connectionLost(self): 74 pass 75 76 77 78class DummyMessage(object): 79 """ 80 L{BrokenMessage} is an L{IMessage} which saves the message delivered to it 81 to its domain object. 82 83 @ivar domain: A L{DummyDomain} which will be used to store the message once 84 it is received. 85 """ 86 def __init__(self, domain, user): 87 self.domain = domain 88 self.user = user 89 self.buffer = [] 90 91 92 def lineReceived(self, line): 93 # Throw away the generated Received: header 94 if not re.match('Received: From yyy.com \(\[.*\]\) by localhost;', line): 95 self.buffer.append(line) 96 97 98 def eomReceived(self): 99 message = '\n'.join(self.buffer) + '\n' 100 self.domain.messages[self.user.dest.local].append(message) 101 deferred = defer.Deferred() 102 deferred.callback("saved") 103 return deferred 104 105 106 107class DummyDomain(object): 108 """ 109 L{DummyDomain} is an L{IDomain} which keeps track of messages delivered to 110 it in memory. 111 """ 112 def __init__(self, names): 113 self.messages = {} 114 for name in names: 115 self.messages[name] = [] 116 117 118 def exists(self, user): 119 if user.dest.local in self.messages: 120 return defer.succeed(lambda: DummyMessage(self, user)) 121 return defer.fail(smtp.SMTPBadRcpt(user)) 122 123 124 125class SMTPTestCase(unittest.TestCase): 126 127 messages = [('foo@bar.com', ['foo@baz.com', 'qux@baz.com'], '''\ 128Subject: urgent\015 129\015 130Someone set up us the bomb!\015 131''')] 132 133 mbox = {'foo': ['Subject: urgent\n\nSomeone set up us the bomb!\n']} 134 135 def setUp(self): 136 """ 137 Create an in-memory mail domain to which messages may be delivered by 138 tests and create a factory and transport to do the delivering. 139 """ 140 self.factory = smtp.SMTPFactory() 141 self.factory.domains = {} 142 self.factory.domains['baz.com'] = DummyDomain(['foo']) 143 self.transport = StringTransport() 144 145 146 def testMessages(self): 147 from twisted.mail import protocols 148 protocol = protocols.DomainSMTP() 149 protocol.service = self.factory 150 protocol.factory = self.factory 151 protocol.receivedHeader = spameater 152 protocol.makeConnection(self.transport) 153 protocol.lineReceived('HELO yyy.com') 154 for message in self.messages: 155 protocol.lineReceived('MAIL FROM:<%s>' % message[0]) 156 for target in message[1]: 157 protocol.lineReceived('RCPT TO:<%s>' % target) 158 protocol.lineReceived('DATA') 159 protocol.dataReceived(message[2]) 160 protocol.lineReceived('.') 161 protocol.lineReceived('QUIT') 162 if self.mbox != self.factory.domains['baz.com'].messages: 163 raise AssertionError(self.factory.domains['baz.com'].messages) 164 protocol.setTimeout(None) 165 166 testMessages.suppress = [util.suppress(message='DomainSMTP', category=DeprecationWarning)] 167 168mail = '''\ 169Subject: hello 170 171Goodbye 172''' 173 174class MyClient: 175 def __init__(self, messageInfo=None): 176 if messageInfo is None: 177 messageInfo = ( 178 'moshez@foo.bar', ['moshez@foo.bar'], StringIO(mail)) 179 self._sender = messageInfo[0] 180 self._recipient = messageInfo[1] 181 self._data = messageInfo[2] 182 183 184 def getMailFrom(self): 185 return self._sender 186 187 188 def getMailTo(self): 189 return self._recipient 190 191 192 def getMailData(self): 193 return self._data 194 195 196 def sendError(self, exc): 197 self._error = exc 198 199 200 def sentMail(self, code, resp, numOk, addresses, log): 201 # Prevent another mail from being sent. 202 self._sender = None 203 self._recipient = None 204 self._data = None 205 206 207 208class MySMTPClient(MyClient, smtp.SMTPClient): 209 def __init__(self, messageInfo=None): 210 smtp.SMTPClient.__init__(self, 'foo.baz') 211 MyClient.__init__(self, messageInfo) 212 213class MyESMTPClient(MyClient, smtp.ESMTPClient): 214 def __init__(self, secret = '', contextFactory = None): 215 smtp.ESMTPClient.__init__(self, secret, contextFactory, 'foo.baz') 216 MyClient.__init__(self) 217 218class LoopbackMixin: 219 def loopback(self, server, client): 220 return loopback.loopbackTCP(server, client) 221 222class LoopbackTestCase(LoopbackMixin): 223 def testMessages(self): 224 factory = smtp.SMTPFactory() 225 factory.domains = {} 226 factory.domains['foo.bar'] = DummyDomain(['moshez']) 227 from twisted.mail.protocols import DomainSMTP 228 protocol = DomainSMTP() 229 protocol.service = factory 230 protocol.factory = factory 231 clientProtocol = self.clientClass() 232 return self.loopback(protocol, clientProtocol) 233 testMessages.suppress = [util.suppress(message='DomainSMTP', category=DeprecationWarning)] 234 235class LoopbackSMTPTestCase(LoopbackTestCase, unittest.TestCase): 236 clientClass = MySMTPClient 237 238class LoopbackESMTPTestCase(LoopbackTestCase, unittest.TestCase): 239 clientClass = MyESMTPClient 240 241 242class FakeSMTPServer(basic.LineReceiver): 243 244 clientData = [ 245 '220 hello', '250 nice to meet you', 246 '250 great', '250 great', '354 go on, lad' 247 ] 248 249 def connectionMade(self): 250 self.buffer = [] 251 self.clientData = self.clientData[:] 252 self.clientData.reverse() 253 self.sendLine(self.clientData.pop()) 254 255 def lineReceived(self, line): 256 self.buffer.append(line) 257 if line == "QUIT": 258 self.transport.write("221 see ya around\r\n") 259 self.transport.loseConnection() 260 elif line == ".": 261 self.transport.write("250 gotcha\r\n") 262 elif line == "RSET": 263 self.transport.loseConnection() 264 265 if self.clientData: 266 self.sendLine(self.clientData.pop()) 267 268 269class SMTPClientTestCase(unittest.TestCase, LoopbackMixin): 270 """ 271 Tests for L{smtp.SMTPClient}. 272 """ 273 274 def test_timeoutConnection(self): 275 """ 276 L{smtp.SMTPClient.timeoutConnection} calls the C{sendError} hook with a 277 fatal L{SMTPTimeoutError} with the current line log. 278 """ 279 errors = [] 280 client = MySMTPClient() 281 client.sendError = errors.append 282 client.makeConnection(StringTransport()) 283 client.lineReceived("220 hello") 284 client.timeoutConnection() 285 self.assertIsInstance(errors[0], smtp.SMTPTimeoutError) 286 self.assertTrue(errors[0].isFatal) 287 self.assertEqual( 288 str(errors[0]), 289 "Timeout waiting for SMTP server response\n" 290 "<<< 220 hello\n" 291 ">>> HELO foo.baz\n") 292 293 294 expected_output = [ 295 'HELO foo.baz', 'MAIL FROM:<moshez@foo.bar>', 296 'RCPT TO:<moshez@foo.bar>', 'DATA', 297 'Subject: hello', '', 'Goodbye', '.', 'RSET' 298 ] 299 300 def test_messages(self): 301 """ 302 L{smtp.SMTPClient} sends I{HELO}, I{MAIL FROM}, I{RCPT TO}, and I{DATA} 303 commands based on the return values of its C{getMailFrom}, 304 C{getMailTo}, and C{getMailData} methods. 305 """ 306 client = MySMTPClient() 307 server = FakeSMTPServer() 308 d = self.loopback(server, client) 309 d.addCallback(lambda x : 310 self.assertEqual(server.buffer, self.expected_output)) 311 return d 312 313 314 def test_transferError(self): 315 """ 316 If there is an error while producing the message body to the 317 connection, the C{sendError} callback is invoked. 318 """ 319 client = MySMTPClient( 320 ('alice@example.com', ['bob@example.com'], StringIO("foo"))) 321 transport = StringTransport() 322 client.makeConnection(transport) 323 client.dataReceived( 324 '220 Ok\r\n' # Greeting 325 '250 Ok\r\n' # EHLO response 326 '250 Ok\r\n' # MAIL FROM response 327 '250 Ok\r\n' # RCPT TO response 328 '354 Ok\r\n' # DATA response 329 ) 330 331 # Sanity check - a pull producer should be registered now. 332 self.assertNotIdentical(transport.producer, None) 333 self.assertFalse(transport.streaming) 334 335 # Now stop the producer prematurely, meaning the message was not sent. 336 transport.producer.stopProducing() 337 338 # The sendError hook should have been invoked as a result. 339 self.assertIsInstance(client._error, Exception) 340 341 342 def test_sendFatalError(self): 343 """ 344 If L{smtp.SMTPClient.sendError} is called with an L{SMTPClientError} 345 which is fatal, it disconnects its transport without writing anything 346 more to it. 347 """ 348 client = smtp.SMTPClient(None) 349 transport = StringTransport() 350 client.makeConnection(transport) 351 client.sendError(smtp.SMTPClientError(123, "foo", isFatal=True)) 352 self.assertEqual(transport.value(), "") 353 self.assertTrue(transport.disconnecting) 354 355 356 def test_sendNonFatalError(self): 357 """ 358 If L{smtp.SMTPClient.sendError} is called with an L{SMTPClientError} 359 which is not fatal, it sends C{"QUIT"} and waits for the server to 360 close the connection. 361 """ 362 client = smtp.SMTPClient(None) 363 transport = StringTransport() 364 client.makeConnection(transport) 365 client.sendError(smtp.SMTPClientError(123, "foo", isFatal=False)) 366 self.assertEqual(transport.value(), "QUIT\r\n") 367 self.assertFalse(transport.disconnecting) 368 369 370 def test_sendOtherError(self): 371 """ 372 If L{smtp.SMTPClient.sendError} is called with an exception which is 373 not an L{SMTPClientError}, it disconnects its transport without 374 writing anything more to it. 375 """ 376 client = smtp.SMTPClient(None) 377 transport = StringTransport() 378 client.makeConnection(transport) 379 client.sendError(Exception("foo")) 380 self.assertEqual(transport.value(), "") 381 self.assertTrue(transport.disconnecting) 382 383 384 385class DummySMTPMessage: 386 387 def __init__(self, protocol, users): 388 self.protocol = protocol 389 self.users = users 390 self.buffer = [] 391 392 def lineReceived(self, line): 393 self.buffer.append(line) 394 395 def eomReceived(self): 396 message = '\n'.join(self.buffer) + '\n' 397 helo, origin = self.users[0].helo[0], str(self.users[0].orig) 398 recipients = [] 399 for user in self.users: 400 recipients.append(str(user)) 401 self.protocol.message[tuple(recipients)] = (helo, origin, recipients, message) 402 return defer.succeed("saved") 403 404 405 406class DummyProto: 407 def connectionMade(self): 408 self.dummyMixinBase.connectionMade(self) 409 self.message = {} 410 411 412 def receivedHeader(*spam): 413 return None 414 415 416 def validateTo(self, user): 417 self.delivery = SimpleDelivery(None) 418 return lambda: DummySMTPMessage(self, [user]) 419 420 421 def validateFrom(self, helo, origin): 422 return origin 423 424 425 426class DummySMTP(DummyProto, smtp.SMTP): 427 dummyMixinBase = smtp.SMTP 428 429class DummyESMTP(DummyProto, smtp.ESMTP): 430 dummyMixinBase = smtp.ESMTP 431 432class AnotherTestCase: 433 serverClass = None 434 clientClass = None 435 436 messages = [ ('foo.com', 'moshez@foo.com', ['moshez@bar.com'], 437 'moshez@foo.com', ['moshez@bar.com'], '''\ 438From: Moshe 439To: Moshe 440 441Hi, 442how are you? 443'''), 444 ('foo.com', 'tttt@rrr.com', ['uuu@ooo', 'yyy@eee'], 445 'tttt@rrr.com', ['uuu@ooo', 'yyy@eee'], '''\ 446Subject: pass 447 448..rrrr.. 449'''), 450 ('foo.com', '@this,@is,@ignored:foo@bar.com', 451 ['@ignore,@this,@too:bar@foo.com'], 452 'foo@bar.com', ['bar@foo.com'], '''\ 453Subject: apa 454To: foo 455 456123 457. 458456 459'''), 460 ] 461 462 data = [ 463 ('', '220.*\r\n$', None, None), 464 ('HELO foo.com\r\n', '250.*\r\n$', None, None), 465 ('RSET\r\n', '250.*\r\n$', None, None), 466 ] 467 for helo_, from_, to_, realfrom, realto, msg in messages: 468 data.append(('MAIL FROM:<%s>\r\n' % from_, '250.*\r\n', 469 None, None)) 470 for rcpt in to_: 471 data.append(('RCPT TO:<%s>\r\n' % rcpt, '250.*\r\n', 472 None, None)) 473 474 data.append(('DATA\r\n','354.*\r\n', 475 msg, ('250.*\r\n', 476 (helo_, realfrom, realto, msg)))) 477 478 479 def test_buffer(self): 480 """ 481 Exercise a lot of the SMTP client code. This is a "shotgun" style unit 482 test. It does a lot of things and hopes that something will go really 483 wrong if it is going to go wrong. This test should be replaced with a 484 suite of nicer tests. 485 """ 486 transport = StringTransport() 487 a = self.serverClass() 488 class fooFactory: 489 domain = 'foo.com' 490 491 a.factory = fooFactory() 492 a.makeConnection(transport) 493 for (send, expect, msg, msgexpect) in self.data: 494 if send: 495 a.dataReceived(send) 496 data = transport.value() 497 transport.clear() 498 if not re.match(expect, data): 499 raise AssertionError, (send, expect, data) 500 if data[:3] == '354': 501 for line in msg.splitlines(): 502 if line and line[0] == '.': 503 line = '.' + line 504 a.dataReceived(line + '\r\n') 505 a.dataReceived('.\r\n') 506 # Special case for DATA. Now we want a 250, and then 507 # we compare the messages 508 data = transport.value() 509 transport.clear() 510 resp, msgdata = msgexpect 511 if not re.match(resp, data): 512 raise AssertionError, (resp, data) 513 for recip in msgdata[2]: 514 expected = list(msgdata[:]) 515 expected[2] = [recip] 516 self.assertEqual( 517 a.message[(recip,)], 518 tuple(expected) 519 ) 520 a.setTimeout(None) 521 522 523class AnotherESMTPTestCase(AnotherTestCase, unittest.TestCase): 524 serverClass = DummyESMTP 525 clientClass = MyESMTPClient 526 527class AnotherSMTPTestCase(AnotherTestCase, unittest.TestCase): 528 serverClass = DummySMTP 529 clientClass = MySMTPClient 530 531 532 533class DummyChecker: 534 implements(cred.checkers.ICredentialsChecker) 535 536 users = { 537 'testuser': 'testpassword' 538 } 539 540 credentialInterfaces = (cred.credentials.IUsernamePassword, 541 cred.credentials.IUsernameHashedPassword) 542 543 def requestAvatarId(self, credentials): 544 return defer.maybeDeferred( 545 credentials.checkPassword, self.users[credentials.username] 546 ).addCallback(self._cbCheck, credentials.username) 547 548 def _cbCheck(self, result, username): 549 if result: 550 return username 551 raise cred.error.UnauthorizedLogin() 552 553 554 555class SimpleDelivery(object): 556 """ 557 L{SimpleDelivery} is a message delivery factory with no interesting 558 behavior. 559 """ 560 implements(smtp.IMessageDelivery) 561 562 def __init__(self, messageFactory): 563 self._messageFactory = messageFactory 564 565 566 def receivedHeader(self, helo, origin, recipients): 567 return None 568 569 570 def validateFrom(self, helo, origin): 571 return origin 572 573 574 def validateTo(self, user): 575 return lambda: self._messageFactory(user) 576 577 578 579class DummyRealm: 580 def requestAvatar(self, avatarId, mind, *interfaces): 581 return smtp.IMessageDelivery, SimpleDelivery(None), lambda: None 582 583 584 585class AuthTestCase(unittest.TestCase, LoopbackMixin): 586 def test_crammd5Auth(self): 587 """ 588 L{ESMTPClient} can authenticate using the I{CRAM-MD5} SASL mechanism. 589 590 @see: U{http://tools.ietf.org/html/rfc2195} 591 """ 592 realm = DummyRealm() 593 p = cred.portal.Portal(realm) 594 p.registerChecker(DummyChecker()) 595 596 server = DummyESMTP({'CRAM-MD5': cred.credentials.CramMD5Credentials}) 597 server.portal = p 598 client = MyESMTPClient('testpassword') 599 600 cAuth = smtp.CramMD5ClientAuthenticator('testuser') 601 client.registerAuthenticator(cAuth) 602 603 d = self.loopback(server, client) 604 d.addCallback(lambda x : self.assertEqual(server.authenticated, 1)) 605 return d 606 607 608 def test_loginAuth(self): 609 """ 610 L{ESMTPClient} can authenticate using the I{LOGIN} SASL mechanism. 611 612 @see: U{http://sepp.oetiker.ch/sasl-2.1.19-ds/draft-murchison-sasl-login-00.txt} 613 """ 614 realm = DummyRealm() 615 p = cred.portal.Portal(realm) 616 p.registerChecker(DummyChecker()) 617 618 server = DummyESMTP({'LOGIN': imap4.LOGINCredentials}) 619 server.portal = p 620 client = MyESMTPClient('testpassword') 621 622 cAuth = smtp.LOGINAuthenticator('testuser') 623 client.registerAuthenticator(cAuth) 624 625 d = self.loopback(server, client) 626 d.addCallback(lambda x: self.assertTrue(server.authenticated)) 627 return d 628 629 630 def test_loginAgainstWeirdServer(self): 631 """ 632 When communicating with a server which implements the I{LOGIN} SASL 633 mechanism using C{"Username:"} as the challenge (rather than C{"User 634 Name\\0"}), L{ESMTPClient} can still authenticate successfully using 635 the I{LOGIN} mechanism. 636 """ 637 realm = DummyRealm() 638 p = cred.portal.Portal(realm) 639 p.registerChecker(DummyChecker()) 640 641 server = DummyESMTP({'LOGIN': smtp.LOGINCredentials}) 642 server.portal = p 643 644 client = MyESMTPClient('testpassword') 645 cAuth = smtp.LOGINAuthenticator('testuser') 646 client.registerAuthenticator(cAuth) 647 648 d = self.loopback(server, client) 649 d.addCallback(lambda x: self.assertTrue(server.authenticated)) 650 return d 651 652 653 654class SMTPHelperTestCase(unittest.TestCase): 655 def testMessageID(self): 656 d = {} 657 for i in range(1000): 658 m = smtp.messageid('testcase') 659 self.failIf(m in d) 660 d[m] = None 661 662 def testQuoteAddr(self): 663 cases = [ 664 ['user@host.name', '<user@host.name>'], 665 ['"User Name" <user@host.name>', '<user@host.name>'], 666 [smtp.Address('someguy@someplace'), '<someguy@someplace>'], 667 ['', '<>'], 668 [smtp.Address(''), '<>'], 669 ] 670 671 for (c, e) in cases: 672 self.assertEqual(smtp.quoteaddr(c), e) 673 674 def testUser(self): 675 u = smtp.User('user@host', 'helo.host.name', None, None) 676 self.assertEqual(str(u), 'user@host') 677 678 def testXtextEncoding(self): 679 cases = [ 680 ('Hello world', 'Hello+20world'), 681 ('Hello+world', 'Hello+2Bworld'), 682 ('\0\1\2\3\4\5', '+00+01+02+03+04+05'), 683 ('e=mc2@example.com', 'e+3Dmc2@example.com') 684 ] 685 686 for (case, expected) in cases: 687 self.assertEqual(smtp.xtext_encode(case), (expected, len(case))) 688 self.assertEqual(case.encode('xtext'), expected) 689 self.assertEqual( 690 smtp.xtext_decode(expected), (case, len(expected))) 691 self.assertEqual(expected.decode('xtext'), case) 692 693 694 def test_encodeWithErrors(self): 695 """ 696 Specifying an error policy to C{unicode.encode} with the 697 I{xtext} codec should produce the same result as not 698 specifying the error policy. 699 """ 700 text = u'Hello world' 701 self.assertEqual( 702 smtp.xtext_encode(text, 'strict'), 703 (text.encode('xtext'), len(text))) 704 self.assertEqual( 705 text.encode('xtext', 'strict'), 706 text.encode('xtext')) 707 708 709 def test_decodeWithErrors(self): 710 """ 711 Similar to L{test_encodeWithErrors}, but for C{str.decode}. 712 """ 713 bytes = 'Hello world' 714 self.assertEqual( 715 smtp.xtext_decode(bytes, 'strict'), 716 (bytes.decode('xtext'), len(bytes))) 717 self.assertEqual( 718 bytes.decode('xtext', 'strict'), 719 bytes.decode('xtext')) 720 721 722 723class NoticeTLSClient(MyESMTPClient): 724 tls = False 725 726 def esmtpState_starttls(self, code, resp): 727 MyESMTPClient.esmtpState_starttls(self, code, resp) 728 self.tls = True 729 730 731 732class TLSTestCase(unittest.TestCase, LoopbackMixin): 733 if sslSkip is not None: 734 skip = sslSkip 735 736 def testTLS(self): 737 clientCTX = ClientTLSContext() 738 serverCTX = ServerTLSContext() 739 740 client = NoticeTLSClient(contextFactory=clientCTX) 741 server = DummyESMTP(contextFactory=serverCTX) 742 743 def check(ignored): 744 self.assertEqual(client.tls, True) 745 self.assertEqual(server.startedTLS, True) 746 747 return self.loopback(server, client).addCallback(check) 748 749if not interfaces.IReactorSSL.providedBy(reactor): 750 for case in (TLSTestCase,): 751 case.skip = "Reactor doesn't support SSL" 752 753 754 755class EmptyLineTestCase(unittest.TestCase): 756 def test_emptyLineSyntaxError(self): 757 """ 758 If L{smtp.SMTP} receives an empty line, it responds with a 500 error 759 response code and a message about a syntax error. 760 """ 761 proto = smtp.SMTP() 762 transport = StringTransport() 763 proto.makeConnection(transport) 764 proto.lineReceived('') 765 proto.setTimeout(None) 766 767 out = transport.value().splitlines() 768 self.assertEqual(len(out), 2) 769 self.failUnless(out[0].startswith('220')) 770 self.assertEqual(out[1], "500 Error: bad syntax") 771 772 773 774class TimeoutTestCase(unittest.TestCase, LoopbackMixin): 775 """ 776 Check that SMTP client factories correctly use the timeout. 777 """ 778 779 def _timeoutTest(self, onDone, clientFactory): 780 """ 781 Connect the clientFactory, and check the timeout on the request. 782 """ 783 clock = task.Clock() 784 client = clientFactory.buildProtocol( 785 address.IPv4Address('TCP', 'example.net', 25)) 786 client.callLater = clock.callLater 787 t = StringTransport() 788 client.makeConnection(t) 789 t.protocol = client 790 def check(ign): 791 self.assertEqual(clock.seconds(), 0.5) 792 d = self.assertFailure(onDone, smtp.SMTPTimeoutError 793 ).addCallback(check) 794 # The first call should not trigger the timeout 795 clock.advance(0.1) 796 # But this one should 797 clock.advance(0.4) 798 return d 799 800 801 def test_SMTPClient(self): 802 """ 803 Test timeout for L{smtp.SMTPSenderFactory}: the response L{Deferred} 804 should be errback with a L{smtp.SMTPTimeoutError}. 805 """ 806 onDone = defer.Deferred() 807 clientFactory = smtp.SMTPSenderFactory( 808 'source@address', 'recipient@address', 809 StringIO("Message body"), onDone, 810 retries=0, timeout=0.5) 811 return self._timeoutTest(onDone, clientFactory) 812 813 814 def test_ESMTPClient(self): 815 """ 816 Test timeout for L{smtp.ESMTPSenderFactory}: the response L{Deferred} 817 should be errback with a L{smtp.SMTPTimeoutError}. 818 """ 819 onDone = defer.Deferred() 820 clientFactory = smtp.ESMTPSenderFactory( 821 'username', 'password', 822 'source@address', 'recipient@address', 823 StringIO("Message body"), onDone, 824 retries=0, timeout=0.5) 825 return self._timeoutTest(onDone, clientFactory) 826 827 828 def test_resetTimeoutWhileSending(self): 829 """ 830 The timeout is not allowed to expire after the server has accepted a 831 DATA command and the client is actively sending data to it. 832 """ 833 class SlowFile: 834 """ 835 A file-like which returns one byte from each read call until the 836 specified number of bytes have been returned. 837 """ 838 def __init__(self, size): 839 self._size = size 840 841 def read(self, max=None): 842 if self._size: 843 self._size -= 1 844 return 'x' 845 return '' 846 847 failed = [] 848 onDone = defer.Deferred() 849 onDone.addErrback(failed.append) 850 clientFactory = smtp.SMTPSenderFactory( 851 'source@address', 'recipient@address', 852 SlowFile(1), onDone, retries=0, timeout=3) 853 clientFactory.domain = "example.org" 854 clock = task.Clock() 855 client = clientFactory.buildProtocol( 856 address.IPv4Address('TCP', 'example.net', 25)) 857 client.callLater = clock.callLater 858 transport = StringTransport() 859 client.makeConnection(transport) 860 861 client.dataReceived( 862 "220 Ok\r\n" # Greet the client 863 "250 Ok\r\n" # Respond to HELO 864 "250 Ok\r\n" # Respond to MAIL FROM 865 "250 Ok\r\n" # Respond to RCPT TO 866 "354 Ok\r\n" # Respond to DATA 867 ) 868 869 # Now the client is producing data to the server. Any time 870 # resumeProducing is called on the producer, the timeout should be 871 # extended. First, a sanity check. This test is only written to 872 # handle pull producers. 873 self.assertNotIdentical(transport.producer, None) 874 self.assertFalse(transport.streaming) 875 876 # Now, allow 2 seconds (1 less than the timeout of 3 seconds) to 877 # elapse. 878 clock.advance(2) 879 880 # The timeout has not expired, so the failure should not have happened. 881 self.assertEqual(failed, []) 882 883 # Let some bytes be produced, extending the timeout. Then advance the 884 # clock some more and verify that the timeout still hasn't happened. 885 transport.producer.resumeProducing() 886 clock.advance(2) 887 self.assertEqual(failed, []) 888 889 # The file has been completely produced - the next resume producing 890 # finishes the upload, successfully. 891 transport.producer.resumeProducing() 892 client.dataReceived("250 Ok\r\n") 893 self.assertEqual(failed, []) 894 895 # Verify that the client actually did send the things expected. 896 self.assertEqual( 897 transport.value(), 898 "HELO example.org\r\n" 899 "MAIL FROM:<source@address>\r\n" 900 "RCPT TO:<recipient@address>\r\n" 901 "DATA\r\n" 902 "x\r\n" 903 ".\r\n" 904 # This RSET is just an implementation detail. It's nice, but this 905 # test doesn't really care about it. 906 "RSET\r\n") 907 908 909 910class MultipleDeliveryFactorySMTPServerFactory(protocol.ServerFactory): 911 """ 912 L{MultipleDeliveryFactorySMTPServerFactory} creates SMTP server protocol 913 instances with message delivery factory objects supplied to it. Each 914 factory is used for one connection and then discarded. Factories are used 915 in the order they are supplied. 916 """ 917 def __init__(self, messageFactories): 918 self._messageFactories = messageFactories 919 920 921 def buildProtocol(self, addr): 922 p = protocol.ServerFactory.buildProtocol(self, addr) 923 p.delivery = SimpleDelivery(self._messageFactories.pop(0)) 924 return p 925 926 927 928class SMTPSenderFactoryTestCase(unittest.TestCase): 929 """ 930 Tests for L{smtp.SMTPSenderFactory}. 931 """ 932 def test_removeCurrentProtocolWhenClientConnectionLost(self): 933 """ 934 L{smtp.SMTPSenderFactory} removes the current protocol when the client 935 connection is lost. 936 """ 937 reactor = MemoryReactor() 938 sentDeferred = defer.Deferred() 939 clientFactory = smtp.SMTPSenderFactory( 940 "source@address", "recipient@address", 941 StringIO("message"), sentDeferred) 942 connector = reactor.connectTCP("localhost", 25, clientFactory) 943 clientFactory.buildProtocol(None) 944 clientFactory.clientConnectionLost(connector, 945 error.ConnectionDone("Bye.")) 946 self.assertEqual(clientFactory.currentProtocol, None) 947 948 949 def test_removeCurrentProtocolWhenClientConnectionFailed(self): 950 """ 951 L{smtp.SMTPSenderFactory} removes the current protocol when the client 952 connection is failed. 953 """ 954 reactor = MemoryReactor() 955 sentDeferred = defer.Deferred() 956 clientFactory = smtp.SMTPSenderFactory( 957 "source@address", "recipient@address", 958 StringIO("message"), sentDeferred) 959 connector = reactor.connectTCP("localhost", 25, clientFactory) 960 clientFactory.buildProtocol(None) 961 clientFactory.clientConnectionFailed(connector, 962 error.ConnectionDone("Bye.")) 963 self.assertEqual(clientFactory.currentProtocol, None) 964 965 966 967class SMTPSenderFactoryRetryTestCase(unittest.TestCase): 968 """ 969 Tests for the retry behavior of L{smtp.SMTPSenderFactory}. 970 """ 971 def test_retryAfterDisconnect(self): 972 """ 973 If the protocol created by L{SMTPSenderFactory} loses its connection 974 before receiving confirmation of message delivery, it reconnects and 975 tries to deliver the message again. 976 """ 977 recipient = 'alice' 978 message = "some message text" 979 domain = DummyDomain([recipient]) 980 981 class CleanSMTP(smtp.SMTP): 982 """ 983 An SMTP subclass which ensures that its transport will be 984 disconnected before the test ends. 985 """ 986 def makeConnection(innerSelf, transport): 987 self.addCleanup(transport.loseConnection) 988 smtp.SMTP.makeConnection(innerSelf, transport) 989 990 # Create a server which will fail the first message deliver attempt to 991 # it with a 500 and a disconnect, but which will accept a message 992 # delivered over the 2nd connection to it. 993 serverFactory = MultipleDeliveryFactorySMTPServerFactory([ 994 BrokenMessage, 995 lambda user: DummyMessage(domain, user)]) 996 serverFactory.protocol = CleanSMTP 997 serverPort = reactor.listenTCP(0, serverFactory, interface='127.0.0.1') 998 serverHost = serverPort.getHost() 999 self.addCleanup(serverPort.stopListening) 1000 1001 # Set up a client to try to deliver a message to the above created 1002 # server. 1003 sentDeferred = defer.Deferred() 1004 clientFactory = smtp.SMTPSenderFactory( 1005 "bob@example.org", recipient + "@example.com", 1006 StringIO(message), sentDeferred) 1007 clientFactory.domain = "example.org" 1008 clientConnector = reactor.connectTCP( 1009 serverHost.host, serverHost.port, clientFactory) 1010 self.addCleanup(clientConnector.disconnect) 1011 1012 def cbSent(ignored): 1013 """ 1014 Verify that the message was successfully delivered and flush the 1015 error which caused the first attempt to fail. 1016 """ 1017 self.assertEqual( 1018 domain.messages, 1019 {recipient: ["\n%s\n" % (message,)]}) 1020 # Flush the RuntimeError that BrokenMessage caused to be logged. 1021 self.assertEqual(len(self.flushLoggedErrors(RuntimeError)), 1) 1022 sentDeferred.addCallback(cbSent) 1023 return sentDeferred 1024 1025 1026 1027class SingletonRealm(object): 1028 """ 1029 Trivial realm implementation which is constructed with an interface and an 1030 avatar and returns that avatar when asked for that interface. 1031 """ 1032 implements(IRealm) 1033 1034 def __init__(self, interface, avatar): 1035 self.interface = interface 1036 self.avatar = avatar 1037 1038 1039 def requestAvatar(self, avatarId, mind, *interfaces): 1040 for iface in interfaces: 1041 if iface is self.interface: 1042 return iface, self.avatar, lambda: None 1043 1044 1045 1046class NotImplementedDelivery(object): 1047 """ 1048 Non-implementation of L{smtp.IMessageDelivery} which only has methods which 1049 raise L{NotImplementedError}. Subclassed by various tests to provide the 1050 particular behavior being tested. 1051 """ 1052 def validateFrom(self, helo, origin): 1053 raise NotImplementedError("This oughtn't be called in the course of this test.") 1054 1055 1056 def validateTo(self, user): 1057 raise NotImplementedError("This oughtn't be called in the course of this test.") 1058 1059 1060 def receivedHeader(self, helo, origin, recipients): 1061 raise NotImplementedError("This oughtn't be called in the course of this test.") 1062 1063 1064 1065class SMTPServerTestCase(unittest.TestCase): 1066 """ 1067 Test various behaviors of L{twisted.mail.smtp.SMTP} and 1068 L{twisted.mail.smtp.ESMTP}. 1069 """ 1070 def testSMTPGreetingHost(self, serverClass=smtp.SMTP): 1071 """ 1072 Test that the specified hostname shows up in the SMTP server's 1073 greeting. 1074 """ 1075 s = serverClass() 1076 s.host = "example.com" 1077 t = StringTransport() 1078 s.makeConnection(t) 1079 s.connectionLost(error.ConnectionDone()) 1080 self.assertIn("example.com", t.value()) 1081 1082 1083 def testSMTPGreetingNotExtended(self): 1084 """ 1085 Test that the string "ESMTP" does not appear in the SMTP server's 1086 greeting since that string strongly suggests the presence of support 1087 for various SMTP extensions which are not supported by L{smtp.SMTP}. 1088 """ 1089 s = smtp.SMTP() 1090 t = StringTransport() 1091 s.makeConnection(t) 1092 s.connectionLost(error.ConnectionDone()) 1093 self.assertNotIn("ESMTP", t.value()) 1094 1095 1096 def testESMTPGreetingHost(self): 1097 """ 1098 Similar to testSMTPGreetingHost, but for the L{smtp.ESMTP} class. 1099 """ 1100 self.testSMTPGreetingHost(smtp.ESMTP) 1101 1102 1103 def testESMTPGreetingExtended(self): 1104 """ 1105 Test that the string "ESMTP" does appear in the ESMTP server's 1106 greeting since L{smtp.ESMTP} does support the SMTP extensions which 1107 that advertises to the client. 1108 """ 1109 s = smtp.ESMTP() 1110 t = StringTransport() 1111 s.makeConnection(t) 1112 s.connectionLost(error.ConnectionDone()) 1113 self.assertIn("ESMTP", t.value()) 1114 1115 1116 def test_acceptSenderAddress(self): 1117 """ 1118 Test that a C{MAIL FROM} command with an acceptable address is 1119 responded to with the correct success code. 1120 """ 1121 class AcceptanceDelivery(NotImplementedDelivery): 1122 """ 1123 Delivery object which accepts all senders as valid. 1124 """ 1125 def validateFrom(self, helo, origin): 1126 return origin 1127 1128 realm = SingletonRealm(smtp.IMessageDelivery, AcceptanceDelivery()) 1129 portal = Portal(realm, [AllowAnonymousAccess()]) 1130 proto = smtp.SMTP() 1131 proto.portal = portal 1132 trans = StringTransport() 1133 proto.makeConnection(trans) 1134 1135 # Deal with the necessary preliminaries 1136 proto.dataReceived('HELO example.com\r\n') 1137 trans.clear() 1138 1139 # Try to specify our sender address 1140 proto.dataReceived('MAIL FROM:<alice@example.com>\r\n') 1141 1142 # Clean up the protocol before doing anything that might raise an 1143 # exception. 1144 proto.connectionLost(error.ConnectionLost()) 1145 1146 # Make sure that we received exactly the correct response 1147 self.assertEqual( 1148 trans.value(), 1149 '250 Sender address accepted\r\n') 1150 1151 1152 def test_deliveryRejectedSenderAddress(self): 1153 """ 1154 Test that a C{MAIL FROM} command with an address rejected by a 1155 L{smtp.IMessageDelivery} instance is responded to with the correct 1156 error code. 1157 """ 1158 class RejectionDelivery(NotImplementedDelivery): 1159 """ 1160 Delivery object which rejects all senders as invalid. 1161 """ 1162 def validateFrom(self, helo, origin): 1163 raise smtp.SMTPBadSender(origin) 1164 1165 realm = SingletonRealm(smtp.IMessageDelivery, RejectionDelivery()) 1166 portal = Portal(realm, [AllowAnonymousAccess()]) 1167 proto = smtp.SMTP() 1168 proto.portal = portal 1169 trans = StringTransport() 1170 proto.makeConnection(trans) 1171 1172 # Deal with the necessary preliminaries 1173 proto.dataReceived('HELO example.com\r\n') 1174 trans.clear() 1175 1176 # Try to specify our sender address 1177 proto.dataReceived('MAIL FROM:<alice@example.com>\r\n') 1178 1179 # Clean up the protocol before doing anything that might raise an 1180 # exception. 1181 proto.connectionLost(error.ConnectionLost()) 1182 1183 # Make sure that we received exactly the correct response 1184 self.assertEqual( 1185 trans.value(), 1186 '550 Cannot receive from specified address ' 1187 '<alice@example.com>: Sender not acceptable\r\n') 1188 1189 1190 def test_portalRejectedSenderAddress(self): 1191 """ 1192 Test that a C{MAIL FROM} command with an address rejected by an 1193 L{smtp.SMTP} instance's portal is responded to with the correct error 1194 code. 1195 """ 1196 class DisallowAnonymousAccess(object): 1197 """ 1198 Checker for L{IAnonymous} which rejects authentication attempts. 1199 """ 1200 implements(ICredentialsChecker) 1201 1202 credentialInterfaces = (IAnonymous,) 1203 1204 def requestAvatarId(self, credentials): 1205 return defer.fail(UnauthorizedLogin()) 1206 1207 realm = SingletonRealm(smtp.IMessageDelivery, NotImplementedDelivery()) 1208 portal = Portal(realm, [DisallowAnonymousAccess()]) 1209 proto = smtp.SMTP() 1210 proto.portal = portal 1211 trans = StringTransport() 1212 proto.makeConnection(trans) 1213 1214 # Deal with the necessary preliminaries 1215 proto.dataReceived('HELO example.com\r\n') 1216 trans.clear() 1217 1218 # Try to specify our sender address 1219 proto.dataReceived('MAIL FROM:<alice@example.com>\r\n') 1220 1221 # Clean up the protocol before doing anything that might raise an 1222 # exception. 1223 proto.connectionLost(error.ConnectionLost()) 1224 1225 # Make sure that we received exactly the correct response 1226 self.assertEqual( 1227 trans.value(), 1228 '550 Cannot receive from specified address ' 1229 '<alice@example.com>: Sender not acceptable\r\n') 1230 1231 1232 def test_portalRejectedAnonymousSender(self): 1233 """ 1234 Test that a C{MAIL FROM} command issued without first authenticating 1235 when a portal has been configured to disallow anonymous logins is 1236 responded to with the correct error code. 1237 """ 1238 realm = SingletonRealm(smtp.IMessageDelivery, NotImplementedDelivery()) 1239 portal = Portal(realm, []) 1240 proto = smtp.SMTP() 1241 proto.portal = portal 1242 trans = StringTransport() 1243 proto.makeConnection(trans) 1244 1245 # Deal with the necessary preliminaries 1246 proto.dataReceived('HELO example.com\r\n') 1247 trans.clear() 1248 1249 # Try to specify our sender address 1250 proto.dataReceived('MAIL FROM:<alice@example.com>\r\n') 1251 1252 # Clean up the protocol before doing anything that might raise an 1253 # exception. 1254 proto.connectionLost(error.ConnectionLost()) 1255 1256 # Make sure that we received exactly the correct response 1257 self.assertEqual( 1258 trans.value(), 1259 '550 Cannot receive from specified address ' 1260 '<alice@example.com>: Unauthenticated senders not allowed\r\n') 1261 1262 1263 1264class ESMTPAuthenticationTestCase(unittest.TestCase): 1265 def assertServerResponse(self, bytes, response): 1266 """ 1267 Assert that when the given bytes are delivered to the ESMTP server 1268 instance, it responds with the indicated lines. 1269 1270 @type bytes: str 1271 @type response: list of str 1272 """ 1273 self.transport.clear() 1274 self.server.dataReceived(bytes) 1275 self.assertEqual( 1276 response, 1277 self.transport.value().splitlines()) 1278 1279 1280 def assertServerAuthenticated(self, loginArgs, username="username", password="password"): 1281 """ 1282 Assert that a login attempt has been made, that the credentials and 1283 interfaces passed to it are correct, and that when the login request 1284 is satisfied, a successful response is sent by the ESMTP server 1285 instance. 1286 1287 @param loginArgs: A C{list} previously passed to L{portalFactory}. 1288 """ 1289 d, credentials, mind, interfaces = loginArgs.pop() 1290 self.assertEqual(loginArgs, []) 1291 self.failUnless(twisted.cred.credentials.IUsernamePassword.providedBy(credentials)) 1292 self.assertEqual(credentials.username, username) 1293 self.failUnless(credentials.checkPassword(password)) 1294 self.assertIn(smtp.IMessageDeliveryFactory, interfaces) 1295 self.assertIn(smtp.IMessageDelivery, interfaces) 1296 d.callback((smtp.IMessageDeliveryFactory, None, lambda: None)) 1297 1298 self.assertEqual( 1299 ["235 Authentication successful."], 1300 self.transport.value().splitlines()) 1301 1302 1303 def setUp(self): 1304 """ 1305 Create an ESMTP instance attached to a StringTransport. 1306 """ 1307 self.server = smtp.ESMTP({ 1308 'LOGIN': imap4.LOGINCredentials}) 1309 self.server.host = 'localhost' 1310 self.transport = StringTransport( 1311 peerAddress=address.IPv4Address('TCP', '127.0.0.1', 12345)) 1312 self.server.makeConnection(self.transport) 1313 1314 1315 def tearDown(self): 1316 """ 1317 Disconnect the ESMTP instance to clean up its timeout DelayedCall. 1318 """ 1319 self.server.connectionLost(error.ConnectionDone()) 1320 1321 1322 def portalFactory(self, loginList): 1323 class DummyPortal: 1324 def login(self, credentials, mind, *interfaces): 1325 d = defer.Deferred() 1326 loginList.append((d, credentials, mind, interfaces)) 1327 return d 1328 return DummyPortal() 1329 1330 1331 def test_authenticationCapabilityAdvertised(self): 1332 """ 1333 Test that AUTH is advertised to clients which issue an EHLO command. 1334 """ 1335 self.transport.clear() 1336 self.server.dataReceived('EHLO\r\n') 1337 responseLines = self.transport.value().splitlines() 1338 self.assertEqual( 1339 responseLines[0], 1340 "250-localhost Hello 127.0.0.1, nice to meet you") 1341 self.assertEqual( 1342 responseLines[1], 1343 "250 AUTH LOGIN") 1344 self.assertEqual(len(responseLines), 2) 1345 1346 1347 def test_plainAuthentication(self): 1348 """ 1349 Test that the LOGIN authentication mechanism can be used 1350 """ 1351 loginArgs = [] 1352 self.server.portal = self.portalFactory(loginArgs) 1353 1354 self.server.dataReceived('EHLO\r\n') 1355 self.transport.clear() 1356 1357 self.assertServerResponse( 1358 'AUTH LOGIN\r\n', 1359 ["334 " + "User Name\0".encode('base64').strip()]) 1360 1361 self.assertServerResponse( 1362 'username'.encode('base64') + '\r\n', 1363 ["334 " + "Password\0".encode('base64').strip()]) 1364 1365 self.assertServerResponse( 1366 'password'.encode('base64').strip() + '\r\n', 1367 []) 1368 1369 self.assertServerAuthenticated(loginArgs) 1370 1371 1372 def test_plainAuthenticationEmptyPassword(self): 1373 """ 1374 Test that giving an empty password for plain auth succeeds. 1375 """ 1376 loginArgs = [] 1377 self.server.portal = self.portalFactory(loginArgs) 1378 1379 self.server.dataReceived('EHLO\r\n') 1380 self.transport.clear() 1381 1382 self.assertServerResponse( 1383 'AUTH LOGIN\r\n', 1384 ["334 " + "User Name\0".encode('base64').strip()]) 1385 1386 self.assertServerResponse( 1387 'username'.encode('base64') + '\r\n', 1388 ["334 " + "Password\0".encode('base64').strip()]) 1389 1390 self.assertServerResponse('\r\n', []) 1391 self.assertServerAuthenticated(loginArgs, password='') 1392 1393 1394 def test_plainAuthenticationInitialResponse(self): 1395 """ 1396 The response to the first challenge may be included on the AUTH command 1397 line. Test that this is also supported. 1398 """ 1399 loginArgs = [] 1400 self.server.portal = self.portalFactory(loginArgs) 1401 1402 self.server.dataReceived('EHLO\r\n') 1403 self.transport.clear() 1404 1405 self.assertServerResponse( 1406 'AUTH LOGIN ' + "username".encode('base64').strip() + '\r\n', 1407 ["334 " + "Password\0".encode('base64').strip()]) 1408 1409 self.assertServerResponse( 1410 'password'.encode('base64').strip() + '\r\n', 1411 []) 1412 1413 self.assertServerAuthenticated(loginArgs) 1414 1415 1416 def test_abortAuthentication(self): 1417 """ 1418 Test that a challenge/response sequence can be aborted by the client. 1419 """ 1420 loginArgs = [] 1421 self.server.portal = self.portalFactory(loginArgs) 1422 1423 self.server.dataReceived('EHLO\r\n') 1424 self.server.dataReceived('AUTH LOGIN\r\n') 1425 1426 self.assertServerResponse( 1427 '*\r\n', 1428 ['501 Authentication aborted']) 1429 1430 1431 def test_invalidBase64EncodedResponse(self): 1432 """ 1433 Test that a response which is not properly Base64 encoded results in 1434 the appropriate error code. 1435 """ 1436 loginArgs = [] 1437 self.server.portal = self.portalFactory(loginArgs) 1438 1439 self.server.dataReceived('EHLO\r\n') 1440 self.server.dataReceived('AUTH LOGIN\r\n') 1441 1442 self.assertServerResponse( 1443 'x\r\n', 1444 ['501 Syntax error in parameters or arguments']) 1445 1446 self.assertEqual(loginArgs, []) 1447 1448 1449 def test_invalidBase64EncodedInitialResponse(self): 1450 """ 1451 Like L{test_invalidBase64EncodedResponse} but for the case of an 1452 initial response included with the C{AUTH} command. 1453 """ 1454 loginArgs = [] 1455 self.server.portal = self.portalFactory(loginArgs) 1456 1457 self.server.dataReceived('EHLO\r\n') 1458 self.assertServerResponse( 1459 'AUTH LOGIN x\r\n', 1460 ['501 Syntax error in parameters or arguments']) 1461 1462 self.assertEqual(loginArgs, []) 1463 1464 1465 def test_unexpectedLoginFailure(self): 1466 """ 1467 If the L{Deferred} returned by L{Portal.login} fires with an 1468 exception of any type other than L{UnauthorizedLogin}, the exception 1469 is logged and the client is informed that the authentication attempt 1470 has failed. 1471 """ 1472 loginArgs = [] 1473 self.server.portal = self.portalFactory(loginArgs) 1474 1475 self.server.dataReceived('EHLO\r\n') 1476 self.transport.clear() 1477 1478 self.assertServerResponse( 1479 'AUTH LOGIN ' + 'username'.encode('base64').strip() + '\r\n', 1480 ['334 ' + 'Password\0'.encode('base64').strip()]) 1481 self.assertServerResponse( 1482 'password'.encode('base64').strip() + '\r\n', 1483 []) 1484 1485 d, credentials, mind, interfaces = loginArgs.pop() 1486 d.errback(RuntimeError("Something wrong with the server")) 1487 1488 self.assertEqual( 1489 '451 Requested action aborted: local error in processing\r\n', 1490 self.transport.value()) 1491 1492 self.assertEqual(len(self.flushLoggedErrors(RuntimeError)), 1) 1493 1494 1495 1496class SMTPClientErrorTestCase(unittest.TestCase): 1497 """ 1498 Tests for L{smtp.SMTPClientError}. 1499 """ 1500 def test_str(self): 1501 """ 1502 The string representation of a L{SMTPClientError} instance includes 1503 the response code and response string. 1504 """ 1505 err = smtp.SMTPClientError(123, "some text") 1506 self.assertEqual(str(err), "123 some text") 1507 1508 1509 def test_strWithNegativeCode(self): 1510 """ 1511 If the response code supplied to L{SMTPClientError} is negative, it 1512 is excluded from the string representation. 1513 """ 1514 err = smtp.SMTPClientError(-1, "foo bar") 1515 self.assertEqual(str(err), "foo bar") 1516 1517 1518 def test_strWithLog(self): 1519 """ 1520 If a line log is supplied to L{SMTPClientError}, its contents are 1521 included in the string representation of the exception instance. 1522 """ 1523 log = LineLog(10) 1524 log.append("testlog") 1525 log.append("secondline") 1526 err = smtp.SMTPClientError(100, "test error", log=log.str()) 1527 self.assertEqual( 1528 str(err), 1529 "100 test error\n" 1530 "testlog\n" 1531 "secondline\n") 1532 1533 1534 1535class SenderMixinSentMailTests(unittest.TestCase): 1536 """ 1537 Tests for L{smtp.SenderMixin.sentMail}, used in particular by 1538 L{smtp.SMTPSenderFactory} and L{smtp.ESMTPSenderFactory}. 1539 """ 1540 def test_onlyLogFailedAddresses(self): 1541 """ 1542 L{smtp.SenderMixin.sentMail} adds only the addresses with failing 1543 SMTP response codes to the log passed to the factory's errback. 1544 """ 1545 onDone = self.assertFailure(defer.Deferred(), smtp.SMTPDeliveryError) 1546 onDone.addCallback(lambda e: self.assertEqual( 1547 e.log, "bob@example.com: 199 Error in sending.\n")) 1548 1549 clientFactory = smtp.SMTPSenderFactory( 1550 'source@address', 'recipient@address', 1551 StringIO("Message body"), onDone, 1552 retries=0, timeout=0.5) 1553 1554 client = clientFactory.buildProtocol( 1555 address.IPv4Address('TCP', 'example.net', 25)) 1556 1557 addresses = [("alice@example.com", 200, "No errors here!"), 1558 ("bob@example.com", 199, "Error in sending.")] 1559 client.sentMail(199, "Test response", 1, addresses, client.log) 1560 1561 return onDone 1562 1563 1564 1565class SSLTestCase(unittest.TestCase): 1566 """ 1567 Tests for the TLS negotiation done by L{smtp.ESMTPClient}. 1568 """ 1569 if sslSkip is not None: 1570 skip = sslSkip 1571 1572 SERVER_GREETING = "220 localhost NO UCE NO UBE NO RELAY PROBES ESMTP\r\n" 1573 EHLO_RESPONSE = "250-localhost Hello 127.0.0.1, nice to meet you\r\n" 1574 1575 def setUp(self): 1576 self.clientProtocol = smtp.ESMTPClient( 1577 "testpassword", ClientTLSContext(), "testuser") 1578 self.clientProtocol.requireTransportSecurity = True 1579 self.clientProtocol.getMailFrom = lambda: "test@example.org" 1580 1581 1582 def _requireTransportSecurityOverSSLTest(self, capabilities): 1583 """ 1584 Verify that when L{smtp.ESMTPClient} connects to a server over a 1585 transport providing L{ISSLTransport}, C{requireTransportSecurity} is 1586 C{True}, and it is presented with the given capabilities, it will try 1587 to send its mail and not first attempt to negotiate TLS using the 1588 I{STARTTLS} protocol action. 1589 1590 @param capabilities: Bytes to include in the test server's capability 1591 response. These must be formatted exactly as required by the 1592 protocol, including a line which ends the capability response. 1593 @type param: L{bytes} 1594 1595 @raise: C{self.failureException} if the behavior of 1596 C{self.clientProtocol} is not as described. 1597 """ 1598 transport = StringTransport() 1599 directlyProvides(transport, interfaces.ISSLTransport) 1600 self.clientProtocol.makeConnection(transport) 1601 1602 # Get the handshake out of the way 1603 self.clientProtocol.dataReceived(self.SERVER_GREETING) 1604 transport.clear() 1605 1606 # Tell the client about the server's capabilities 1607 self.clientProtocol.dataReceived(self.EHLO_RESPONSE + capabilities) 1608 1609 # The client should now try to send a message - without first trying to 1610 # negotiate TLS, since the transport is already secure. 1611 self.assertEqual( 1612 b"MAIL FROM:<test@example.org>\r\n", 1613 transport.value()) 1614 1615 1616 def test_requireTransportSecurityOverSSL(self): 1617 """ 1618 When C{requireTransportSecurity} is C{True} and the client is connected 1619 over an SSL transport, mail may be delivered. 1620 """ 1621 self._requireTransportSecurityOverSSLTest(b"250 AUTH LOGIN\r\n") 1622 1623 1624 def test_requireTransportSecurityTLSOffered(self): 1625 """ 1626 When C{requireTransportSecurity} is C{True} and the client is connected 1627 over a non-SSL transport, if the server offers the I{STARTTLS} 1628 extension, it is used before mail is delivered. 1629 """ 1630 transport = StringTransport() 1631 self.clientProtocol.makeConnection(transport) 1632 1633 # Get the handshake out of the way 1634 self.clientProtocol.dataReceived(self.SERVER_GREETING) 1635 transport.clear() 1636 1637 # Tell the client about the server's capabilities - including STARTTLS 1638 self.clientProtocol.dataReceived( 1639 self.EHLO_RESPONSE + 1640 "250-AUTH LOGIN\r\n" 1641 "250 STARTTLS\r\n") 1642 1643 # The client should try to start TLS before sending the message. 1644 self.assertEqual("STARTTLS\r\n", transport.value()) 1645 1646 1647 def test_requireTransportSecurityTLSOfferedOverSSL(self): 1648 """ 1649 When C{requireTransportSecurity} is C{True} and the client is connected 1650 over an SSL transport, if the server offers the I{STARTTLS} 1651 extension, it is not used before mail is delivered. 1652 """ 1653 self._requireTransportSecurityOverSSLTest( 1654 b"250-AUTH LOGIN\r\n" 1655 b"250 STARTTLS\r\n") 1656 1657 1658 def test_requireTransportSecurityTLSNotOffered(self): 1659 """ 1660 When C{requireTransportSecurity} is C{True} and the client is connected 1661 over a non-SSL transport, if the server does not offer the I{STARTTLS} 1662 extension, mail is not delivered. 1663 """ 1664 transport = StringTransport() 1665 self.clientProtocol.makeConnection(transport) 1666 1667 # Get the handshake out of the way 1668 self.clientProtocol.dataReceived(self.SERVER_GREETING) 1669 transport.clear() 1670 1671 # Tell the client about the server's capabilities - excluding STARTTLS 1672 self.clientProtocol.dataReceived( 1673 self.EHLO_RESPONSE + 1674 "250 AUTH LOGIN\r\n") 1675 1676 # The client give up 1677 self.assertEqual("QUIT\r\n", transport.value()) 1678 1679 1680 def test_esmtpClientTlsModeDeprecationGet(self): 1681 """ 1682 L{smtp.ESMTPClient.tlsMode} is deprecated. 1683 """ 1684 val = self.clientProtocol.tlsMode 1685 del val 1686 warningsShown = self.flushWarnings( 1687 offendingFunctions=[self.test_esmtpClientTlsModeDeprecationGet]) 1688 self.assertEqual(len(warningsShown), 1) 1689 self.assertIdentical( 1690 warningsShown[0]['category'], DeprecationWarning) 1691 self.assertEqual( 1692 warningsShown[0]['message'], 1693 "tlsMode attribute of twisted.mail.smtp.ESMTPClient " 1694 "is deprecated since Twisted 13.0") 1695 1696 1697 def test_esmtpClientTlsModeDeprecationGetAttributeError(self): 1698 """ 1699 L{smtp.ESMTPClient.__getattr__} raises an attribute error for other 1700 attribute names which do not exist. 1701 """ 1702 self.assertRaises( 1703 AttributeError, lambda: self.clientProtocol.doesNotExist) 1704 1705 1706 def test_esmtpClientTlsModeDeprecationSet(self): 1707 """ 1708 L{smtp.ESMTPClient.tlsMode} is deprecated. 1709 """ 1710 self.clientProtocol.tlsMode = False 1711 warningsShown = self.flushWarnings( 1712 offendingFunctions=[self.test_esmtpClientTlsModeDeprecationSet]) 1713 self.assertEqual(len(warningsShown), 1) 1714 self.assertIdentical( 1715 warningsShown[0]['category'], DeprecationWarning) 1716 self.assertEqual( 1717 warningsShown[0]['message'], 1718 "tlsMode attribute of twisted.mail.smtp.ESMTPClient " 1719 "is deprecated since Twisted 13.0") 1720 1721 1722 1723class AbortableStringTransport(StringTransport): 1724 """ 1725 A version of L{StringTransport} that supports C{abortConnection}. 1726 """ 1727 # This should be replaced by a common version in #6530. 1728 aborting = False 1729 1730 1731 def abortConnection(self): 1732 """ 1733 A testable version of the C{ITCPTransport.abortConnection} method. 1734 1735 Since this is a special case of closing the connection, 1736 C{loseConnection} is also called. 1737 """ 1738 self.aborting = True 1739 self.loseConnection() 1740 1741 1742 1743class SendmailTestCase(unittest.TestCase): 1744 """ 1745 Tests for L{twisted.mail.smtp.sendmail}. 1746 """ 1747 def test_defaultReactorIsGlobalReactor(self): 1748 """ 1749 The default C{reactor} parameter of L{twisted.mail.smtp.sendmail} is 1750 L{twisted.internet.reactor}. 1751 """ 1752 args, varArgs, keywords, defaults = inspect.getargspec(smtp.sendmail) 1753 index = len(args) - args.index("reactor") + 1 1754 self.assertEqual(reactor, defaults[index]) 1755 1756 1757 def test_cancelBeforeConnectionMade(self): 1758 """ 1759 When a user cancels L{twisted.mail.smtp.sendmail} before the connection 1760 is made, the connection is closed by 1761 L{twisted.internet.interfaces.IConnector.disconnect}. 1762 """ 1763 reactor = MemoryReactor() 1764 d = smtp.sendmail("localhost", "source@address", "recipient@address", 1765 "message", reactor=reactor) 1766 d.cancel() 1767 self.assertEqual(reactor.connectors[0]._disconnected, True) 1768 failure = self.failureResultOf(d) 1769 failure.trap(defer.CancelledError) 1770 1771 1772 def test_cancelAfterConnectionMade(self): 1773 """ 1774 When a user cancels L{twisted.mail.smtp.sendmail} after the connection 1775 is made, the connection is closed by 1776 L{twisted.internet.interfaces.ITransport.abortConnection}. 1777 """ 1778 reactor = MemoryReactor() 1779 transport = AbortableStringTransport() 1780 d = smtp.sendmail("localhost", "source@address", "recipient@address", 1781 "message", reactor=reactor) 1782 factory = reactor.tcpClients[0][2] 1783 p = factory.buildProtocol(None) 1784 p.makeConnection(transport) 1785 d.cancel() 1786 self.assertEqual(transport.aborting, True) 1787 self.assertEqual(transport.disconnecting, True) 1788 failure = self.failureResultOf(d) 1789 failure.trap(defer.CancelledError) 1790