| 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 |