1# -*- test-case-name: twisted.mail.test.test_pop3client -*- 2# Copyright (c) 2001-2004 Divmod Inc. 3# See LICENSE for details. 4 5import sys 6import inspect 7 8from zope.interface import directlyProvides 9 10from twisted.mail.pop3 import AdvancedPOP3Client as POP3Client 11from twisted.mail.pop3 import InsecureAuthenticationDisallowed 12from twisted.mail.pop3 import ServerErrorResponse 13from twisted.protocols import loopback 14from twisted.internet import reactor, defer, error, protocol, interfaces 15from twisted.python import log 16 17from twisted.trial import unittest 18from twisted.test.proto_helpers import StringTransport 19from twisted.protocols import basic 20 21from twisted.mail.test import pop3testserver 22 23try: 24 from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext 25except ImportError: 26 ClientTLSContext = ServerTLSContext = None 27 28 29class StringTransportWithConnectionLosing(StringTransport): 30 def loseConnection(self): 31 self.protocol.connectionLost(error.ConnectionDone()) 32 33 34capCache = {"TOP": None, "LOGIN-DELAY": "180", "UIDL": None, \ 35 "STLS": None, "USER": None, "SASL": "LOGIN"} 36def setUp(greet=True): 37 p = POP3Client() 38 39 # Skip the CAPA login will issue if it doesn't already have a 40 # capability cache 41 p._capCache = capCache 42 43 t = StringTransportWithConnectionLosing() 44 t.protocol = p 45 p.makeConnection(t) 46 47 if greet: 48 p.dataReceived('+OK Hello!\r\n') 49 50 return p, t 51 52def strip(f): 53 return lambda result, f=f: f() 54 55class POP3ClientLoginTestCase(unittest.TestCase): 56 def testNegativeGreeting(self): 57 p, t = setUp(greet=False) 58 p.allowInsecureLogin = True 59 d = p.login("username", "password") 60 p.dataReceived('-ERR Offline for maintenance\r\n') 61 return self.assertFailure( 62 d, ServerErrorResponse).addCallback( 63 lambda exc: self.assertEqual(exc.args[0], "Offline for maintenance")) 64 65 66 def testOkUser(self): 67 p, t = setUp() 68 d = p.user("username") 69 self.assertEqual(t.value(), "USER username\r\n") 70 p.dataReceived("+OK send password\r\n") 71 return d.addCallback(self.assertEqual, "send password") 72 73 def testBadUser(self): 74 p, t = setUp() 75 d = p.user("username") 76 self.assertEqual(t.value(), "USER username\r\n") 77 p.dataReceived("-ERR account suspended\r\n") 78 return self.assertFailure( 79 d, ServerErrorResponse).addCallback( 80 lambda exc: self.assertEqual(exc.args[0], "account suspended")) 81 82 def testOkPass(self): 83 p, t = setUp() 84 d = p.password("password") 85 self.assertEqual(t.value(), "PASS password\r\n") 86 p.dataReceived("+OK you're in!\r\n") 87 return d.addCallback(self.assertEqual, "you're in!") 88 89 def testBadPass(self): 90 p, t = setUp() 91 d = p.password("password") 92 self.assertEqual(t.value(), "PASS password\r\n") 93 p.dataReceived("-ERR go away\r\n") 94 return self.assertFailure( 95 d, ServerErrorResponse).addCallback( 96 lambda exc: self.assertEqual(exc.args[0], "go away")) 97 98 def testOkLogin(self): 99 p, t = setUp() 100 p.allowInsecureLogin = True 101 d = p.login("username", "password") 102 self.assertEqual(t.value(), "USER username\r\n") 103 p.dataReceived("+OK go ahead\r\n") 104 self.assertEqual(t.value(), "USER username\r\nPASS password\r\n") 105 p.dataReceived("+OK password accepted\r\n") 106 return d.addCallback(self.assertEqual, "password accepted") 107 108 def testBadPasswordLogin(self): 109 p, t = setUp() 110 p.allowInsecureLogin = True 111 d = p.login("username", "password") 112 self.assertEqual(t.value(), "USER username\r\n") 113 p.dataReceived("+OK waiting on you\r\n") 114 self.assertEqual(t.value(), "USER username\r\nPASS password\r\n") 115 p.dataReceived("-ERR bogus login\r\n") 116 return self.assertFailure( 117 d, ServerErrorResponse).addCallback( 118 lambda exc: self.assertEqual(exc.args[0], "bogus login")) 119 120 def testBadUsernameLogin(self): 121 p, t = setUp() 122 p.allowInsecureLogin = True 123 d = p.login("username", "password") 124 self.assertEqual(t.value(), "USER username\r\n") 125 p.dataReceived("-ERR bogus login\r\n") 126 return self.assertFailure( 127 d, ServerErrorResponse).addCallback( 128 lambda exc: self.assertEqual(exc.args[0], "bogus login")) 129 130 def testServerGreeting(self): 131 p, t = setUp(greet=False) 132 p.dataReceived("+OK lalala this has no challenge\r\n") 133 self.assertEqual(p.serverChallenge, None) 134 135 def testServerGreetingWithChallenge(self): 136 p, t = setUp(greet=False) 137 p.dataReceived("+OK <here is the challenge>\r\n") 138 self.assertEqual(p.serverChallenge, "<here is the challenge>") 139 140 def testAPOP(self): 141 p, t = setUp(greet=False) 142 p.dataReceived("+OK <challenge string goes here>\r\n") 143 d = p.login("username", "password") 144 self.assertEqual(t.value(), "APOP username f34f1e464d0d7927607753129cabe39a\r\n") 145 p.dataReceived("+OK Welcome!\r\n") 146 return d.addCallback(self.assertEqual, "Welcome!") 147 148 def testInsecureLoginRaisesException(self): 149 p, t = setUp(greet=False) 150 p.dataReceived("+OK Howdy\r\n") 151 d = p.login("username", "password") 152 self.failIf(t.value()) 153 return self.assertFailure( 154 d, InsecureAuthenticationDisallowed) 155 156 157 def testSSLTransportConsideredSecure(self): 158 """ 159 If a server doesn't offer APOP but the transport is secured using 160 SSL or TLS, a plaintext login should be allowed, not rejected with 161 an InsecureAuthenticationDisallowed exception. 162 """ 163 p, t = setUp(greet=False) 164 directlyProvides(t, interfaces.ISSLTransport) 165 p.dataReceived("+OK Howdy\r\n") 166 d = p.login("username", "password") 167 self.assertEqual(t.value(), "USER username\r\n") 168 t.clear() 169 p.dataReceived("+OK\r\n") 170 self.assertEqual(t.value(), "PASS password\r\n") 171 p.dataReceived("+OK\r\n") 172 return d 173 174 175 176class ListConsumer: 177 def __init__(self): 178 self.data = {} 179 180 def consume(self, (item, value)): 181 self.data.setdefault(item, []).append(value) 182 183class MessageConsumer: 184 def __init__(self): 185 self.data = [] 186 187 def consume(self, line): 188 self.data.append(line) 189 190class POP3ClientListTestCase(unittest.TestCase): 191 def testListSize(self): 192 p, t = setUp() 193 d = p.listSize() 194 self.assertEqual(t.value(), "LIST\r\n") 195 p.dataReceived("+OK Here it comes\r\n") 196 p.dataReceived("1 3\r\n2 2\r\n3 1\r\n.\r\n") 197 return d.addCallback(self.assertEqual, [3, 2, 1]) 198 199 def testListSizeWithConsumer(self): 200 p, t = setUp() 201 c = ListConsumer() 202 f = c.consume 203 d = p.listSize(f) 204 self.assertEqual(t.value(), "LIST\r\n") 205 p.dataReceived("+OK Here it comes\r\n") 206 p.dataReceived("1 3\r\n2 2\r\n3 1\r\n") 207 self.assertEqual(c.data, {0: [3], 1: [2], 2: [1]}) 208 p.dataReceived("5 3\r\n6 2\r\n7 1\r\n") 209 self.assertEqual(c.data, {0: [3], 1: [2], 2: [1], 4: [3], 5: [2], 6: [1]}) 210 p.dataReceived(".\r\n") 211 return d.addCallback(self.assertIdentical, f) 212 213 def testFailedListSize(self): 214 p, t = setUp() 215 d = p.listSize() 216 self.assertEqual(t.value(), "LIST\r\n") 217 p.dataReceived("-ERR Fatal doom server exploded\r\n") 218 return self.assertFailure( 219 d, ServerErrorResponse).addCallback( 220 lambda exc: self.assertEqual(exc.args[0], "Fatal doom server exploded")) 221 222 def testListUID(self): 223 p, t = setUp() 224 d = p.listUID() 225 self.assertEqual(t.value(), "UIDL\r\n") 226 p.dataReceived("+OK Here it comes\r\n") 227 p.dataReceived("1 abc\r\n2 def\r\n3 ghi\r\n.\r\n") 228 return d.addCallback(self.assertEqual, ["abc", "def", "ghi"]) 229 230 def testListUIDWithConsumer(self): 231 p, t = setUp() 232 c = ListConsumer() 233 f = c.consume 234 d = p.listUID(f) 235 self.assertEqual(t.value(), "UIDL\r\n") 236 p.dataReceived("+OK Here it comes\r\n") 237 p.dataReceived("1 xyz\r\n2 abc\r\n5 mno\r\n") 238 self.assertEqual(c.data, {0: ["xyz"], 1: ["abc"], 4: ["mno"]}) 239 p.dataReceived(".\r\n") 240 return d.addCallback(self.assertIdentical, f) 241 242 def testFailedListUID(self): 243 p, t = setUp() 244 d = p.listUID() 245 self.assertEqual(t.value(), "UIDL\r\n") 246 p.dataReceived("-ERR Fatal doom server exploded\r\n") 247 return self.assertFailure( 248 d, ServerErrorResponse).addCallback( 249 lambda exc: self.assertEqual(exc.args[0], "Fatal doom server exploded")) 250 251class POP3ClientMessageTestCase(unittest.TestCase): 252 def testRetrieve(self): 253 p, t = setUp() 254 d = p.retrieve(7) 255 self.assertEqual(t.value(), "RETR 8\r\n") 256 p.dataReceived("+OK Message incoming\r\n") 257 p.dataReceived("La la la here is message text\r\n") 258 p.dataReceived("..Further message text tra la la\r\n") 259 p.dataReceived(".\r\n") 260 return d.addCallback( 261 self.assertEqual, 262 ["La la la here is message text", 263 ".Further message text tra la la"]) 264 265 def testRetrieveWithConsumer(self): 266 p, t = setUp() 267 c = MessageConsumer() 268 f = c.consume 269 d = p.retrieve(7, f) 270 self.assertEqual(t.value(), "RETR 8\r\n") 271 p.dataReceived("+OK Message incoming\r\n") 272 p.dataReceived("La la la here is message text\r\n") 273 p.dataReceived("..Further message text\r\n.\r\n") 274 return d.addCallback(self._cbTestRetrieveWithConsumer, f, c) 275 276 def _cbTestRetrieveWithConsumer(self, result, f, c): 277 self.assertIdentical(result, f) 278 self.assertEqual(c.data, ["La la la here is message text", 279 ".Further message text"]) 280 281 def testPartialRetrieve(self): 282 p, t = setUp() 283 d = p.retrieve(7, lines=2) 284 self.assertEqual(t.value(), "TOP 8 2\r\n") 285 p.dataReceived("+OK 2 lines on the way\r\n") 286 p.dataReceived("Line the first! Woop\r\n") 287 p.dataReceived("Line the last! Bye\r\n") 288 p.dataReceived(".\r\n") 289 return d.addCallback( 290 self.assertEqual, 291 ["Line the first! Woop", 292 "Line the last! Bye"]) 293 294 def testPartialRetrieveWithConsumer(self): 295 p, t = setUp() 296 c = MessageConsumer() 297 f = c.consume 298 d = p.retrieve(7, f, lines=2) 299 self.assertEqual(t.value(), "TOP 8 2\r\n") 300 p.dataReceived("+OK 2 lines on the way\r\n") 301 p.dataReceived("Line the first! Woop\r\n") 302 p.dataReceived("Line the last! Bye\r\n") 303 p.dataReceived(".\r\n") 304 return d.addCallback(self._cbTestPartialRetrieveWithConsumer, f, c) 305 306 def _cbTestPartialRetrieveWithConsumer(self, result, f, c): 307 self.assertIdentical(result, f) 308 self.assertEqual(c.data, ["Line the first! Woop", 309 "Line the last! Bye"]) 310 311 def testFailedRetrieve(self): 312 p, t = setUp() 313 d = p.retrieve(0) 314 self.assertEqual(t.value(), "RETR 1\r\n") 315 p.dataReceived("-ERR Fatal doom server exploded\r\n") 316 return self.assertFailure( 317 d, ServerErrorResponse).addCallback( 318 lambda exc: self.assertEqual(exc.args[0], "Fatal doom server exploded")) 319 320 321 def test_concurrentRetrieves(self): 322 """ 323 Issue three retrieve calls immediately without waiting for any to 324 succeed and make sure they all do succeed eventually. 325 """ 326 p, t = setUp() 327 messages = [ 328 p.retrieve(i).addCallback( 329 self.assertEqual, 330 ["First line of %d." % (i + 1,), 331 "Second line of %d." % (i + 1,)]) 332 for i 333 in range(3)] 334 335 for i in range(1, 4): 336 self.assertEqual(t.value(), "RETR %d\r\n" % (i,)) 337 t.clear() 338 p.dataReceived("+OK 2 lines on the way\r\n") 339 p.dataReceived("First line of %d.\r\n" % (i,)) 340 p.dataReceived("Second line of %d.\r\n" % (i,)) 341 self.assertEqual(t.value(), "") 342 p.dataReceived(".\r\n") 343 344 return defer.DeferredList(messages, fireOnOneErrback=True) 345 346 347 348class POP3ClientMiscTestCase(unittest.TestCase): 349 def testCapability(self): 350 p, t = setUp() 351 d = p.capabilities(useCache=0) 352 self.assertEqual(t.value(), "CAPA\r\n") 353 p.dataReceived("+OK Capabilities on the way\r\n") 354 p.dataReceived("X\r\nY\r\nZ\r\nA 1 2 3\r\nB 1 2\r\nC 1\r\n.\r\n") 355 return d.addCallback( 356 self.assertEqual, 357 {"X": None, "Y": None, "Z": None, 358 "A": ["1", "2", "3"], 359 "B": ["1", "2"], 360 "C": ["1"]}) 361 362 def testCapabilityError(self): 363 p, t = setUp() 364 d = p.capabilities(useCache=0) 365 self.assertEqual(t.value(), "CAPA\r\n") 366 p.dataReceived("-ERR This server is lame!\r\n") 367 return d.addCallback(self.assertEqual, {}) 368 369 def testStat(self): 370 p, t = setUp() 371 d = p.stat() 372 self.assertEqual(t.value(), "STAT\r\n") 373 p.dataReceived("+OK 1 1212\r\n") 374 return d.addCallback(self.assertEqual, (1, 1212)) 375 376 def testStatError(self): 377 p, t = setUp() 378 d = p.stat() 379 self.assertEqual(t.value(), "STAT\r\n") 380 p.dataReceived("-ERR This server is lame!\r\n") 381 return self.assertFailure( 382 d, ServerErrorResponse).addCallback( 383 lambda exc: self.assertEqual(exc.args[0], "This server is lame!")) 384 385 def testNoop(self): 386 p, t = setUp() 387 d = p.noop() 388 self.assertEqual(t.value(), "NOOP\r\n") 389 p.dataReceived("+OK No-op to you too!\r\n") 390 return d.addCallback(self.assertEqual, "No-op to you too!") 391 392 def testNoopError(self): 393 p, t = setUp() 394 d = p.noop() 395 self.assertEqual(t.value(), "NOOP\r\n") 396 p.dataReceived("-ERR This server is lame!\r\n") 397 return self.assertFailure( 398 d, ServerErrorResponse).addCallback( 399 lambda exc: self.assertEqual(exc.args[0], "This server is lame!")) 400 401 def testRset(self): 402 p, t = setUp() 403 d = p.reset() 404 self.assertEqual(t.value(), "RSET\r\n") 405 p.dataReceived("+OK Reset state\r\n") 406 return d.addCallback(self.assertEqual, "Reset state") 407 408 def testRsetError(self): 409 p, t = setUp() 410 d = p.reset() 411 self.assertEqual(t.value(), "RSET\r\n") 412 p.dataReceived("-ERR This server is lame!\r\n") 413 return self.assertFailure( 414 d, ServerErrorResponse).addCallback( 415 lambda exc: self.assertEqual(exc.args[0], "This server is lame!")) 416 417 def testDelete(self): 418 p, t = setUp() 419 d = p.delete(3) 420 self.assertEqual(t.value(), "DELE 4\r\n") 421 p.dataReceived("+OK Hasta la vista\r\n") 422 return d.addCallback(self.assertEqual, "Hasta la vista") 423 424 def testDeleteError(self): 425 p, t = setUp() 426 d = p.delete(3) 427 self.assertEqual(t.value(), "DELE 4\r\n") 428 p.dataReceived("-ERR Winner is not you.\r\n") 429 return self.assertFailure( 430 d, ServerErrorResponse).addCallback( 431 lambda exc: self.assertEqual(exc.args[0], "Winner is not you.")) 432 433 434class SimpleClient(POP3Client): 435 def __init__(self, deferred, contextFactory = None): 436 self.deferred = deferred 437 self.allowInsecureLogin = True 438 439 def serverGreeting(self, challenge): 440 self.deferred.callback(None) 441 442class POP3HelperMixin: 443 serverCTX = None 444 clientCTX = None 445 446 def setUp(self): 447 d = defer.Deferred() 448 self.server = pop3testserver.POP3TestServer(contextFactory=self.serverCTX) 449 self.client = SimpleClient(d, contextFactory=self.clientCTX) 450 self.client.timeout = 30 451 self.connected = d 452 453 def tearDown(self): 454 del self.server 455 del self.client 456 del self.connected 457 458 def _cbStopClient(self, ignore): 459 self.client.transport.loseConnection() 460 461 def _ebGeneral(self, failure): 462 self.client.transport.loseConnection() 463 self.server.transport.loseConnection() 464 return failure 465 466 def loopback(self): 467 return loopback.loopbackTCP(self.server, self.client, noisy=False) 468 469 470class TLSServerFactory(protocol.ServerFactory): 471 class protocol(basic.LineReceiver): 472 context = None 473 output = [] 474 def connectionMade(self): 475 self.factory.input = [] 476 self.output = self.output[:] 477 map(self.sendLine, self.output.pop(0)) 478 def lineReceived(self, line): 479 self.factory.input.append(line) 480 map(self.sendLine, self.output.pop(0)) 481 if line == 'STLS': 482 self.transport.startTLS(self.context) 483 484 485class POP3TLSTestCase(unittest.TestCase): 486 """ 487 Tests for POP3Client's support for TLS connections. 488 """ 489 490 def test_startTLS(self): 491 """ 492 POP3Client.startTLS starts a TLS session over its existing TCP 493 connection. 494 """ 495 sf = TLSServerFactory() 496 sf.protocol.output = [ 497 ['+OK'], # Server greeting 498 ['+OK', 'STLS', '.'], # CAPA response 499 ['+OK'], # STLS response 500 ['+OK', '.'], # Second CAPA response 501 ['+OK'] # QUIT response 502 ] 503 sf.protocol.context = ServerTLSContext() 504 port = reactor.listenTCP(0, sf, interface='127.0.0.1') 505 self.addCleanup(port.stopListening) 506 H = port.getHost().host 507 P = port.getHost().port 508 509 connLostDeferred = defer.Deferred() 510 cp = SimpleClient(defer.Deferred(), ClientTLSContext()) 511 def connectionLost(reason): 512 SimpleClient.connectionLost(cp, reason) 513 connLostDeferred.callback(None) 514 cp.connectionLost = connectionLost 515 cf = protocol.ClientFactory() 516 cf.protocol = lambda: cp 517 518 conn = reactor.connectTCP(H, P, cf) 519 520 def cbConnected(ignored): 521 log.msg("Connected to server; starting TLS") 522 return cp.startTLS() 523 524 def cbStartedTLS(ignored): 525 log.msg("Started TLS; disconnecting") 526 return cp.quit() 527 528 def cbDisconnected(ign): 529 log.msg("Disconnected; asserting correct input received") 530 self.assertEqual( 531 sf.input, 532 ['CAPA', 'STLS', 'CAPA', 'QUIT']) 533 534 def cleanup(result): 535 log.msg("Asserted correct input; disconnecting client and shutting down server") 536 conn.disconnect() 537 return connLostDeferred 538 539 cp.deferred.addCallback(cbConnected) 540 cp.deferred.addCallback(cbStartedTLS) 541 cp.deferred.addCallback(cbDisconnected) 542 cp.deferred.addBoth(cleanup) 543 544 return cp.deferred 545 546 547class POP3TimeoutTestCase(POP3HelperMixin, unittest.TestCase): 548 def testTimeout(self): 549 def login(): 550 d = self.client.login('test', 'twisted') 551 d.addCallback(loggedIn) 552 d.addErrback(timedOut) 553 return d 554 555 def loggedIn(result): 556 self.fail("Successfully logged in!? Impossible!") 557 558 559 def timedOut(failure): 560 failure.trap(error.TimeoutError) 561 self._cbStopClient(None) 562 563 def quit(): 564 return self.client.quit() 565 566 self.client.timeout = 0.01 567 568 # Tell the server to not return a response to client. This 569 # will trigger a timeout. 570 pop3testserver.TIMEOUT_RESPONSE = True 571 572 methods = [login, quit] 573 map(self.connected.addCallback, map(strip, methods)) 574 self.connected.addCallback(self._cbStopClient) 575 self.connected.addErrback(self._ebGeneral) 576 return self.loopback() 577 578 579if ClientTLSContext is None: 580 for case in (POP3TLSTestCase,): 581 case.skip = "OpenSSL not present" 582elif interfaces.IReactorSSL(reactor, None) is None: 583 for case in (POP3TLSTestCase,): 584 case.skip = "Reactor doesn't support SSL" 585 586 587 588import twisted.mail.pop3client 589 590class POP3ClientMiscTestCase(unittest.TestCase): 591 """ 592 Miscellaneous tests more to do with module/package structure than 593 anything to do with the POP3 client. 594 """ 595 def test_all(self): 596 """ 597 twisted.mail.pop3client.__all__ should be empty because all classes 598 should be imported through twisted.mail.pop3. 599 """ 600 self.assertEqual(twisted.mail.pop3client.__all__, []) 601 602 603 def test_import(self): 604 """ 605 Every public class in twisted.mail.pop3client should be available as a 606 member of twisted.mail.pop3 with the exception of 607 twisted.mail.pop3client.POP3Client which should be available as 608 twisted.mail.pop3.AdvancedClient. 609 """ 610 publicClasses = [c[0] for c in inspect.getmembers( 611 sys.modules['twisted.mail.pop3client'], 612 inspect.isclass) 613 if not c[0][0] == '_'] 614 615 for pc in publicClasses: 616 if not pc == 'POP3Client': 617 self.failUnless(hasattr(twisted.mail.pop3, pc)) 618 else: 619 self.failUnless(hasattr(twisted.mail.pop3, 620 'AdvancedPOP3Client')) 621