~justin-fathomdb/nova/justinsb-openstack-api-volumes

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/mail/test/test_smtp.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

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