1
# Copyright (c) 2001-2009 Twisted Matrix Laboratories.
2
# See LICENSE for details.
5
Test cases for twisted.mail.smtp module.
8
from zope.interface import implements
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
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
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
29
from twisted.mail import imap4
33
from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext
35
ClientTLSContext = ServerTLSContext = None
40
from cStringIO import StringIO
42
from StringIO import StringIO
45
def spameater(*spam, **eggs):
50
class BrokenMessage(object):
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.
56
implements(smtp.IMessage)
58
def __init__(self, user):
62
def lineReceived(self, line):
66
def eomReceived(self):
67
raise RuntimeError("Some problem, delivery is failing.")
70
def connectionLost(self):
75
class DummyMessage(object):
77
L{BrokenMessage} is an L{IMessage} which saves the message delivered to it
80
@ivar domain: A L{DummyDomain} which will be used to store the message once
83
def __init__(self, domain, user):
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)
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")
104
class DummyDomain(object):
106
L{DummyDomain} is an L{IDomain} which keeps track of messages delivered to
109
def __init__(self, names):
112
self.messages[name] = []
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))
121
def startMessage(self, user):
122
return DummyMessage(self, user)
126
class SMTPTestCase(unittest.TestCase):
128
messages = [('foo@bar.com', ['foo@baz.com', 'qux@baz.com'], '''\
131
Someone set up us the bomb!\015
134
mbox = {'foo': ['Subject: urgent\n\nSomeone set up us the bomb!\n']}
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.
141
self.factory = smtp.SMTPFactory()
142
self.factory.domains = {}
143
self.factory.domains['baz.com'] = DummyDomain(['foo'])
144
self.transport = StringTransport()
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)
167
testMessages.suppress = [util.suppress(message='DomainSMTP', category=DeprecationWarning)]
176
def __init__(self, messageInfo=None):
177
if messageInfo is None:
179
'moshez@foo.bar', ['moshez@foo.bar'], StringIO(mail))
180
self._sender = messageInfo[0]
181
self._recipient = messageInfo[1]
182
self._data = messageInfo[2]
185
def getMailFrom(self):
190
return self._recipient
193
def getMailData(self):
197
def sendError(self, exc):
201
def sentMail(self, code, resp, numOk, addresses, log):
202
# Prevent another mail from being sent.
204
self._recipient = None
209
class MySMTPClient(MyClient, smtp.SMTPClient):
210
def __init__(self, messageInfo=None):
211
smtp.SMTPClient.__init__(self, 'foo.baz')
212
MyClient.__init__(self, messageInfo)
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)
220
def loopback(self, server, client):
221
return loopback.loopbackTCP(server, client)
223
class LoopbackTestCase(LoopbackMixin):
224
def testMessages(self):
225
factory = smtp.SMTPFactory()
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)]
236
class LoopbackSMTPTestCase(LoopbackTestCase, unittest.TestCase):
237
clientClass = MySMTPClient
239
class LoopbackESMTPTestCase(LoopbackTestCase, unittest.TestCase):
240
clientClass = MyESMTPClient
243
class FakeSMTPServer(basic.LineReceiver):
246
'220 hello', '250 nice to meet you',
247
'250 great', '250 great', '354 go on, lad'
250
def connectionMade(self):
252
self.clientData = self.clientData[:]
253
self.clientData.reverse()
254
self.sendLine(self.clientData.pop())
256
def lineReceived(self, line):
257
self.buffer.append(line)
259
self.transport.write("221 see ya around\r\n")
260
self.transport.loseConnection()
262
self.transport.write("250 gotcha\r\n")
264
self.transport.loseConnection()
267
self.sendLine(self.clientData.pop())
270
class SMTPClientTestCase(unittest.TestCase, LoopbackMixin):
272
Tests for L{smtp.SMTPClient}.
275
def test_timeoutConnection(self):
277
L{smtp.SMTPClient.timeoutConnection} calls the C{sendError} hook with a
278
fatal L{SMTPTimeoutError} with the current line log.
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)
290
"Timeout waiting for SMTP server response\n"
292
">>> HELO foo.baz\n")
296
'HELO foo.baz', 'MAIL FROM:<moshez@foo.bar>',
297
'RCPT TO:<moshez@foo.bar>', 'DATA',
298
'Subject: hello', '', 'Goodbye', '.', 'RSET'
301
def test_messages(self):
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.
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))
315
def test_transferError(self):
317
If there is an error while producing the message body to the
318
connection, the C{sendError} callback is invoked.
320
client = MySMTPClient(
321
('alice@example.com', ['bob@example.com'], StringIO("foo")))
322
transport = StringTransport()
323
client.makeConnection(transport)
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
332
# Sanity check - a pull producer should be registered now.
333
self.assertNotIdentical(transport.producer, None)
334
self.assertFalse(transport.streaming)
336
# Now stop the producer prematurely, meaning the message was not sent.
337
transport.producer.stopProducing()
339
# The sendError hook should have been invoked as a result.
340
self.assertIsInstance(client._error, Exception)
343
def test_sendFatalError(self):
345
If L{smtp.SMTPClient.sendError} is called with an L{SMTPClientError}
346
which is fatal, it disconnects its transport without writing anything
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)
357
def test_sendNonFatalError(self):
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.
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)
371
def test_sendOtherError(self):
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.
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)
386
class DummySMTPMessage:
388
def __init__(self, protocol, users):
389
self.protocol = protocol
393
def lineReceived(self, line):
394
self.buffer.append(line)
396
def eomReceived(self):
397
message = '\n'.join(self.buffer) + '\n'
398
helo, origin = self.users[0].helo[0], str(self.users[0].orig)
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")
408
def connectionMade(self):
409
self.dummyMixinBase.connectionMade(self)
412
def startMessage(self, users):
413
return DummySMTPMessage(self, users)
415
def receivedHeader(*spam):
418
def validateTo(self, user):
419
self.delivery = SimpleDelivery(None)
420
return lambda: self.startMessage([user])
422
def validateFrom(self, helo, origin):
427
class DummySMTP(DummyProto, smtp.SMTP):
428
dummyMixinBase = smtp.SMTP
430
class DummyESMTP(DummyProto, smtp.ESMTP):
431
dummyMixinBase = smtp.ESMTP
433
class AnotherTestCase:
437
messages = [ ('foo.com', 'moshez@foo.com', ['moshez@bar.com'],
438
'moshez@foo.com', ['moshez@bar.com'], '''\
445
('foo.com', 'tttt@rrr.com', ['uuu@ooo', 'yyy@eee'],
446
'tttt@rrr.com', ['uuu@ooo', 'yyy@eee'], '''\
451
('foo.com', '@this,@is,@ignored:foo@bar.com',
452
['@ignore,@this,@too:bar@foo.com'],
453
'foo@bar.com', ['bar@foo.com'], '''\
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),
468
for helo_, from_, to_, realfrom, realto, msg in messages:
469
data.append(('MAIL FROM:<%s>\r\n' % from_, '250.*\r\n',
472
data.append(('RCPT TO:<%s>\r\n' % rcpt, '250.*\r\n',
475
data.append(('DATA\r\n','354.*\r\n',
477
(helo_, realfrom, realto, msg))))
480
def test_buffer(self):
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.
487
transport = StringTransport()
488
a = self.serverClass()
492
a.factory = fooFactory()
493
a.makeConnection(transport)
494
for (send, expect, msg, msgexpect) in self.data:
497
data = transport.value()
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] == '.':
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()
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]
524
class AnotherESMTPTestCase(AnotherTestCase, unittest.TestCase):
525
serverClass = DummyESMTP
526
clientClass = MyESMTPClient
528
class AnotherSMTPTestCase(AnotherTestCase, unittest.TestCase):
529
serverClass = DummySMTP
530
clientClass = MySMTPClient
535
implements(cred.checkers.ICredentialsChecker)
538
'testuser': 'testpassword'
541
credentialInterfaces = (cred.credentials.IUsernamePassword,
542
cred.credentials.IUsernameHashedPassword)
544
def requestAvatarId(self, credentials):
545
return defer.maybeDeferred(
546
credentials.checkPassword, self.users[credentials.username]
547
).addCallback(self._cbCheck, credentials.username)
549
def _cbCheck(self, result, username):
552
raise cred.error.UnauthorizedLogin()
556
class SimpleDelivery(object):
558
L{SimpleDelivery} is a message delivery factory with no interesting
561
implements(smtp.IMessageDelivery)
563
def __init__(self, messageFactory):
564
self._messageFactory = messageFactory
567
def receivedHeader(self, helo, origin, recipients):
571
def validateFrom(self, helo, origin):
575
def validateTo(self, user):
576
return lambda: self._messageFactory(user)
581
def requestAvatar(self, avatarId, mind, *interfaces):
582
return smtp.IMessageDelivery, SimpleDelivery(None), lambda: None
586
class AuthTestCase(unittest.TestCase, LoopbackMixin):
587
def test_crammd5Auth(self):
589
L{ESMTPClient} can authenticate using the I{CRAM-MD5} SASL mechanism.
591
@see: U{http://tools.ietf.org/html/rfc2195}
594
p = cred.portal.Portal(realm)
595
p.registerChecker(DummyChecker())
597
server = DummyESMTP({'CRAM-MD5': cred.credentials.CramMD5Credentials})
599
client = MyESMTPClient('testpassword')
601
cAuth = smtp.CramMD5ClientAuthenticator('testuser')
602
client.registerAuthenticator(cAuth)
604
d = self.loopback(server, client)
605
d.addCallback(lambda x : self.assertEquals(server.authenticated, 1))
609
def test_loginAuth(self):
611
L{ESMTPClient} can authenticate using the I{LOGIN} SASL mechanism.
613
@see: U{http://sepp.oetiker.ch/sasl-2.1.19-ds/draft-murchison-sasl-login-00.txt}
616
p = cred.portal.Portal(realm)
617
p.registerChecker(DummyChecker())
619
server = DummyESMTP({'LOGIN': imap4.LOGINCredentials})
621
client = MyESMTPClient('testpassword')
623
cAuth = smtp.LOGINAuthenticator('testuser')
624
client.registerAuthenticator(cAuth)
626
d = self.loopback(server, client)
627
d.addCallback(lambda x: self.assertTrue(server.authenticated))
631
def test_loginAgainstWeirdServer(self):
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.
639
p = cred.portal.Portal(realm)
640
p.registerChecker(DummyChecker())
642
class WeirdLOGIN(imap4.LOGINCredentials):
644
imap4.LOGINCredentials.__init__(self)
645
self.challenges[1] = 'Username:'
647
server = DummyESMTP({'LOGIN': WeirdLOGIN})
650
client = MyESMTPClient('testpassword')
651
cAuth = smtp.LOGINAuthenticator('testuser')
652
client.registerAuthenticator(cAuth)
654
d = self.loopback(server, client)
655
d.addCallback(lambda x: self.assertTrue(server.authenticated))
660
class SMTPHelperTestCase(unittest.TestCase):
661
def testMessageID(self):
663
for i in range(1000):
664
m = smtp.messageid('testcase')
668
def testQuoteAddr(self):
670
['user@host.name', '<user@host.name>'],
671
['"User Name" <user@host.name>', '<user@host.name>'],
672
[smtp.Address('someguy@someplace'), '<someguy@someplace>'],
674
[smtp.Address(''), '<>'],
678
self.assertEquals(smtp.quoteaddr(c), e)
681
u = smtp.User('user@host', 'helo.host.name', None, None)
682
self.assertEquals(str(u), 'user@host')
684
def testXtextEncoding(self):
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')
692
for (case, expected) in cases:
693
self.assertEqual(smtp.xtext_encode(case), (expected, len(case)))
694
self.assertEquals(case.encode('xtext'), expected)
696
smtp.xtext_decode(expected), (case, len(expected)))
697
self.assertEquals(expected.decode('xtext'), case)
700
def test_encodeWithErrors(self):
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.
706
text = u'Hello world'
708
smtp.xtext_encode(text, 'strict'),
709
(text.encode('xtext'), len(text)))
711
text.encode('xtext', 'strict'),
712
text.encode('xtext'))
715
def test_decodeWithErrors(self):
717
Similar to L{test_encodeWithErrors}, but for C{str.decode}.
719
bytes = 'Hello world'
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.
726
smtp.xtext_decode(bytes, 'strict'),
727
(bytes.decode('xtext'), len(bytes)))
729
bytes.decode('xtext', 'strict'),
730
bytes.decode('xtext'))
734
class NoticeTLSClient(MyESMTPClient):
737
def esmtpState_starttls(self, code, resp):
738
MyESMTPClient.esmtpState_starttls(self, code, resp)
741
class TLSTestCase(unittest.TestCase, LoopbackMixin):
743
clientCTX = ClientTLSContext()
744
serverCTX = ServerTLSContext()
746
client = NoticeTLSClient(contextFactory=clientCTX)
747
server = DummyESMTP(contextFactory=serverCTX)
750
self.assertEquals(client.tls, True)
751
self.assertEquals(server.startedTLS, True)
753
return self.loopback(server, client).addCallback(check)
755
if ClientTLSContext is None:
756
for case in (TLSTestCase,):
757
case.skip = "OpenSSL not present"
759
if not interfaces.IReactorSSL.providedBy(reactor):
760
for case in (TLSTestCase,):
761
case.skip = "Reactor doesn't support SSL"
763
class EmptyLineTestCase(unittest.TestCase):
764
def test_emptyLineSyntaxError(self):
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.
770
transport = StringTransport()
771
proto.makeConnection(transport)
772
proto.lineReceived('')
773
proto.setTimeout(None)
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")
782
class TimeoutTestCase(unittest.TestCase, LoopbackMixin):
784
Check that SMTP client factories correctly use the timeout.
787
def _timeoutTest(self, onDone, clientFactory):
789
Connect the clientFactory, and check the timeout on the request.
792
client = clientFactory.buildProtocol(
793
address.IPv4Address('TCP', 'example.net', 25))
794
client.callLater = clock.callLater
795
t = StringTransport()
796
client.makeConnection(t)
799
self.assertEquals(clock.seconds(), 0.5)
800
d = self.assertFailure(onDone, smtp.SMTPTimeoutError
802
# The first call should not trigger the timeout
804
# But this one should
809
def test_SMTPClient(self):
811
Test timeout for L{smtp.SMTPSenderFactory}: the response L{Deferred}
812
should be errback with a L{smtp.SMTPTimeoutError}.
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)
822
def test_ESMTPClient(self):
824
Test timeout for L{smtp.ESMTPSenderFactory}: the response L{Deferred}
825
should be errback with a L{smtp.SMTPTimeoutError}.
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)
836
def test_resetTimeoutWhileSending(self):
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.
843
A file-like which returns one byte from each read call until the
844
specified number of bytes have been returned.
846
def __init__(self, size):
849
def read(self, max=None):
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"
863
client = clientFactory.buildProtocol(
864
address.IPv4Address('TCP', 'example.net', 25))
865
client.callLater = clock.callLater
866
transport = StringTransport()
867
client.makeConnection(transport)
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
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)
884
# Now, allow 2 seconds (1 less than the timeout of 3 seconds) to
888
# The timeout has not expired, so the failure should not have happened.
889
self.assertEqual(failed, [])
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()
895
self.assertEqual(failed, [])
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, [])
903
# Verify that the client actually did send the things expected.
906
"HELO example.org\r\n"
907
"MAIL FROM:<source@address>\r\n"
908
"RCPT TO:<recipient@address>\r\n"
912
# This RSET is just an implementation detail. It's nice, but this
913
# test doesn't really care about it.
918
class MultipleDeliveryFactorySMTPServerFactory(protocol.ServerFactory):
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.
925
def __init__(self, messageFactories):
926
self._messageFactories = messageFactories
929
def buildProtocol(self, addr):
930
p = protocol.ServerFactory.buildProtocol(self, addr)
931
p.delivery = SimpleDelivery(self._messageFactories.pop(0))
936
class SMTPSenderFactoryRetryTestCase(unittest.TestCase):
938
Tests for the retry behavior of L{smtp.SMTPSenderFactory}.
940
def test_retryAfterDisconnect(self):
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.
947
message = "some message text"
948
domain = DummyDomain([recipient])
950
class CleanSMTP(smtp.SMTP):
952
An SMTP subclass which ensures that its transport will be
953
disconnected before the test ends.
955
def makeConnection(innerSelf, transport):
956
self.addCleanup(transport.loseConnection)
957
smtp.SMTP.makeConnection(innerSelf, transport)
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([
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)
970
# Set up a client to try to deliver a message to the above created
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)
983
Verify that the message was successfully delivered and flush the
984
error which caused the first attempt to fail.
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)
996
class SingletonRealm(object):
998
Trivial realm implementation which is constructed with an interface and an
999
avatar and returns that avatar when asked for that interface.
1003
def __init__(self, interface, avatar):
1004
self.interface = interface
1005
self.avatar = avatar
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
1015
class NotImplementedDelivery(object):
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.
1021
def validateFrom(self, helo, origin):
1022
raise NotImplementedError("This oughtn't be called in the course of this test.")
1025
def validateTo(self, user):
1026
raise NotImplementedError("This oughtn't be called in the course of this test.")
1029
def receivedHeader(self, helo, origin, recipients):
1030
raise NotImplementedError("This oughtn't be called in the course of this test.")
1034
class SMTPServerTestCase(unittest.TestCase):
1036
Test various behaviors of L{twisted.mail.smtp.SMTP} and
1037
L{twisted.mail.smtp.ESMTP}.
1039
def testSMTPGreetingHost(self, serverClass=smtp.SMTP):
1041
Test that the specified hostname shows up in the SMTP server's
1045
s.host = "example.com"
1046
t = StringTransport()
1048
s.connectionLost(error.ConnectionDone())
1049
self.assertIn("example.com", t.value())
1052
def testSMTPGreetingNotExtended(self):
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}.
1059
t = StringTransport()
1061
s.connectionLost(error.ConnectionDone())
1062
self.assertNotIn("ESMTP", t.value())
1065
def testESMTPGreetingHost(self):
1067
Similar to testSMTPGreetingHost, but for the L{smtp.ESMTP} class.
1069
self.testSMTPGreetingHost(smtp.ESMTP)
1072
def testESMTPGreetingExtended(self):
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.
1079
t = StringTransport()
1081
s.connectionLost(error.ConnectionDone())
1082
self.assertIn("ESMTP", t.value())
1085
def test_acceptSenderAddress(self):
1087
Test that a C{MAIL FROM} command with an acceptable address is
1088
responded to with the correct success code.
1090
class AcceptanceDelivery(NotImplementedDelivery):
1092
Delivery object which accepts all senders as valid.
1094
def validateFrom(self, helo, origin):
1097
realm = SingletonRealm(smtp.IMessageDelivery, AcceptanceDelivery())
1098
portal = Portal(realm, [AllowAnonymousAccess()])
1100
proto.portal = portal
1101
trans = StringTransport()
1102
proto.makeConnection(trans)
1104
# Deal with the necessary preliminaries
1105
proto.dataReceived('HELO example.com\r\n')
1108
# Try to specify our sender address
1109
proto.dataReceived('MAIL FROM:<alice@example.com>\r\n')
1111
# Clean up the protocol before doing anything that might raise an
1113
proto.connectionLost(error.ConnectionLost())
1115
# Make sure that we received exactly the correct response
1118
'250 Sender address accepted\r\n')
1121
def test_deliveryRejectedSenderAddress(self):
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
1127
class RejectionDelivery(NotImplementedDelivery):
1129
Delivery object which rejects all senders as invalid.
1131
def validateFrom(self, helo, origin):
1132
raise smtp.SMTPBadSender(origin)
1134
realm = SingletonRealm(smtp.IMessageDelivery, RejectionDelivery())
1135
portal = Portal(realm, [AllowAnonymousAccess()])
1137
proto.portal = portal
1138
trans = StringTransport()
1139
proto.makeConnection(trans)
1141
# Deal with the necessary preliminaries
1142
proto.dataReceived('HELO example.com\r\n')
1145
# Try to specify our sender address
1146
proto.dataReceived('MAIL FROM:<alice@example.com>\r\n')
1148
# Clean up the protocol before doing anything that might raise an
1150
proto.connectionLost(error.ConnectionLost())
1152
# Make sure that we received exactly the correct response
1155
'550 Cannot receive from specified address '
1156
'<alice@example.com>: Sender not acceptable\r\n')
1159
def test_portalRejectedSenderAddress(self):
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
1165
class DisallowAnonymousAccess(object):
1167
Checker for L{IAnonymous} which rejects authentication attempts.
1169
implements(ICredentialsChecker)
1171
credentialInterfaces = (IAnonymous,)
1173
def requestAvatarId(self, credentials):
1174
return defer.fail(UnauthorizedLogin())
1176
realm = SingletonRealm(smtp.IMessageDelivery, NotImplementedDelivery())
1177
portal = Portal(realm, [DisallowAnonymousAccess()])
1179
proto.portal = portal
1180
trans = StringTransport()
1181
proto.makeConnection(trans)
1183
# Deal with the necessary preliminaries
1184
proto.dataReceived('HELO example.com\r\n')
1187
# Try to specify our sender address
1188
proto.dataReceived('MAIL FROM:<alice@example.com>\r\n')
1190
# Clean up the protocol before doing anything that might raise an
1192
proto.connectionLost(error.ConnectionLost())
1194
# Make sure that we received exactly the correct response
1197
'550 Cannot receive from specified address '
1198
'<alice@example.com>: Sender not acceptable\r\n')
1201
def test_portalRejectedAnonymousSender(self):
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.
1207
realm = SingletonRealm(smtp.IMessageDelivery, NotImplementedDelivery())
1208
portal = Portal(realm, [])
1210
proto.portal = portal
1211
trans = StringTransport()
1212
proto.makeConnection(trans)
1214
# Deal with the necessary preliminaries
1215
proto.dataReceived('HELO example.com\r\n')
1218
# Try to specify our sender address
1219
proto.dataReceived('MAIL FROM:<alice@example.com>\r\n')
1221
# Clean up the protocol before doing anything that might raise an
1223
proto.connectionLost(error.ConnectionLost())
1225
# Make sure that we received exactly the correct response
1228
'550 Cannot receive from specified address '
1229
'<alice@example.com>: Unauthenticated senders not allowed\r\n')
1233
class ESMTPAuthenticationTestCase(unittest.TestCase):
1234
def assertServerResponse(self, bytes, response):
1236
Assert that when the given bytes are delivered to the ESMTP server
1237
instance, it responds with the indicated lines.
1240
@type response: list of str
1242
self.transport.clear()
1243
self.server.dataReceived(bytes)
1246
self.transport.value().splitlines())
1249
def assertServerAuthenticated(self, loginArgs, username="username", password="password"):
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
1256
@param loginArgs: A C{list} previously passed to L{portalFactory}.
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))
1268
["235 Authentication successful."],
1269
self.transport.value().splitlines())
1274
Create an ESMTP instance attached to a StringTransport.
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)
1286
Disconnect the ESMTP instance to clean up its timeout DelayedCall.
1288
self.server.connectionLost(error.ConnectionDone())
1291
def portalFactory(self, loginList):
1293
def login(self, credentials, mind, *interfaces):
1294
d = defer.Deferred()
1295
loginList.append((d, credentials, mind, interfaces))
1297
return DummyPortal()
1300
def test_authenticationCapabilityAdvertised(self):
1302
Test that AUTH is advertised to clients which issue an EHLO command.
1304
self.transport.clear()
1305
self.server.dataReceived('EHLO\r\n')
1306
responseLines = self.transport.value().splitlines()
1309
"250-localhost Hello 127.0.0.1, nice to meet you")
1313
self.assertEqual(len(responseLines), 2)
1316
def test_plainAuthentication(self):
1318
Test that the LOGIN authentication mechanism can be used
1321
self.server.portal = self.portalFactory(loginArgs)
1323
self.server.dataReceived('EHLO\r\n')
1324
self.transport.clear()
1326
self.assertServerResponse(
1328
["334 " + "User Name\0".encode('base64').strip()])
1330
self.assertServerResponse(
1331
'username'.encode('base64') + '\r\n',
1332
["334 " + "Password\0".encode('base64').strip()])
1334
self.assertServerResponse(
1335
'password'.encode('base64').strip() + '\r\n',
1338
self.assertServerAuthenticated(loginArgs)
1341
def test_plainAuthenticationEmptyPassword(self):
1343
Test that giving an empty password for plain auth succeeds.
1346
self.server.portal = self.portalFactory(loginArgs)
1348
self.server.dataReceived('EHLO\r\n')
1349
self.transport.clear()
1351
self.assertServerResponse(
1353
["334 " + "User Name\0".encode('base64').strip()])
1355
self.assertServerResponse(
1356
'username'.encode('base64') + '\r\n',
1357
["334 " + "Password\0".encode('base64').strip()])
1359
self.assertServerResponse('\r\n', [])
1360
self.assertServerAuthenticated(loginArgs, password='')
1363
def test_plainAuthenticationInitialResponse(self):
1365
The response to the first challenge may be included on the AUTH command
1366
line. Test that this is also supported.
1369
self.server.portal = self.portalFactory(loginArgs)
1371
self.server.dataReceived('EHLO\r\n')
1372
self.transport.clear()
1374
self.assertServerResponse(
1375
'AUTH LOGIN ' + "username".encode('base64').strip() + '\r\n',
1376
["334 " + "Password\0".encode('base64').strip()])
1378
self.assertServerResponse(
1379
'password'.encode('base64').strip() + '\r\n',
1382
self.assertServerAuthenticated(loginArgs)
1385
def test_abortAuthentication(self):
1387
Test that a challenge/response sequence can be aborted by the client.
1390
self.server.portal = self.portalFactory(loginArgs)
1392
self.server.dataReceived('EHLO\r\n')
1393
self.server.dataReceived('AUTH LOGIN\r\n')
1395
self.assertServerResponse(
1397
['501 Authentication aborted'])
1400
def test_invalidBase64EncodedResponse(self):
1402
Test that a response which is not properly Base64 encoded results in
1403
the appropriate error code.
1406
self.server.portal = self.portalFactory(loginArgs)
1408
self.server.dataReceived('EHLO\r\n')
1409
self.server.dataReceived('AUTH LOGIN\r\n')
1411
self.assertServerResponse(
1413
['501 Syntax error in parameters or arguments'])
1415
self.assertEqual(loginArgs, [])
1418
def test_invalidBase64EncodedInitialResponse(self):
1420
Like L{test_invalidBase64EncodedResponse} but for the case of an
1421
initial response included with the C{AUTH} command.
1424
self.server.portal = self.portalFactory(loginArgs)
1426
self.server.dataReceived('EHLO\r\n')
1427
self.assertServerResponse(
1429
['501 Syntax error in parameters or arguments'])
1431
self.assertEqual(loginArgs, [])
1434
def test_unexpectedLoginFailure(self):
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
1442
self.server.portal = self.portalFactory(loginArgs)
1444
self.server.dataReceived('EHLO\r\n')
1445
self.transport.clear()
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',
1454
d, credentials, mind, interfaces = loginArgs.pop()
1455
d.errback(RuntimeError("Something wrong with the server"))
1458
'451 Requested action aborted: local error in processing\r\n',
1459
self.transport.value())
1461
self.assertEquals(len(self.flushLoggedErrors(RuntimeError)), 1)
1465
class SMTPClientErrorTestCase(unittest.TestCase):
1467
Tests for L{smtp.SMTPClientError}.
1471
The string representation of a L{SMTPClientError} instance includes
1472
the response code and response string.
1474
err = smtp.SMTPClientError(123, "some text")
1475
self.assertEquals(str(err), "123 some text")
1478
def test_strWithNegativeCode(self):
1480
If the response code supplied to L{SMTPClientError} is negative, it
1481
is excluded from the string representation.
1483
err = smtp.SMTPClientError(-1, "foo bar")
1484
self.assertEquals(str(err), "foo bar")
1487
def test_strWithLog(self):
1489
If a line log is supplied to L{SMTPClientError}, its contents are
1490
included in the string representation of the exception instance.
1493
log.append("testlog")
1494
log.append("secondline")
1495
err = smtp.SMTPClientError(100, "test error", log=log.str())
1504
class SenderMixinSentMailTests(unittest.TestCase):
1506
Tests for L{smtp.SenderMixin.sentMail}, used in particular by
1507
L{smtp.SMTPSenderFactory} and L{smtp.ESMTPSenderFactory}.
1509
def test_onlyLogFailedAddresses(self):
1511
L{smtp.SenderMixin.sentMail} adds only the addresses with failing
1512
SMTP response codes to the log passed to the factory's errback.
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"))
1518
clientFactory = smtp.SMTPSenderFactory(
1519
'source@address', 'recipient@address',
1520
StringIO("Message body"), onDone,
1521
retries=0, timeout=0.5)
1523
client = clientFactory.buildProtocol(
1524
address.IPv4Address('TCP', 'example.net', 25))
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)