OLD | NEW |
| (Empty) |
1 # Copyright (c) 2001-2008 Twisted Matrix Laboratories. | |
2 # See LICENSE for details. | |
3 | |
4 """ | |
5 Tests for large portions of L{twisted.mail}. | |
6 """ | |
7 | |
8 import os | |
9 import errno | |
10 import md5 | |
11 import shutil | |
12 import pickle | |
13 import StringIO | |
14 import rfc822 | |
15 import tempfile | |
16 import signal | |
17 | |
18 from zope.interface import Interface, implements | |
19 | |
20 from twisted.trial import unittest | |
21 from twisted.mail import smtp | |
22 from twisted.mail import pop3 | |
23 from twisted.names import dns | |
24 from twisted.internet import protocol | |
25 from twisted.internet import defer | |
26 from twisted.internet.defer import Deferred | |
27 from twisted.internet import reactor | |
28 from twisted.internet import interfaces | |
29 from twisted.internet import task | |
30 from twisted.internet.error import DNSLookupError, CannotListenError | |
31 from twisted.internet.error import ProcessDone, ProcessTerminated | |
32 from twisted.internet import address | |
33 from twisted.python import failure | |
34 from twisted.python.filepath import FilePath | |
35 | |
36 from twisted import mail | |
37 import twisted.mail.mail | |
38 import twisted.mail.maildir | |
39 import twisted.mail.relay | |
40 import twisted.mail.relaymanager | |
41 import twisted.mail.protocols | |
42 import twisted.mail.alias | |
43 | |
44 from twisted.names.error import DNSNameError | |
45 from twisted.names.dns import RRHeader, Record_CNAME, Record_MX | |
46 | |
47 from twisted import cred | |
48 import twisted.cred.credentials | |
49 import twisted.cred.checkers | |
50 import twisted.cred.portal | |
51 | |
52 from twisted.test.proto_helpers import LineSendingProtocol | |
53 | |
54 class DomainWithDefaultsTestCase(unittest.TestCase): | |
55 def testMethods(self): | |
56 d = dict([(x, x + 10) for x in range(10)]) | |
57 d = mail.mail.DomainWithDefaultDict(d, 'Default') | |
58 | |
59 self.assertEquals(len(d), 10) | |
60 self.assertEquals(list(iter(d)), range(10)) | |
61 self.assertEquals(list(d.iterkeys()), list(iter(d))) | |
62 | |
63 items = list(d.iteritems()) | |
64 items.sort() | |
65 self.assertEquals(items, [(x, x + 10) for x in range(10)]) | |
66 | |
67 values = list(d.itervalues()) | |
68 values.sort() | |
69 self.assertEquals(values, range(10, 20)) | |
70 | |
71 items = d.items() | |
72 items.sort() | |
73 self.assertEquals(items, [(x, x + 10) for x in range(10)]) | |
74 | |
75 values = d.values() | |
76 values.sort() | |
77 self.assertEquals(values, range(10, 20)) | |
78 | |
79 for x in range(10): | |
80 self.assertEquals(d[x], x + 10) | |
81 self.assertEquals(d.get(x), x + 10) | |
82 self.failUnless(x in d) | |
83 self.failUnless(d.has_key(x)) | |
84 | |
85 del d[2], d[4], d[6] | |
86 | |
87 self.assertEquals(len(d), 7) | |
88 self.assertEquals(d[2], 'Default') | |
89 self.assertEquals(d[4], 'Default') | |
90 self.assertEquals(d[6], 'Default') | |
91 | |
92 d.update({'a': None, 'b': (), 'c': '*'}) | |
93 self.assertEquals(len(d), 10) | |
94 self.assertEquals(d['a'], None) | |
95 self.assertEquals(d['b'], ()) | |
96 self.assertEquals(d['c'], '*') | |
97 | |
98 d.clear() | |
99 self.assertEquals(len(d), 0) | |
100 | |
101 self.assertEquals(d.setdefault('key', 'value'), 'value') | |
102 self.assertEquals(d['key'], 'value') | |
103 | |
104 self.assertEquals(d.popitem(), ('key', 'value')) | |
105 self.assertEquals(len(d), 0) | |
106 | |
107 dcopy = d.copy() | |
108 self.assertEquals(d.domains, dcopy.domains) | |
109 self.assertEquals(d.default, dcopy.default) | |
110 | |
111 | |
112 def _stringificationTest(self, stringifier): | |
113 """ | |
114 Assert that the class name of a L{mail.mail.DomainWithDefaultDict} | |
115 instance and the string-formatted underlying domain dictionary both | |
116 appear in the string produced by the given string-returning function. | |
117 | |
118 @type stringifier: one-argument callable | |
119 @param stringifier: either C{str} or C{repr}, to be used to get a | |
120 string to make assertions against. | |
121 """ | |
122 domain = mail.mail.DomainWithDefaultDict({}, 'Default') | |
123 self.assertIn(domain.__class__.__name__, stringifier(domain)) | |
124 domain['key'] = 'value' | |
125 self.assertIn(str({'key': 'value'}), stringifier(domain)) | |
126 | |
127 | |
128 def test_str(self): | |
129 """ | |
130 L{DomainWithDefaultDict.__str__} should return a string including | |
131 the class name and the domain mapping held by the instance. | |
132 """ | |
133 self._stringificationTest(str) | |
134 | |
135 | |
136 def test_repr(self): | |
137 """ | |
138 L{DomainWithDefaultDict.__repr__} should return a string including | |
139 the class name and the domain mapping held by the instance. | |
140 """ | |
141 self._stringificationTest(repr) | |
142 | |
143 | |
144 | |
145 class BounceTestCase(unittest.TestCase): | |
146 def setUp(self): | |
147 self.domain = mail.mail.BounceDomain() | |
148 | |
149 def testExists(self): | |
150 self.assertRaises(smtp.AddressError, self.domain.exists, "any user") | |
151 | |
152 def testRelay(self): | |
153 self.assertEquals( | |
154 self.domain.willRelay("random q emailer", "protocol"), | |
155 False | |
156 ) | |
157 | |
158 def testMessage(self): | |
159 self.assertRaises(NotImplementedError, self.domain.startMessage, "whomev
er") | |
160 | |
161 def testAddUser(self): | |
162 self.domain.addUser("bob", "password") | |
163 self.assertRaises(smtp.SMTPBadRcpt, self.domain.exists, "bob") | |
164 | |
165 class FileMessageTestCase(unittest.TestCase): | |
166 def setUp(self): | |
167 self.name = "fileMessage.testFile" | |
168 self.final = "final.fileMessage.testFile" | |
169 self.f = file(self.name, 'w') | |
170 self.fp = mail.mail.FileMessage(self.f, self.name, self.final) | |
171 | |
172 def tearDown(self): | |
173 try: | |
174 self.f.close() | |
175 except: | |
176 pass | |
177 try: | |
178 os.remove(self.name) | |
179 except: | |
180 pass | |
181 try: | |
182 os.remove(self.final) | |
183 except: | |
184 pass | |
185 | |
186 def testFinalName(self): | |
187 return self.fp.eomReceived().addCallback(self._cbFinalName) | |
188 | |
189 def _cbFinalName(self, result): | |
190 self.assertEquals(result, self.final) | |
191 self.failUnless(self.f.closed) | |
192 self.failIf(os.path.exists(self.name)) | |
193 | |
194 def testContents(self): | |
195 contents = "first line\nsecond line\nthird line\n" | |
196 for line in contents.splitlines(): | |
197 self.fp.lineReceived(line) | |
198 self.fp.eomReceived() | |
199 self.assertEquals(file(self.final).read(), contents) | |
200 | |
201 def testInterrupted(self): | |
202 contents = "first line\nsecond line\n" | |
203 for line in contents.splitlines(): | |
204 self.fp.lineReceived(line) | |
205 self.fp.connectionLost() | |
206 self.failIf(os.path.exists(self.name)) | |
207 self.failIf(os.path.exists(self.final)) | |
208 | |
209 class MailServiceTestCase(unittest.TestCase): | |
210 def setUp(self): | |
211 self.service = mail.mail.MailService() | |
212 | |
213 def testFactories(self): | |
214 f = self.service.getPOP3Factory() | |
215 self.failUnless(isinstance(f, protocol.ServerFactory)) | |
216 self.failUnless(f.buildProtocol(('127.0.0.1', 12345)), pop3.POP3) | |
217 | |
218 f = self.service.getSMTPFactory() | |
219 self.failUnless(isinstance(f, protocol.ServerFactory)) | |
220 self.failUnless(f.buildProtocol(('127.0.0.1', 12345)), smtp.SMTP) | |
221 | |
222 f = self.service.getESMTPFactory() | |
223 self.failUnless(isinstance(f, protocol.ServerFactory)) | |
224 self.failUnless(f.buildProtocol(('127.0.0.1', 12345)), smtp.ESMTP) | |
225 | |
226 def testPortals(self): | |
227 o1 = object() | |
228 o2 = object() | |
229 self.service.portals['domain'] = o1 | |
230 self.service.portals[''] = o2 | |
231 | |
232 self.failUnless(self.service.lookupPortal('domain') is o1) | |
233 self.failUnless(self.service.defaultPortal() is o2) | |
234 | |
235 class FailingMaildirMailboxAppendMessageTask(mail.maildir._MaildirMailboxAppendM
essageTask): | |
236 _openstate = True | |
237 _writestate = True | |
238 _renamestate = True | |
239 def osopen(self, fn, attr, mode): | |
240 if self._openstate: | |
241 return os.open(fn, attr, mode) | |
242 else: | |
243 raise OSError(errno.EPERM, "Faked Permission Problem") | |
244 def oswrite(self, fh, data): | |
245 if self._writestate: | |
246 return os.write(fh, data) | |
247 else: | |
248 raise OSError(errno.ENOSPC, "Faked Space problem") | |
249 def osrename(self, oldname, newname): | |
250 if self._renamestate: | |
251 return os.rename(oldname, newname) | |
252 else: | |
253 raise OSError(errno.EPERM, "Faked Permission Problem") | |
254 | |
255 class MaildirAppendStringTestCase(unittest.TestCase): | |
256 def setUp(self): | |
257 self.d = self.mktemp() | |
258 mail.maildir.initializeMaildir(self.d) | |
259 | |
260 def tearDown(self): | |
261 shutil.rmtree(self.d) | |
262 | |
263 def _append(self, ignored, mbox): | |
264 d = mbox.appendMessage('TEST') | |
265 return self.assertFailure(d, Exception) | |
266 | |
267 def _setState(self, ignored, mbox, rename=None, write=None, open=None): | |
268 if rename is not None: | |
269 mbox.AppendFactory._renameState = rename | |
270 if write is not None: | |
271 mbox.AppendFactory._writeState = write | |
272 if open is not None: | |
273 mbox.AppendFactory._openstate = open | |
274 | |
275 def testAppend(self): | |
276 mbox = mail.maildir.MaildirMailbox(self.d) | |
277 mbox.AppendFactory = FailingMaildirMailboxAppendMessageTask | |
278 ds = [] | |
279 for i in xrange(1, 11): | |
280 ds.append(mbox.appendMessage("X" * i)) | |
281 ds[-1].addCallback(self.assertEqual, None) | |
282 d = defer.gatherResults(ds) | |
283 d.addCallback(self._cbTestAppend, mbox) | |
284 return d | |
285 | |
286 def _cbTestAppend(self, result, mbox): | |
287 self.assertEquals(len(mbox.listMessages()), | |
288 10) | |
289 self.assertEquals(len(mbox.getMessage(5).read()), 6) | |
290 # test in the right order: last to first error location. | |
291 mbox.AppendFactory._renamestate = False | |
292 d = self._append(None, mbox) | |
293 d.addCallback(self._setState, mbox, rename=True, write=False) | |
294 d.addCallback(self._append, mbox) | |
295 d.addCallback(self._setState, mbox, write=True, open=False) | |
296 d.addCallback(self._append, mbox) | |
297 d.addCallback(self._setState, mbox, open=True) | |
298 return d | |
299 | |
300 | |
301 class MaildirAppendFileTestCase(unittest.TestCase): | |
302 def setUp(self): | |
303 self.d = self.mktemp() | |
304 mail.maildir.initializeMaildir(self.d) | |
305 | |
306 def tearDown(self): | |
307 shutil.rmtree(self.d) | |
308 | |
309 def testAppend(self): | |
310 mbox = mail.maildir.MaildirMailbox(self.d) | |
311 ds = [] | |
312 def _check(res, t): | |
313 t.close() | |
314 self.assertEqual(res, None) | |
315 for i in xrange(1, 11): | |
316 temp = tempfile.TemporaryFile() | |
317 temp.write("X" * i) | |
318 temp.seek(0,0) | |
319 ds.append(mbox.appendMessage(temp)) | |
320 ds[-1].addCallback(_check, temp) | |
321 return defer.gatherResults(ds).addCallback(self._cbTestAppend, mbox) | |
322 | |
323 def _cbTestAppend(self, result, mbox): | |
324 self.assertEquals(len(mbox.listMessages()), | |
325 10) | |
326 self.assertEquals(len(mbox.getMessage(5).read()), 6) | |
327 | |
328 | |
329 class MaildirTestCase(unittest.TestCase): | |
330 def setUp(self): | |
331 self.d = self.mktemp() | |
332 mail.maildir.initializeMaildir(self.d) | |
333 | |
334 def tearDown(self): | |
335 shutil.rmtree(self.d) | |
336 | |
337 def testInitializer(self): | |
338 d = self.d | |
339 trash = os.path.join(d, '.Trash') | |
340 | |
341 self.failUnless(os.path.exists(d) and os.path.isdir(d)) | |
342 self.failUnless(os.path.exists(os.path.join(d, 'new'))) | |
343 self.failUnless(os.path.exists(os.path.join(d, 'cur'))) | |
344 self.failUnless(os.path.exists(os.path.join(d, 'tmp'))) | |
345 self.failUnless(os.path.isdir(os.path.join(d, 'new'))) | |
346 self.failUnless(os.path.isdir(os.path.join(d, 'cur'))) | |
347 self.failUnless(os.path.isdir(os.path.join(d, 'tmp'))) | |
348 | |
349 self.failUnless(os.path.exists(os.path.join(trash, 'new'))) | |
350 self.failUnless(os.path.exists(os.path.join(trash, 'cur'))) | |
351 self.failUnless(os.path.exists(os.path.join(trash, 'tmp'))) | |
352 self.failUnless(os.path.isdir(os.path.join(trash, 'new'))) | |
353 self.failUnless(os.path.isdir(os.path.join(trash, 'cur'))) | |
354 self.failUnless(os.path.isdir(os.path.join(trash, 'tmp'))) | |
355 | |
356 def testMailbox(self): | |
357 j = os.path.join | |
358 n = mail.maildir._generateMaildirName | |
359 msgs = [j(b, n()) for b in ('cur', 'new') for x in range(5)] | |
360 | |
361 # Toss a few files into the mailbox | |
362 i = 1 | |
363 for f in msgs: | |
364 f = file(j(self.d, f), 'w') | |
365 f.write('x' * i) | |
366 f.close() | |
367 i = i + 1 | |
368 | |
369 mb = mail.maildir.MaildirMailbox(self.d) | |
370 self.assertEquals(mb.listMessages(), range(1, 11)) | |
371 self.assertEquals(mb.listMessages(1), 2) | |
372 self.assertEquals(mb.listMessages(5), 6) | |
373 | |
374 self.assertEquals(mb.getMessage(6).read(), 'x' * 7) | |
375 self.assertEquals(mb.getMessage(1).read(), 'x' * 2) | |
376 | |
377 d = {} | |
378 for i in range(10): | |
379 u = mb.getUidl(i) | |
380 self.failIf(u in d) | |
381 d[u] = None | |
382 | |
383 p, f = os.path.split(msgs[5]) | |
384 | |
385 mb.deleteMessage(5) | |
386 self.assertEquals(mb.listMessages(5), 0) | |
387 self.failUnless(os.path.exists(j(self.d, '.Trash', 'cur', f))) | |
388 self.failIf(os.path.exists(j(self.d, msgs[5]))) | |
389 | |
390 mb.undeleteMessages() | |
391 self.assertEquals(mb.listMessages(5), 6) | |
392 self.failIf(os.path.exists(j(self.d, '.Trash', 'cur', f))) | |
393 self.failUnless(os.path.exists(j(self.d, msgs[5]))) | |
394 | |
395 class MaildirDirdbmDomainTestCase(unittest.TestCase): | |
396 def setUp(self): | |
397 self.P = self.mktemp() | |
398 self.S = mail.mail.MailService() | |
399 self.D = mail.maildir.MaildirDirdbmDomain(self.S, self.P) | |
400 | |
401 def tearDown(self): | |
402 shutil.rmtree(self.P) | |
403 | |
404 def testAddUser(self): | |
405 toAdd = (('user1', 'pwd1'), ('user2', 'pwd2'), ('user3', 'pwd3')) | |
406 for (u, p) in toAdd: | |
407 self.D.addUser(u, p) | |
408 | |
409 for (u, p) in toAdd: | |
410 self.failUnless(u in self.D.dbm) | |
411 self.assertEquals(self.D.dbm[u], p) | |
412 self.failUnless(os.path.exists(os.path.join(self.P, u))) | |
413 | |
414 def testCredentials(self): | |
415 creds = self.D.getCredentialsCheckers() | |
416 | |
417 self.assertEquals(len(creds), 1) | |
418 self.failUnless(cred.checkers.ICredentialsChecker.providedBy(creds[0])) | |
419 self.failUnless(cred.credentials.IUsernamePassword in creds[0].credentia
lInterfaces) | |
420 | |
421 def testRequestAvatar(self): | |
422 class ISomething(Interface): | |
423 pass | |
424 | |
425 self.D.addUser('user', 'password') | |
426 self.assertRaises( | |
427 NotImplementedError, | |
428 self.D.requestAvatar, 'user', None, ISomething | |
429 ) | |
430 | |
431 t = self.D.requestAvatar('user', None, pop3.IMailbox) | |
432 self.assertEquals(len(t), 3) | |
433 self.failUnless(t[0] is pop3.IMailbox) | |
434 self.failUnless(pop3.IMailbox.providedBy(t[1])) | |
435 | |
436 t[2]() | |
437 | |
438 def testRequestAvatarId(self): | |
439 self.D.addUser('user', 'password') | |
440 database = self.D.getCredentialsCheckers()[0] | |
441 | |
442 creds = cred.credentials.UsernamePassword('user', 'wrong password') | |
443 self.assertRaises( | |
444 cred.error.UnauthorizedLogin, | |
445 database.requestAvatarId, creds | |
446 ) | |
447 | |
448 creds = cred.credentials.UsernamePassword('user', 'password') | |
449 self.assertEquals(database.requestAvatarId(creds), 'user') | |
450 | |
451 | |
452 class StubAliasableDomain(object): | |
453 """ | |
454 Minimal testable implementation of IAliasableDomain. | |
455 """ | |
456 implements(mail.mail.IAliasableDomain) | |
457 | |
458 def exists(self, user): | |
459 """ | |
460 No test coverage for invocations of this method on domain objects, | |
461 so we just won't implement it. | |
462 """ | |
463 raise NotImplementedError() | |
464 | |
465 | |
466 def addUser(self, user, password): | |
467 """ | |
468 No test coverage for invocations of this method on domain objects, | |
469 so we just won't implement it. | |
470 """ | |
471 raise NotImplementedError() | |
472 | |
473 | |
474 def getCredentialsCheckers(self): | |
475 """ | |
476 This needs to succeed in order for other tests to complete | |
477 successfully, but we don't actually assert anything about its | |
478 behavior. Return an empty list. Sometime later we should return | |
479 something else and assert that a portal got set up properly. | |
480 """ | |
481 return [] | |
482 | |
483 | |
484 def setAliasGroup(self, aliases): | |
485 """ | |
486 Just record the value so the test can check it later. | |
487 """ | |
488 self.aliasGroup = aliases | |
489 | |
490 | |
491 class ServiceDomainTestCase(unittest.TestCase): | |
492 def setUp(self): | |
493 self.S = mail.mail.MailService() | |
494 self.D = mail.protocols.DomainDeliveryBase(self.S, None) | |
495 self.D.service = self.S | |
496 self.D.protocolName = 'TEST' | |
497 self.D.host = 'hostname' | |
498 | |
499 self.tmpdir = self.mktemp() | |
500 domain = mail.maildir.MaildirDirdbmDomain(self.S, self.tmpdir) | |
501 domain.addUser('user', 'password') | |
502 self.S.addDomain('test.domain', domain) | |
503 | |
504 def tearDown(self): | |
505 shutil.rmtree(self.tmpdir) | |
506 | |
507 | |
508 def testAddAliasableDomain(self): | |
509 """ | |
510 Test that adding an IAliasableDomain to a mail service properly sets | |
511 up alias group references and such. | |
512 """ | |
513 aliases = object() | |
514 domain = StubAliasableDomain() | |
515 self.S.aliases = aliases | |
516 self.S.addDomain('example.com', domain) | |
517 self.assertIdentical(domain.aliasGroup, aliases) | |
518 | |
519 | |
520 def testReceivedHeader(self): | |
521 hdr = self.D.receivedHeader( | |
522 ('remotehost', '123.232.101.234'), | |
523 smtp.Address('<someguy@somplace>'), | |
524 ['user@host.name'] | |
525 ) | |
526 fp = StringIO.StringIO(hdr) | |
527 m = rfc822.Message(fp) | |
528 self.assertEquals(len(m.items()), 1) | |
529 self.failUnless(m.has_key('Received')) | |
530 | |
531 def testValidateTo(self): | |
532 user = smtp.User('user@test.domain', 'helo', None, 'wherever@whatever') | |
533 return defer.maybeDeferred(self.D.validateTo, user | |
534 ).addCallback(self._cbValidateTo | |
535 ) | |
536 | |
537 def _cbValidateTo(self, result): | |
538 self.failUnless(callable(result)) | |
539 | |
540 def testValidateToBadUsername(self): | |
541 user = smtp.User('resu@test.domain', 'helo', None, 'wherever@whatever') | |
542 return self.assertFailure( | |
543 defer.maybeDeferred(self.D.validateTo, user), | |
544 smtp.SMTPBadRcpt) | |
545 | |
546 def testValidateToBadDomain(self): | |
547 user = smtp.User('user@domain.test', 'helo', None, 'wherever@whatever') | |
548 return self.assertFailure( | |
549 defer.maybeDeferred(self.D.validateTo, user), | |
550 smtp.SMTPBadRcpt) | |
551 | |
552 def testValidateFrom(self): | |
553 helo = ('hostname', '127.0.0.1') | |
554 origin = smtp.Address('<user@hostname>') | |
555 self.failUnless(self.D.validateFrom(helo, origin) is origin) | |
556 | |
557 helo = ('hostname', '1.2.3.4') | |
558 origin = smtp.Address('<user@hostname>') | |
559 self.failUnless(self.D.validateFrom(helo, origin) is origin) | |
560 | |
561 helo = ('hostname', '1.2.3.4') | |
562 origin = smtp.Address('<>') | |
563 self.failUnless(self.D.validateFrom(helo, origin) is origin) | |
564 | |
565 self.assertRaises( | |
566 smtp.SMTPBadSender, | |
567 self.D.validateFrom, None, origin | |
568 ) | |
569 | |
570 class VirtualPOP3TestCase(unittest.TestCase): | |
571 def setUp(self): | |
572 self.tmpdir = self.mktemp() | |
573 self.S = mail.mail.MailService() | |
574 self.D = mail.maildir.MaildirDirdbmDomain(self.S, self.tmpdir) | |
575 self.D.addUser('user', 'password') | |
576 self.S.addDomain('test.domain', self.D) | |
577 | |
578 portal = cred.portal.Portal(self.D) | |
579 map(portal.registerChecker, self.D.getCredentialsCheckers()) | |
580 self.S.portals[''] = self.S.portals['test.domain'] = portal | |
581 | |
582 self.P = mail.protocols.VirtualPOP3() | |
583 self.P.service = self.S | |
584 self.P.magic = '<unit test magic>' | |
585 | |
586 def tearDown(self): | |
587 shutil.rmtree(self.tmpdir) | |
588 | |
589 def testAuthenticateAPOP(self): | |
590 resp = md5.new(self.P.magic + 'password').hexdigest() | |
591 return self.P.authenticateUserAPOP('user', resp | |
592 ).addCallback(self._cbAuthenticateAPOP | |
593 ) | |
594 | |
595 def _cbAuthenticateAPOP(self, result): | |
596 self.assertEquals(len(result), 3) | |
597 self.assertEquals(result[0], pop3.IMailbox) | |
598 self.failUnless(pop3.IMailbox.providedBy(result[1])) | |
599 result[2]() | |
600 | |
601 def testAuthenticateIncorrectUserAPOP(self): | |
602 resp = md5.new(self.P.magic + 'password').hexdigest() | |
603 return self.assertFailure( | |
604 self.P.authenticateUserAPOP('resu', resp), | |
605 cred.error.UnauthorizedLogin) | |
606 | |
607 def testAuthenticateIncorrectResponseAPOP(self): | |
608 resp = md5.new('wrong digest').hexdigest() | |
609 return self.assertFailure( | |
610 self.P.authenticateUserAPOP('user', resp), | |
611 cred.error.UnauthorizedLogin) | |
612 | |
613 def testAuthenticatePASS(self): | |
614 return self.P.authenticateUserPASS('user', 'password' | |
615 ).addCallback(self._cbAuthenticatePASS | |
616 ) | |
617 | |
618 def _cbAuthenticatePASS(self, result): | |
619 self.assertEquals(len(result), 3) | |
620 self.assertEquals(result[0], pop3.IMailbox) | |
621 self.failUnless(pop3.IMailbox.providedBy(result[1])) | |
622 result[2]() | |
623 | |
624 def testAuthenticateBadUserPASS(self): | |
625 return self.assertFailure( | |
626 self.P.authenticateUserPASS('resu', 'password'), | |
627 cred.error.UnauthorizedLogin) | |
628 | |
629 def testAuthenticateBadPasswordPASS(self): | |
630 return self.assertFailure( | |
631 self.P.authenticateUserPASS('user', 'wrong password'), | |
632 cred.error.UnauthorizedLogin) | |
633 | |
634 class empty(smtp.User): | |
635 def __init__(self): | |
636 pass | |
637 | |
638 class RelayTestCase(unittest.TestCase): | |
639 def testExists(self): | |
640 service = mail.mail.MailService() | |
641 domain = mail.relay.DomainQueuer(service) | |
642 | |
643 doRelay = [ | |
644 address.UNIXAddress('/var/run/mail-relay'), | |
645 address.IPv4Address('TCP', '127.0.0.1', 12345), | |
646 ] | |
647 | |
648 dontRelay = [ | |
649 address.IPv4Address('TCP', '192.168.2.1', 62), | |
650 address.IPv4Address('TCP', '1.2.3.4', 1943), | |
651 ] | |
652 | |
653 for peer in doRelay: | |
654 user = empty() | |
655 user.orig = 'user@host' | |
656 user.dest = 'tsoh@resu' | |
657 user.protocol = empty() | |
658 user.protocol.transport = empty() | |
659 user.protocol.transport.getPeer = lambda: peer | |
660 | |
661 self.failUnless(callable(domain.exists(user))) | |
662 | |
663 for peer in dontRelay: | |
664 user = empty() | |
665 user.orig = 'some@place' | |
666 user.protocol = empty() | |
667 user.protocol.transport = empty() | |
668 user.protocol.transport.getPeer = lambda: peer | |
669 user.dest = 'who@cares' | |
670 | |
671 self.assertRaises(smtp.SMTPBadRcpt, domain.exists, user) | |
672 | |
673 class RelayerTestCase(unittest.TestCase): | |
674 def setUp(self): | |
675 self.tmpdir = self.mktemp() | |
676 os.mkdir(self.tmpdir) | |
677 self.messageFiles = [] | |
678 for i in range(10): | |
679 name = os.path.join(self.tmpdir, 'body-%d' % (i,)) | |
680 f = file(name + '-H', 'w') | |
681 pickle.dump(['from-%d' % (i,), 'to-%d' % (i,)], f) | |
682 f.close() | |
683 | |
684 f = file(name + '-D', 'w') | |
685 f.write(name) | |
686 f.seek(0, 0) | |
687 self.messageFiles.append(name) | |
688 | |
689 self.R = mail.relay.RelayerMixin() | |
690 self.R.loadMessages(self.messageFiles) | |
691 | |
692 def tearDown(self): | |
693 shutil.rmtree(self.tmpdir) | |
694 | |
695 def testMailFrom(self): | |
696 for i in range(10): | |
697 self.assertEquals(self.R.getMailFrom(), 'from-%d' % (i,)) | |
698 self.R.sentMail(250, None, None, None, None) | |
699 self.assertEquals(self.R.getMailFrom(), None) | |
700 | |
701 def testMailTo(self): | |
702 for i in range(10): | |
703 self.assertEquals(self.R.getMailTo(), ['to-%d' % (i,)]) | |
704 self.R.sentMail(250, None, None, None, None) | |
705 self.assertEquals(self.R.getMailTo(), None) | |
706 | |
707 def testMailData(self): | |
708 for i in range(10): | |
709 name = os.path.join(self.tmpdir, 'body-%d' % (i,)) | |
710 self.assertEquals(self.R.getMailData().read(), name) | |
711 self.R.sentMail(250, None, None, None, None) | |
712 self.assertEquals(self.R.getMailData(), None) | |
713 | |
714 class Manager: | |
715 def __init__(self): | |
716 self.success = [] | |
717 self.failure = [] | |
718 self.done = [] | |
719 | |
720 def notifySuccess(self, factory, message): | |
721 self.success.append((factory, message)) | |
722 | |
723 def notifyFailure(self, factory, message): | |
724 self.failure.append((factory, message)) | |
725 | |
726 def notifyDone(self, factory): | |
727 self.done.append(factory) | |
728 | |
729 class ManagedRelayerTestCase(unittest.TestCase): | |
730 def setUp(self): | |
731 self.manager = Manager() | |
732 self.messages = range(0, 20, 2) | |
733 self.factory = object() | |
734 self.relay = mail.relaymanager.ManagedRelayerMixin(self.manager) | |
735 self.relay.messages = self.messages[:] | |
736 self.relay.names = self.messages[:] | |
737 self.relay.factory = self.factory | |
738 | |
739 def testSuccessfulSentMail(self): | |
740 for i in self.messages: | |
741 self.relay.sentMail(250, None, None, None, None) | |
742 | |
743 self.assertEquals( | |
744 self.manager.success, | |
745 [(self.factory, m) for m in self.messages] | |
746 ) | |
747 | |
748 def testFailedSentMail(self): | |
749 for i in self.messages: | |
750 self.relay.sentMail(550, None, None, None, None) | |
751 | |
752 self.assertEquals( | |
753 self.manager.failure, | |
754 [(self.factory, m) for m in self.messages] | |
755 ) | |
756 | |
757 def testConnectionLost(self): | |
758 self.relay.connectionLost(failure.Failure(Exception())) | |
759 self.assertEquals(self.manager.done, [self.factory]) | |
760 | |
761 class DirectoryQueueTestCase(unittest.TestCase): | |
762 def setUp(self): | |
763 # This is almost a test case itself. | |
764 self.tmpdir = self.mktemp() | |
765 os.mkdir(self.tmpdir) | |
766 self.queue = mail.relaymanager.Queue(self.tmpdir) | |
767 self.queue.noisy = False | |
768 for m in range(25): | |
769 hdrF, msgF = self.queue.createNewMessage() | |
770 pickle.dump(['header', m], hdrF) | |
771 hdrF.close() | |
772 msgF.lineReceived('body: %d' % (m,)) | |
773 msgF.eomReceived() | |
774 self.queue.readDirectory() | |
775 | |
776 def tearDown(self): | |
777 shutil.rmtree(self.tmpdir) | |
778 | |
779 def testWaiting(self): | |
780 self.failUnless(self.queue.hasWaiting()) | |
781 self.assertEquals(len(self.queue.getWaiting()), 25) | |
782 | |
783 waiting = self.queue.getWaiting() | |
784 self.queue.setRelaying(waiting[0]) | |
785 self.assertEquals(len(self.queue.getWaiting()), 24) | |
786 | |
787 self.queue.setWaiting(waiting[0]) | |
788 self.assertEquals(len(self.queue.getWaiting()), 25) | |
789 | |
790 def testRelaying(self): | |
791 for m in self.queue.getWaiting(): | |
792 self.queue.setRelaying(m) | |
793 self.assertEquals( | |
794 len(self.queue.getRelayed()), | |
795 25 - len(self.queue.getWaiting()) | |
796 ) | |
797 | |
798 self.failIf(self.queue.hasWaiting()) | |
799 | |
800 relayed = self.queue.getRelayed() | |
801 self.queue.setWaiting(relayed[0]) | |
802 self.assertEquals(len(self.queue.getWaiting()), 1) | |
803 self.assertEquals(len(self.queue.getRelayed()), 24) | |
804 | |
805 def testDone(self): | |
806 msg = self.queue.getWaiting()[0] | |
807 self.queue.setRelaying(msg) | |
808 self.queue.done(msg) | |
809 | |
810 self.assertEquals(len(self.queue.getWaiting()), 24) | |
811 self.assertEquals(len(self.queue.getRelayed()), 0) | |
812 | |
813 self.failIf(msg in self.queue.getWaiting()) | |
814 self.failIf(msg in self.queue.getRelayed()) | |
815 | |
816 def testEnvelope(self): | |
817 envelopes = [] | |
818 | |
819 for msg in self.queue.getWaiting(): | |
820 envelopes.append(self.queue.getEnvelope(msg)) | |
821 | |
822 envelopes.sort() | |
823 for i in range(25): | |
824 self.assertEquals( | |
825 envelopes.pop(0), | |
826 ['header', i] | |
827 ) | |
828 | |
829 from twisted.names import server | |
830 from twisted.names import client | |
831 from twisted.names import common | |
832 | |
833 class TestAuthority(common.ResolverBase): | |
834 def __init__(self): | |
835 common.ResolverBase.__init__(self) | |
836 self.addresses = {} | |
837 | |
838 def _lookup(self, name, cls, type, timeout = None): | |
839 if name in self.addresses and type == dns.MX: | |
840 results = [] | |
841 for a in self.addresses[name]: | |
842 hdr = dns.RRHeader( | |
843 name, dns.MX, dns.IN, 60, dns.Record_MX(0, a) | |
844 ) | |
845 results.append(hdr) | |
846 return defer.succeed((results, [], [])) | |
847 return defer.fail(failure.Failure(dns.DomainError(name))) | |
848 | |
849 def setUpDNS(self): | |
850 self.auth = TestAuthority() | |
851 factory = server.DNSServerFactory([self.auth]) | |
852 protocol = dns.DNSDatagramProtocol(factory) | |
853 while 1: | |
854 self.port = reactor.listenTCP(0, factory, interface='127.0.0.1') | |
855 portNumber = self.port.getHost().port | |
856 | |
857 try: | |
858 self.udpPort = reactor.listenUDP(portNumber, protocol, interface='12
7.0.0.1') | |
859 except CannotListenError: | |
860 self.port.stopListening() | |
861 else: | |
862 break | |
863 self.resolver = client.Resolver(servers=[('127.0.0.1', portNumber)]) | |
864 | |
865 | |
866 def tearDownDNS(self): | |
867 dl = [] | |
868 dl.append(defer.maybeDeferred(self.port.stopListening)) | |
869 dl.append(defer.maybeDeferred(self.udpPort.stopListening)) | |
870 if self.resolver.protocol.transport is not None: | |
871 dl.append(defer.maybeDeferred(self.resolver.protocol.transport.stopListe
ning)) | |
872 try: | |
873 self.resolver._parseCall.cancel() | |
874 except: | |
875 pass | |
876 return defer.DeferredList(dl) | |
877 | |
878 class MXTestCase(unittest.TestCase): | |
879 """ | |
880 Tests for L{mail.relaymanager.MXCalculator}. | |
881 """ | |
882 def setUp(self): | |
883 setUpDNS(self) | |
884 self.clock = task.Clock() | |
885 self.mx = mail.relaymanager.MXCalculator(self.resolver, self.clock) | |
886 | |
887 def tearDown(self): | |
888 return tearDownDNS(self) | |
889 | |
890 | |
891 def test_defaultClock(self): | |
892 """ | |
893 L{MXCalculator}'s default clock is C{twisted.internet.reactor}. | |
894 """ | |
895 self.assertIdentical( | |
896 mail.relaymanager.MXCalculator(self.resolver).clock, | |
897 reactor) | |
898 | |
899 | |
900 def testSimpleSuccess(self): | |
901 self.auth.addresses['test.domain'] = ['the.email.test.domain'] | |
902 return self.mx.getMX('test.domain').addCallback(self._cbSimpleSuccess) | |
903 | |
904 def _cbSimpleSuccess(self, mx): | |
905 self.assertEquals(mx.preference, 0) | |
906 self.assertEquals(str(mx.name), 'the.email.test.domain') | |
907 | |
908 def testSimpleFailure(self): | |
909 self.mx.fallbackToDomain = False | |
910 return self.assertFailure(self.mx.getMX('test.domain'), IOError) | |
911 | |
912 def testSimpleFailureWithFallback(self): | |
913 return self.assertFailure(self.mx.getMX('test.domain'), DNSLookupError) | |
914 | |
915 | |
916 def _exchangeTest(self, domain, records, correctMailExchange): | |
917 """ | |
918 Issue an MX request for the given domain and arrange for it to be | |
919 responded to with the given records. Verify that the resulting mail | |
920 exchange is the indicated host. | |
921 | |
922 @type domain: C{str} | |
923 @type records: C{list} of L{RRHeader} | |
924 @type correctMailExchange: C{str} | |
925 @rtype: L{Deferred} | |
926 """ | |
927 class DummyResolver(object): | |
928 def lookupMailExchange(self, name): | |
929 if name == domain: | |
930 return defer.succeed(( | |
931 records, | |
932 [], | |
933 [])) | |
934 return defer.fail(DNSNameError(domain)) | |
935 | |
936 self.mx.resolver = DummyResolver() | |
937 d = self.mx.getMX(domain) | |
938 def gotMailExchange(record): | |
939 self.assertEqual(str(record.name), correctMailExchange) | |
940 d.addCallback(gotMailExchange) | |
941 return d | |
942 | |
943 | |
944 def test_mailExchangePreference(self): | |
945 """ | |
946 The MX record with the lowest preference is returned by | |
947 L{MXCalculator.getMX}. | |
948 """ | |
949 domain = "example.com" | |
950 good = "good.example.com" | |
951 bad = "bad.example.com" | |
952 | |
953 records = [ | |
954 RRHeader(name=domain, | |
955 type=Record_MX.TYPE, | |
956 payload=Record_MX(1, bad)), | |
957 RRHeader(name=domain, | |
958 type=Record_MX.TYPE, | |
959 payload=Record_MX(0, good)), | |
960 RRHeader(name=domain, | |
961 type=Record_MX.TYPE, | |
962 payload=Record_MX(2, bad))] | |
963 return self._exchangeTest(domain, records, good) | |
964 | |
965 | |
966 def test_badExchangeExcluded(self): | |
967 """ | |
968 L{MXCalculator.getMX} returns the MX record with the lowest preference | |
969 which is not also marked as bad. | |
970 """ | |
971 domain = "example.com" | |
972 good = "good.example.com" | |
973 bad = "bad.example.com" | |
974 | |
975 records = [ | |
976 RRHeader(name=domain, | |
977 type=Record_MX.TYPE, | |
978 payload=Record_MX(0, bad)), | |
979 RRHeader(name=domain, | |
980 type=Record_MX.TYPE, | |
981 payload=Record_MX(1, good))] | |
982 self.mx.markBad(bad) | |
983 return self._exchangeTest(domain, records, good) | |
984 | |
985 | |
986 def test_fallbackForAllBadExchanges(self): | |
987 """ | |
988 L{MXCalculator.getMX} returns the MX record with the lowest preference | |
989 if all the MX records in the response have been marked bad. | |
990 """ | |
991 domain = "example.com" | |
992 bad = "bad.example.com" | |
993 worse = "worse.example.com" | |
994 | |
995 records = [ | |
996 RRHeader(name=domain, | |
997 type=Record_MX.TYPE, | |
998 payload=Record_MX(0, bad)), | |
999 RRHeader(name=domain, | |
1000 type=Record_MX.TYPE, | |
1001 payload=Record_MX(1, worse))] | |
1002 self.mx.markBad(bad) | |
1003 self.mx.markBad(worse) | |
1004 return self._exchangeTest(domain, records, bad) | |
1005 | |
1006 | |
1007 def test_badExchangeExpires(self): | |
1008 """ | |
1009 L{MXCalculator.getMX} returns the MX record with the lowest preference | |
1010 if it was last marked bad longer than L{MXCalculator.timeOutBadMX} | |
1011 seconds ago. | |
1012 """ | |
1013 domain = "example.com" | |
1014 good = "good.example.com" | |
1015 previouslyBad = "bad.example.com" | |
1016 | |
1017 records = [ | |
1018 RRHeader(name=domain, | |
1019 type=Record_MX.TYPE, | |
1020 payload=Record_MX(0, previouslyBad)), | |
1021 RRHeader(name=domain, | |
1022 type=Record_MX.TYPE, | |
1023 payload=Record_MX(1, good))] | |
1024 self.mx.markBad(previouslyBad) | |
1025 self.clock.advance(self.mx.timeOutBadMX) | |
1026 return self._exchangeTest(domain, records, previouslyBad) | |
1027 | |
1028 | |
1029 def test_goodExchangeUsed(self): | |
1030 """ | |
1031 L{MXCalculator.getMX} returns the MX record with the lowest preference | |
1032 if it was marked good after it was marked bad. | |
1033 """ | |
1034 domain = "example.com" | |
1035 good = "good.example.com" | |
1036 previouslyBad = "bad.example.com" | |
1037 | |
1038 records = [ | |
1039 RRHeader(name=domain, | |
1040 type=Record_MX.TYPE, | |
1041 payload=Record_MX(0, previouslyBad)), | |
1042 RRHeader(name=domain, | |
1043 type=Record_MX.TYPE, | |
1044 payload=Record_MX(1, good))] | |
1045 self.mx.markBad(previouslyBad) | |
1046 self.mx.markGood(previouslyBad) | |
1047 self.clock.advance(self.mx.timeOutBadMX) | |
1048 return self._exchangeTest(domain, records, previouslyBad) | |
1049 | |
1050 | |
1051 def test_successWithoutResults(self): | |
1052 """ | |
1053 If an MX lookup succeeds but the result set is empty, | |
1054 L{MXCalculator.getMX} should try to look up an I{A} record for the | |
1055 requested name and call back its returned Deferred with that | |
1056 address. | |
1057 """ | |
1058 ip = '1.2.3.4' | |
1059 domain = 'example.org' | |
1060 | |
1061 class DummyResolver(object): | |
1062 """ | |
1063 Fake resolver which will respond to an MX lookup with an empty | |
1064 result set. | |
1065 | |
1066 @ivar mx: A dictionary mapping hostnames to three-tuples of | |
1067 results to be returned from I{MX} lookups. | |
1068 | |
1069 @ivar a: A dictionary mapping hostnames to addresses to be | |
1070 returned from I{A} lookups. | |
1071 """ | |
1072 mx = {domain: ([], [], [])} | |
1073 a = {domain: ip} | |
1074 | |
1075 def lookupMailExchange(self, domain): | |
1076 return defer.succeed(self.mx[domain]) | |
1077 | |
1078 def getHostByName(self, domain): | |
1079 return defer.succeed(self.a[domain]) | |
1080 | |
1081 self.mx.resolver = DummyResolver() | |
1082 d = self.mx.getMX(domain) | |
1083 d.addCallback(self.assertEqual, Record_MX(name=ip)) | |
1084 return d | |
1085 | |
1086 | |
1087 def test_failureWithSuccessfulFallback(self): | |
1088 """ | |
1089 Test that if the MX record lookup fails, fallback is enabled, and an A | |
1090 record is available for the name, then the Deferred returned by | |
1091 L{MXCalculator.getMX} ultimately fires with a Record_MX instance which | |
1092 gives the address in the A record for the name. | |
1093 """ | |
1094 class DummyResolver(object): | |
1095 """ | |
1096 Fake resolver which will fail an MX lookup but then succeed a | |
1097 getHostByName call. | |
1098 """ | |
1099 def lookupMailExchange(self, domain): | |
1100 return defer.fail(DNSNameError()) | |
1101 | |
1102 def getHostByName(self, domain): | |
1103 return defer.succeed("1.2.3.4") | |
1104 | |
1105 self.mx.resolver = DummyResolver() | |
1106 d = self.mx.getMX("domain") | |
1107 d.addCallback(self.assertEqual, Record_MX(name="1.2.3.4")) | |
1108 return d | |
1109 | |
1110 | |
1111 def test_cnameWithoutGlueRecords(self): | |
1112 """ | |
1113 If an MX lookup returns a single CNAME record as a result, MXCalculator | |
1114 will perform an MX lookup for the canonical name indicated and return | |
1115 the MX record which results. | |
1116 """ | |
1117 alias = "alias.example.com" | |
1118 canonical = "canonical.example.com" | |
1119 exchange = "mail.example.com" | |
1120 | |
1121 class DummyResolver(object): | |
1122 """ | |
1123 Fake resolver which will return a CNAME for an MX lookup of a name | |
1124 which is an alias and an MX for an MX lookup of the canonical name. | |
1125 """ | |
1126 def lookupMailExchange(self, domain): | |
1127 if domain == alias: | |
1128 return defer.succeed(( | |
1129 [RRHeader(name=domain, | |
1130 type=Record_CNAME.TYPE, | |
1131 payload=Record_CNAME(canonical))], | |
1132 [], [])) | |
1133 elif domain == canonical: | |
1134 return defer.succeed(( | |
1135 [RRHeader(name=domain, | |
1136 type=Record_MX.TYPE, | |
1137 payload=Record_MX(0, exchange))], | |
1138 [], [])) | |
1139 else: | |
1140 return defer.fail(DNSNameError(domain)) | |
1141 | |
1142 self.mx.resolver = DummyResolver() | |
1143 d = self.mx.getMX(alias) | |
1144 d.addCallback(self.assertEqual, Record_MX(name=exchange)) | |
1145 return d | |
1146 | |
1147 | |
1148 def test_cnameChain(self): | |
1149 """ | |
1150 If L{MXCalculator.getMX} encounters a CNAME chain which is longer than | |
1151 the length specified, the returned L{Deferred} should errback with | |
1152 L{CanonicalNameChainTooLong}. | |
1153 """ | |
1154 class DummyResolver(object): | |
1155 """ | |
1156 Fake resolver which generates a CNAME chain of infinite length in | |
1157 response to MX lookups. | |
1158 """ | |
1159 chainCounter = 0 | |
1160 | |
1161 def lookupMailExchange(self, domain): | |
1162 self.chainCounter += 1 | |
1163 name = 'x-%d.example.com' % (self.chainCounter,) | |
1164 return defer.succeed(( | |
1165 [RRHeader(name=domain, | |
1166 type=Record_CNAME.TYPE, | |
1167 payload=Record_CNAME(name))], | |
1168 [], [])) | |
1169 | |
1170 cnameLimit = 3 | |
1171 self.mx.resolver = DummyResolver() | |
1172 d = self.mx.getMX("mail.example.com", cnameLimit) | |
1173 self.assertFailure( | |
1174 d, twisted.mail.relaymanager.CanonicalNameChainTooLong) | |
1175 def cbChainTooLong(error): | |
1176 self.assertEqual(error.args[0], Record_CNAME("x-%d.example.com" % (c
nameLimit + 1,))) | |
1177 self.assertEqual(self.mx.resolver.chainCounter, cnameLimit + 1) | |
1178 d.addCallback(cbChainTooLong) | |
1179 return d | |
1180 | |
1181 | |
1182 def test_cnameWithGlueRecords(self): | |
1183 """ | |
1184 If an MX lookup returns a CNAME and the MX record for the CNAME, the | |
1185 L{Deferred} returned by L{MXCalculator.getMX} should be called back | |
1186 with the name from the MX record without further lookups being | |
1187 attempted. | |
1188 """ | |
1189 lookedUp = [] | |
1190 alias = "alias.example.com" | |
1191 canonical = "canonical.example.com" | |
1192 exchange = "mail.example.com" | |
1193 | |
1194 class DummyResolver(object): | |
1195 def lookupMailExchange(self, domain): | |
1196 if domain != alias or lookedUp: | |
1197 # Don't give back any results for anything except the alias | |
1198 # or on any request after the first. | |
1199 return ([], [], []) | |
1200 return defer.succeed(( | |
1201 [RRHeader(name=alias, | |
1202 type=Record_CNAME.TYPE, | |
1203 payload=Record_CNAME(canonical)), | |
1204 RRHeader(name=canonical, | |
1205 type=Record_MX.TYPE, | |
1206 payload=Record_MX(name=exchange))], | |
1207 [], [])) | |
1208 | |
1209 self.mx.resolver = DummyResolver() | |
1210 d = self.mx.getMX(alias) | |
1211 d.addCallback(self.assertEqual, Record_MX(name=exchange)) | |
1212 return d | |
1213 | |
1214 | |
1215 def test_cnameLoopWithGlueRecords(self): | |
1216 """ | |
1217 If an MX lookup returns two CNAME records which point to each other, | |
1218 the loop should be detected and the L{Deferred} returned by | |
1219 L{MXCalculator.getMX} should be errbacked with L{CanonicalNameLoop}. | |
1220 """ | |
1221 firstAlias = "cname1.example.com" | |
1222 secondAlias = "cname2.example.com" | |
1223 | |
1224 class DummyResolver(object): | |
1225 def lookupMailExchange(self, domain): | |
1226 return defer.succeed(( | |
1227 [RRHeader(name=firstAlias, | |
1228 type=Record_CNAME.TYPE, | |
1229 payload=Record_CNAME(secondAlias)), | |
1230 RRHeader(name=secondAlias, | |
1231 type=Record_CNAME.TYPE, | |
1232 payload=Record_CNAME(firstAlias))], | |
1233 [], [])) | |
1234 | |
1235 self.mx.resolver = DummyResolver() | |
1236 d = self.mx.getMX(firstAlias) | |
1237 self.assertFailure(d, twisted.mail.relaymanager.CanonicalNameLoop) | |
1238 return d | |
1239 | |
1240 | |
1241 def testManyRecords(self): | |
1242 self.auth.addresses['test.domain'] = [ | |
1243 'mx1.test.domain', 'mx2.test.domain', 'mx3.test.domain' | |
1244 ] | |
1245 return self.mx.getMX('test.domain' | |
1246 ).addCallback(self._cbManyRecordsSuccessfulLookup | |
1247 ) | |
1248 | |
1249 def _cbManyRecordsSuccessfulLookup(self, mx): | |
1250 self.failUnless(str(mx.name).split('.', 1)[0] in ('mx1', 'mx2', 'mx3')) | |
1251 self.mx.markBad(str(mx.name)) | |
1252 return self.mx.getMX('test.domain' | |
1253 ).addCallback(self._cbManyRecordsDifferentResult, mx | |
1254 ) | |
1255 | |
1256 def _cbManyRecordsDifferentResult(self, nextMX, mx): | |
1257 self.assertNotEqual(str(mx.name), str(nextMX.name)) | |
1258 self.mx.markBad(str(nextMX.name)) | |
1259 | |
1260 return self.mx.getMX('test.domain' | |
1261 ).addCallback(self._cbManyRecordsLastResult, mx, nextMX | |
1262 ) | |
1263 | |
1264 def _cbManyRecordsLastResult(self, lastMX, mx, nextMX): | |
1265 self.assertNotEqual(str(mx.name), str(lastMX.name)) | |
1266 self.assertNotEqual(str(nextMX.name), str(lastMX.name)) | |
1267 | |
1268 self.mx.markBad(str(lastMX.name)) | |
1269 self.mx.markGood(str(nextMX.name)) | |
1270 | |
1271 return self.mx.getMX('test.domain' | |
1272 ).addCallback(self._cbManyRecordsRepeatSpecificResult, nextMX | |
1273 ) | |
1274 | |
1275 def _cbManyRecordsRepeatSpecificResult(self, againMX, nextMX): | |
1276 self.assertEqual(str(againMX.name), str(nextMX.name)) | |
1277 | |
1278 class LiveFireExercise(unittest.TestCase): | |
1279 if interfaces.IReactorUDP(reactor, None) is None: | |
1280 skip = "UDP support is required to determining MX records" | |
1281 | |
1282 def setUp(self): | |
1283 setUpDNS(self) | |
1284 self.tmpdirs = [ | |
1285 'domainDir', 'insertionDomain', 'insertionQueue', | |
1286 'destinationDomain', 'destinationQueue' | |
1287 ] | |
1288 | |
1289 def tearDown(self): | |
1290 for d in self.tmpdirs: | |
1291 if os.path.exists(d): | |
1292 shutil.rmtree(d) | |
1293 return tearDownDNS(self) | |
1294 | |
1295 def testLocalDelivery(self): | |
1296 service = mail.mail.MailService() | |
1297 service.smtpPortal.registerChecker(cred.checkers.AllowAnonymousAccess()) | |
1298 domain = mail.maildir.MaildirDirdbmDomain(service, 'domainDir') | |
1299 domain.addUser('user', 'password') | |
1300 service.addDomain('test.domain', domain) | |
1301 service.portals[''] = service.portals['test.domain'] | |
1302 map(service.portals[''].registerChecker, domain.getCredentialsCheckers()
) | |
1303 | |
1304 service.setQueue(mail.relay.DomainQueuer(service)) | |
1305 manager = mail.relaymanager.SmartHostSMTPRelayingManager(service.queue,
None) | |
1306 helper = mail.relaymanager.RelayStateHelper(manager, 1) | |
1307 | |
1308 f = service.getSMTPFactory() | |
1309 | |
1310 self.smtpServer = reactor.listenTCP(0, f, interface='127.0.0.1') | |
1311 | |
1312 client = LineSendingProtocol([ | |
1313 'HELO meson', | |
1314 'MAIL FROM: <user@hostname>', | |
1315 'RCPT TO: <user@test.domain>', | |
1316 'DATA', | |
1317 'This is the message', | |
1318 '.', | |
1319 'QUIT' | |
1320 ]) | |
1321 | |
1322 done = Deferred() | |
1323 f = protocol.ClientFactory() | |
1324 f.protocol = lambda: client | |
1325 f.clientConnectionLost = lambda *args: done.callback(None) | |
1326 reactor.connectTCP('127.0.0.1', self.smtpServer.getHost().port, f) | |
1327 | |
1328 def finished(ign): | |
1329 mbox = domain.requestAvatar('user', None, pop3.IMailbox)[1] | |
1330 msg = mbox.getMessage(0).read() | |
1331 self.failIfEqual(msg.find('This is the message'), -1) | |
1332 | |
1333 return self.smtpServer.stopListening() | |
1334 done.addCallback(finished) | |
1335 return done | |
1336 | |
1337 | |
1338 def testRelayDelivery(self): | |
1339 # Here is the service we will connect to and send mail from | |
1340 insServ = mail.mail.MailService() | |
1341 insServ.smtpPortal.registerChecker(cred.checkers.AllowAnonymousAccess()) | |
1342 domain = mail.maildir.MaildirDirdbmDomain(insServ, 'insertionDomain') | |
1343 insServ.addDomain('insertion.domain', domain) | |
1344 os.mkdir('insertionQueue') | |
1345 insServ.setQueue(mail.relaymanager.Queue('insertionQueue')) | |
1346 insServ.domains.setDefaultDomain(mail.relay.DomainQueuer(insServ)) | |
1347 manager = mail.relaymanager.SmartHostSMTPRelayingManager(insServ.queue) | |
1348 manager.fArgs += ('test.identity.hostname',) | |
1349 helper = mail.relaymanager.RelayStateHelper(manager, 1) | |
1350 # Yoink! Now the internet obeys OUR every whim! | |
1351 manager.mxcalc = mail.relaymanager.MXCalculator(self.resolver) | |
1352 # And this is our whim. | |
1353 self.auth.addresses['destination.domain'] = ['127.0.0.1'] | |
1354 | |
1355 f = insServ.getSMTPFactory() | |
1356 self.insServer = reactor.listenTCP(0, f, interface='127.0.0.1') | |
1357 | |
1358 # Here is the service the previous one will connect to for final | |
1359 # delivery | |
1360 destServ = mail.mail.MailService() | |
1361 destServ.smtpPortal.registerChecker(cred.checkers.AllowAnonymousAccess()
) | |
1362 domain = mail.maildir.MaildirDirdbmDomain(destServ, 'destinationDomain') | |
1363 domain.addUser('user', 'password') | |
1364 destServ.addDomain('destination.domain', domain) | |
1365 os.mkdir('destinationQueue') | |
1366 destServ.setQueue(mail.relaymanager.Queue('destinationQueue')) | |
1367 manager2 = mail.relaymanager.SmartHostSMTPRelayingManager(destServ.queue
) | |
1368 helper = mail.relaymanager.RelayStateHelper(manager, 1) | |
1369 helper.startService() | |
1370 | |
1371 f = destServ.getSMTPFactory() | |
1372 self.destServer = reactor.listenTCP(0, f, interface='127.0.0.1') | |
1373 | |
1374 # Update the port number the *first* relay will connect to, because we c
an't use | |
1375 # port 25 | |
1376 manager.PORT = self.destServer.getHost().port | |
1377 | |
1378 client = LineSendingProtocol([ | |
1379 'HELO meson', | |
1380 'MAIL FROM: <user@wherever>', | |
1381 'RCPT TO: <user@destination.domain>', | |
1382 'DATA', | |
1383 'This is the message', | |
1384 '.', | |
1385 'QUIT' | |
1386 ]) | |
1387 | |
1388 done = Deferred() | |
1389 f = protocol.ClientFactory() | |
1390 f.protocol = lambda: client | |
1391 f.clientConnectionLost = lambda *args: done.callback(None) | |
1392 reactor.connectTCP('127.0.0.1', self.insServer.getHost().port, f) | |
1393 | |
1394 def finished(ign): | |
1395 # First part of the delivery is done. Poke the queue manually now | |
1396 # so we don't have to wait for the queue to be flushed. | |
1397 delivery = manager.checkState() | |
1398 def delivered(ign): | |
1399 mbox = domain.requestAvatar('user', None, pop3.IMailbox)[1] | |
1400 msg = mbox.getMessage(0).read() | |
1401 self.failIfEqual(msg.find('This is the message'), -1) | |
1402 | |
1403 self.insServer.stopListening() | |
1404 self.destServer.stopListening() | |
1405 helper.stopService() | |
1406 delivery.addCallback(delivered) | |
1407 return delivery | |
1408 done.addCallback(finished) | |
1409 return done | |
1410 | |
1411 | |
1412 aliasFile = StringIO.StringIO("""\ | |
1413 # Here's a comment | |
1414 # woop another one | |
1415 testuser: address1,address2, address3, | |
1416 continuation@address, |/bin/process/this | |
1417 | |
1418 usertwo:thisaddress,thataddress, lastaddress | |
1419 lastuser: :/includable, /filename, |/program, address | |
1420 """) | |
1421 | |
1422 class LineBufferMessage: | |
1423 def __init__(self): | |
1424 self.lines = [] | |
1425 self.eom = False | |
1426 self.lost = False | |
1427 | |
1428 def lineReceived(self, line): | |
1429 self.lines.append(line) | |
1430 | |
1431 def eomReceived(self): | |
1432 self.eom = True | |
1433 return defer.succeed('<Whatever>') | |
1434 | |
1435 def connectionLost(self): | |
1436 self.lost = True | |
1437 | |
1438 class AliasTestCase(unittest.TestCase): | |
1439 lines = [ | |
1440 'First line', | |
1441 'Next line', | |
1442 '', | |
1443 'After a blank line', | |
1444 'Last line' | |
1445 ] | |
1446 | |
1447 def setUp(self): | |
1448 aliasFile.seek(0) | |
1449 | |
1450 def testHandle(self): | |
1451 result = {} | |
1452 lines = [ | |
1453 'user: another@host\n', | |
1454 'nextuser: |/bin/program\n', | |
1455 'user: me@again\n', | |
1456 'moreusers: :/etc/include/filename\n', | |
1457 'multiuser: first@host, second@host,last@anotherhost', | |
1458 ] | |
1459 | |
1460 for l in lines: | |
1461 mail.alias.handle(result, l, 'TestCase', None) | |
1462 | |
1463 self.assertEquals(result['user'], ['another@host', 'me@again']) | |
1464 self.assertEquals(result['nextuser'], ['|/bin/program']) | |
1465 self.assertEquals(result['moreusers'], [':/etc/include/filename']) | |
1466 self.assertEquals(result['multiuser'], ['first@host', 'second@host', 'la
st@anotherhost']) | |
1467 | |
1468 def testFileLoader(self): | |
1469 domains = {'': object()} | |
1470 result = mail.alias.loadAliasFile(domains, fp=aliasFile) | |
1471 | |
1472 self.assertEquals(len(result), 3) | |
1473 | |
1474 group = result['testuser'] | |
1475 s = str(group) | |
1476 for a in ('address1', 'address2', 'address3', 'continuation@address', '/
bin/process/this'): | |
1477 self.failIfEqual(s.find(a), -1) | |
1478 self.assertEquals(len(group), 5) | |
1479 | |
1480 group = result['usertwo'] | |
1481 s = str(group) | |
1482 for a in ('thisaddress', 'thataddress', 'lastaddress'): | |
1483 self.failIfEqual(s.find(a), -1) | |
1484 self.assertEquals(len(group), 3) | |
1485 | |
1486 group = result['lastuser'] | |
1487 s = str(group) | |
1488 self.failUnlessEqual(s.find('/includable'), -1) | |
1489 for a in ('/filename', 'program', 'address'): | |
1490 self.failIfEqual(s.find(a), -1, '%s not found' % a) | |
1491 self.assertEquals(len(group), 3) | |
1492 | |
1493 def testMultiWrapper(self): | |
1494 msgs = LineBufferMessage(), LineBufferMessage(), LineBufferMessage() | |
1495 msg = mail.alias.MultiWrapper(msgs) | |
1496 | |
1497 for L in self.lines: | |
1498 msg.lineReceived(L) | |
1499 return msg.eomReceived().addCallback(self._cbMultiWrapper, msgs) | |
1500 | |
1501 def _cbMultiWrapper(self, ignored, msgs): | |
1502 for m in msgs: | |
1503 self.failUnless(m.eom) | |
1504 self.failIf(m.lost) | |
1505 self.assertEquals(self.lines, m.lines) | |
1506 | |
1507 def testFileAlias(self): | |
1508 tmpfile = self.mktemp() | |
1509 a = mail.alias.FileAlias(tmpfile, None, None) | |
1510 m = a.createMessageReceiver() | |
1511 | |
1512 for l in self.lines: | |
1513 m.lineReceived(l) | |
1514 return m.eomReceived().addCallback(self._cbTestFileAlias, tmpfile) | |
1515 | |
1516 def _cbTestFileAlias(self, ignored, tmpfile): | |
1517 lines = file(tmpfile).readlines() | |
1518 self.assertEquals([L[:-1] for L in lines], self.lines) | |
1519 | |
1520 | |
1521 | |
1522 class DummyProcess(object): | |
1523 __slots__ = ['onEnd'] | |
1524 | |
1525 | |
1526 | |
1527 class MockProcessAlias(mail.alias.ProcessAlias): | |
1528 """ | |
1529 A alias processor that doesn't actually launch processes. | |
1530 """ | |
1531 | |
1532 def spawnProcess(self, proto, program, path): | |
1533 """ | |
1534 Don't spawn a process. | |
1535 """ | |
1536 | |
1537 | |
1538 | |
1539 class MockAliasGroup(mail.alias.AliasGroup): | |
1540 """ | |
1541 An alias group using C{MockProcessAlias}. | |
1542 """ | |
1543 processAliasFactory = MockProcessAlias | |
1544 | |
1545 | |
1546 | |
1547 class StubProcess(object): | |
1548 """ | |
1549 Fake implementation of L{IProcessTransport}. | |
1550 | |
1551 @ivar signals: A list of all the signals which have been sent to this fake | |
1552 process. | |
1553 """ | |
1554 def __init__(self): | |
1555 self.signals = [] | |
1556 | |
1557 | |
1558 def loseConnection(self): | |
1559 """ | |
1560 No-op implementation of disconnection. | |
1561 """ | |
1562 | |
1563 | |
1564 def signalProcess(self, signal): | |
1565 """ | |
1566 Record a signal sent to this process for later inspection. | |
1567 """ | |
1568 self.signals.append(signal) | |
1569 | |
1570 | |
1571 | |
1572 class ProcessAliasTestCase(unittest.TestCase): | |
1573 """ | |
1574 Tests for alias resolution. | |
1575 """ | |
1576 | |
1577 lines = [ | |
1578 'First line', | |
1579 'Next line', | |
1580 '', | |
1581 'After a blank line', | |
1582 'Last line' | |
1583 ] | |
1584 | |
1585 def exitStatus(self, code): | |
1586 """ | |
1587 Construct a status from the given exit code. | |
1588 | |
1589 @type code: L{int} between 0 and 255 inclusive. | |
1590 @param code: The exit status which the code will represent. | |
1591 | |
1592 @rtype: L{int} | |
1593 @return: A status integer for the given exit code. | |
1594 """ | |
1595 # /* Macros for constructing status values. */ | |
1596 # #define __W_EXITCODE(ret, sig) ((ret) << 8 | (sig)) | |
1597 status = (code << 8) | 0 | |
1598 | |
1599 # Sanity check | |
1600 self.assertTrue(os.WIFEXITED(status)) | |
1601 self.assertEqual(os.WEXITSTATUS(status), code) | |
1602 self.assertFalse(os.WIFSIGNALED(status)) | |
1603 | |
1604 return status | |
1605 | |
1606 | |
1607 def signalStatus(self, signal): | |
1608 """ | |
1609 Construct a status from the given signal. | |
1610 | |
1611 @type signal: L{int} between 0 and 255 inclusive. | |
1612 @param signal: The signal number which the status will represent. | |
1613 | |
1614 @rtype: L{int} | |
1615 @return: A status integer for the given signal. | |
1616 """ | |
1617 # /* If WIFSIGNALED(STATUS), the terminating signal. */ | |
1618 # #define __WTERMSIG(status) ((status) & 0x7f) | |
1619 # /* Nonzero if STATUS indicates termination by a signal. */ | |
1620 # #define __WIFSIGNALED(status) \ | |
1621 # (((signed char) (((status) & 0x7f) + 1) >> 1) > 0) | |
1622 status = signal | |
1623 | |
1624 # Sanity check | |
1625 self.assertTrue(os.WIFSIGNALED(status)) | |
1626 self.assertEqual(os.WTERMSIG(status), signal) | |
1627 self.assertFalse(os.WIFEXITED(status)) | |
1628 | |
1629 return status | |
1630 | |
1631 | |
1632 def setUp(self): | |
1633 """ | |
1634 Replace L{smtp.DNSNAME} with a well-known value. | |
1635 """ | |
1636 self.DNSNAME = smtp.DNSNAME | |
1637 smtp.DNSNAME = '' | |
1638 | |
1639 | |
1640 def tearDown(self): | |
1641 """ | |
1642 Restore the original value of L{smtp.DNSNAME}. | |
1643 """ | |
1644 smtp.DNSNAME = self.DNSNAME | |
1645 | |
1646 | |
1647 def test_processAlias(self): | |
1648 """ | |
1649 Standard call to C{mail.alias.ProcessAlias}: check that the specified | |
1650 script is called, and that the input is correctly transferred to it. | |
1651 """ | |
1652 sh = FilePath(self.mktemp()) | |
1653 sh.setContent("""\ | |
1654 #!/bin/sh | |
1655 rm -f process.alias.out | |
1656 while read i; do | |
1657 echo $i >> process.alias.out | |
1658 done""") | |
1659 os.chmod(sh.path, 0700) | |
1660 a = mail.alias.ProcessAlias(sh.path, None, None) | |
1661 m = a.createMessageReceiver() | |
1662 | |
1663 for l in self.lines: | |
1664 m.lineReceived(l) | |
1665 | |
1666 def _cbProcessAlias(ignored): | |
1667 lines = file('process.alias.out').readlines() | |
1668 self.assertEquals([L[:-1] for L in lines], self.lines) | |
1669 | |
1670 return m.eomReceived().addCallback(_cbProcessAlias) | |
1671 | |
1672 | |
1673 def test_processAliasTimeout(self): | |
1674 """ | |
1675 If the alias child process does not exit within a particular period of | |
1676 time, the L{Deferred} returned by L{MessageWrapper.eomReceived} should | |
1677 fail with L{ProcessAliasTimeout} and send the I{KILL} signal to the | |
1678 child process.. | |
1679 """ | |
1680 reactor = task.Clock() | |
1681 transport = StubProcess() | |
1682 proto = mail.alias.ProcessAliasProtocol() | |
1683 proto.makeConnection(transport) | |
1684 | |
1685 receiver = mail.alias.MessageWrapper(proto, None, reactor) | |
1686 d = receiver.eomReceived() | |
1687 reactor.advance(receiver.completionTimeout) | |
1688 def timedOut(ignored): | |
1689 self.assertEqual(transport.signals, ['KILL']) | |
1690 # Now that it has been killed, disconnect the protocol associated | |
1691 # with it. | |
1692 proto.processEnded( | |
1693 ProcessTerminated(self.signalStatus(signal.SIGKILL))) | |
1694 self.assertFailure(d, mail.alias.ProcessAliasTimeout) | |
1695 d.addCallback(timedOut) | |
1696 return d | |
1697 | |
1698 | |
1699 def test_earlyProcessTermination(self): | |
1700 """ | |
1701 If the process associated with an L{mail.alias.MessageWrapper} exits | |
1702 before I{eomReceived} is called, the L{Deferred} returned by | |
1703 I{eomReceived} should fail. | |
1704 """ | |
1705 transport = StubProcess() | |
1706 protocol = mail.alias.ProcessAliasProtocol() | |
1707 protocol.makeConnection(transport) | |
1708 receiver = mail.alias.MessageWrapper(protocol, None, None) | |
1709 protocol.processEnded(failure.Failure(ProcessDone(0))) | |
1710 return self.assertFailure(receiver.eomReceived(), ProcessDone) | |
1711 | |
1712 | |
1713 def _terminationTest(self, status): | |
1714 """ | |
1715 Verify that if the process associated with an | |
1716 L{mail.alias.MessageWrapper} exits with the given status, the | |
1717 L{Deferred} returned by I{eomReceived} fails with L{ProcessTerminated}. | |
1718 """ | |
1719 transport = StubProcess() | |
1720 protocol = mail.alias.ProcessAliasProtocol() | |
1721 protocol.makeConnection(transport) | |
1722 receiver = mail.alias.MessageWrapper(protocol, None, None) | |
1723 protocol.processEnded( | |
1724 failure.Failure(ProcessTerminated(status))) | |
1725 return self.assertFailure(receiver.eomReceived(), ProcessTerminated) | |
1726 | |
1727 | |
1728 def test_errorProcessTermination(self): | |
1729 """ | |
1730 If the process associated with an L{mail.alias.MessageWrapper} exits | |
1731 with a non-zero exit code, the L{Deferred} returned by I{eomReceived} | |
1732 should fail. | |
1733 """ | |
1734 return self._terminationTest(self.exitStatus(1)) | |
1735 | |
1736 | |
1737 def test_signalProcessTermination(self): | |
1738 """ | |
1739 If the process associated with an L{mail.alias.MessageWrapper} exits | |
1740 because it received a signal, the L{Deferred} returned by | |
1741 I{eomReceived} should fail. | |
1742 """ | |
1743 return self._terminationTest(self.signalStatus(signal.SIGHUP)) | |
1744 | |
1745 | |
1746 def test_aliasResolution(self): | |
1747 """ | |
1748 Check that the C{resolve} method of alias processors produce the correct | |
1749 set of objects: | |
1750 - direct alias with L{mail.alias.AddressAlias} if a simple input is
passed | |
1751 - aliases in a file with L{mail.alias.FileWrapper} if an input in th
e format | |
1752 '/file' is given | |
1753 - aliases resulting of a process call wrapped by L{mail.alias.Messag
eWrapper} | |
1754 if the format is '|process' | |
1755 """ | |
1756 aliases = {} | |
1757 domain = {'': TestDomain(aliases, ['user1', 'user2', 'user3'])} | |
1758 A1 = MockAliasGroup(['user1', '|echo', '/file'], domain, 'alias1') | |
1759 A2 = MockAliasGroup(['user2', 'user3'], domain, 'alias2') | |
1760 A3 = mail.alias.AddressAlias('alias1', domain, 'alias3') | |
1761 aliases.update({ | |
1762 'alias1': A1, | |
1763 'alias2': A2, | |
1764 'alias3': A3, | |
1765 }) | |
1766 | |
1767 res1 = A1.resolve(aliases) | |
1768 r1 = map(str, res1.objs) | |
1769 r1.sort() | |
1770 expected = map(str, [ | |
1771 mail.alias.AddressAlias('user1', None, None), | |
1772 mail.alias.MessageWrapper(DummyProcess(), 'echo'), | |
1773 mail.alias.FileWrapper('/file'), | |
1774 ]) | |
1775 expected.sort() | |
1776 self.assertEquals(r1, expected) | |
1777 | |
1778 res2 = A2.resolve(aliases) | |
1779 r2 = map(str, res2.objs) | |
1780 r2.sort() | |
1781 expected = map(str, [ | |
1782 mail.alias.AddressAlias('user2', None, None), | |
1783 mail.alias.AddressAlias('user3', None, None) | |
1784 ]) | |
1785 expected.sort() | |
1786 self.assertEquals(r2, expected) | |
1787 | |
1788 res3 = A3.resolve(aliases) | |
1789 r3 = map(str, res3.objs) | |
1790 r3.sort() | |
1791 expected = map(str, [ | |
1792 mail.alias.AddressAlias('user1', None, None), | |
1793 mail.alias.MessageWrapper(DummyProcess(), 'echo'), | |
1794 mail.alias.FileWrapper('/file'), | |
1795 ]) | |
1796 expected.sort() | |
1797 self.assertEquals(r3, expected) | |
1798 | |
1799 | |
1800 def test_cyclicAlias(self): | |
1801 """ | |
1802 Check that a cycle in alias resolution is correctly handled. | |
1803 """ | |
1804 aliases = {} | |
1805 domain = {'': TestDomain(aliases, [])} | |
1806 A1 = mail.alias.AddressAlias('alias2', domain, 'alias1') | |
1807 A2 = mail.alias.AddressAlias('alias3', domain, 'alias2') | |
1808 A3 = mail.alias.AddressAlias('alias1', domain, 'alias3') | |
1809 aliases.update({ | |
1810 'alias1': A1, | |
1811 'alias2': A2, | |
1812 'alias3': A3 | |
1813 }) | |
1814 | |
1815 self.assertEquals(aliases['alias1'].resolve(aliases), None) | |
1816 self.assertEquals(aliases['alias2'].resolve(aliases), None) | |
1817 self.assertEquals(aliases['alias3'].resolve(aliases), None) | |
1818 | |
1819 A4 = MockAliasGroup(['|echo', 'alias1'], domain, 'alias4') | |
1820 aliases['alias4'] = A4 | |
1821 | |
1822 res = A4.resolve(aliases) | |
1823 r = map(str, res.objs) | |
1824 r.sort() | |
1825 expected = map(str, [ | |
1826 mail.alias.MessageWrapper(DummyProcess(), 'echo') | |
1827 ]) | |
1828 expected.sort() | |
1829 self.assertEquals(r, expected) | |
1830 | |
1831 | |
1832 | |
1833 if interfaces.IReactorProcess(reactor, None) is None: | |
1834 ProcessAliasTestCase = "IReactorProcess not supported" | |
1835 | |
1836 | |
1837 | |
1838 class TestDomain: | |
1839 def __init__(self, aliases, users): | |
1840 self.aliases = aliases | |
1841 self.users = users | |
1842 | |
1843 def exists(self, user, memo=None): | |
1844 user = user.dest.local | |
1845 if user in self.users: | |
1846 return lambda: mail.alias.AddressAlias(user, None, None) | |
1847 try: | |
1848 a = self.aliases[user] | |
1849 except: | |
1850 raise smtp.SMTPBadRcpt(user) | |
1851 else: | |
1852 aliases = a.resolve(self.aliases, memo) | |
1853 if aliases: | |
1854 return lambda: aliases | |
1855 raise smtp.SMTPBadRcpt(user) | |
1856 | |
1857 | |
1858 from twisted.python.runtime import platformType | |
1859 import types | |
1860 if platformType != "posix": | |
1861 for o in locals().values(): | |
1862 if isinstance(o, (types.ClassType, type)) and issubclass(o, unittest.Tes
tCase): | |
1863 o.skip = "twisted.mail only works on posix" | |
OLD | NEW |