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