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