1# -*- test-case-name: twisted.mail.test.test_imap -*- 2# Copyright (c) Twisted Matrix Laboratories. 3# See LICENSE for details. 4 5 6""" 7Test case for twisted.mail.imap4 8""" 9 10try: 11 from cStringIO import StringIO 12except ImportError: 13 from StringIO import StringIO 14 15import codecs 16import locale 17import os 18import types 19 20from zope.interface import implements 21 22from twisted.mail.imap4 import MessageSet 23from twisted.mail import imap4 24from twisted.protocols import loopback 25from twisted.internet import defer 26from twisted.internet import error 27from twisted.internet import reactor 28from twisted.internet import interfaces 29from twisted.internet.task import Clock 30from twisted.trial import unittest 31from twisted.python import util, log 32from twisted.python import failure 33 34from twisted import cred 35import twisted.cred.error 36import twisted.cred.checkers 37import twisted.cred.credentials 38import twisted.cred.portal 39 40from twisted.test.proto_helpers import StringTransport, StringTransportWithDisconnection 41 42try: 43 from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext 44except ImportError: 45 ClientTLSContext = ServerTLSContext = None 46 47def strip(f): 48 return lambda result, f=f: f() 49 50def sortNest(l): 51 l = l[:] 52 l.sort() 53 for i in range(len(l)): 54 if isinstance(l[i], types.ListType): 55 l[i] = sortNest(l[i]) 56 elif isinstance(l[i], types.TupleType): 57 l[i] = tuple(sortNest(list(l[i]))) 58 return l 59 60class IMAP4UTF7TestCase(unittest.TestCase): 61 tests = [ 62 [u'Hello world', 'Hello world'], 63 [u'Hello & world', 'Hello &- world'], 64 [u'Hello\xffworld', 'Hello&AP8-world'], 65 [u'\xff\xfe\xfd\xfc', '&AP8A,gD9APw-'], 66 [u'~peter/mail/\u65e5\u672c\u8a9e/\u53f0\u5317', 67 '~peter/mail/&ZeVnLIqe-/&U,BTFw-'], # example from RFC 2060 68 ] 69 70 def test_encodeWithErrors(self): 71 """ 72 Specifying an error policy to C{unicode.encode} with the 73 I{imap4-utf-7} codec should produce the same result as not 74 specifying the error policy. 75 """ 76 text = u'Hello world' 77 self.assertEqual( 78 text.encode('imap4-utf-7', 'strict'), 79 text.encode('imap4-utf-7')) 80 81 82 def test_decodeWithErrors(self): 83 """ 84 Similar to L{test_encodeWithErrors}, but for C{str.decode}. 85 """ 86 bytes = 'Hello world' 87 self.assertEqual( 88 bytes.decode('imap4-utf-7', 'strict'), 89 bytes.decode('imap4-utf-7')) 90 91 92 def test_getreader(self): 93 """ 94 C{codecs.getreader('imap4-utf-7')} returns the I{imap4-utf-7} stream 95 reader class. 96 """ 97 reader = codecs.getreader('imap4-utf-7')(StringIO('Hello&AP8-world')) 98 self.assertEqual(reader.read(), u'Hello\xffworld') 99 100 101 def test_getwriter(self): 102 """ 103 C{codecs.getwriter('imap4-utf-7')} returns the I{imap4-utf-7} stream 104 writer class. 105 """ 106 output = StringIO() 107 writer = codecs.getwriter('imap4-utf-7')(output) 108 writer.write(u'Hello\xffworld') 109 self.assertEqual(output.getvalue(), 'Hello&AP8-world') 110 111 112 def test_encode(self): 113 """ 114 The I{imap4-utf-7} can be used to encode a unicode string into a byte 115 string according to the IMAP4 modified UTF-7 encoding rules. 116 """ 117 for (input, output) in self.tests: 118 self.assertEqual(input.encode('imap4-utf-7'), output) 119 120 121 def test_decode(self): 122 """ 123 The I{imap4-utf-7} can be used to decode a byte string into a unicode 124 string according to the IMAP4 modified UTF-7 encoding rules. 125 """ 126 for (input, output) in self.tests: 127 self.assertEqual(input, output.decode('imap4-utf-7')) 128 129 130 def test_printableSingletons(self): 131 """ 132 The IMAP4 modified UTF-7 implementation encodes all printable 133 characters which are in ASCII using the corresponding ASCII byte. 134 """ 135 # All printables represent themselves 136 for o in range(0x20, 0x26) + range(0x27, 0x7f): 137 self.assertEqual(chr(o), chr(o).encode('imap4-utf-7')) 138 self.assertEqual(chr(o), chr(o).decode('imap4-utf-7')) 139 self.assertEqual('&'.encode('imap4-utf-7'), '&-') 140 self.assertEqual('&-'.decode('imap4-utf-7'), '&') 141 142 143 144class BufferingConsumer: 145 def __init__(self): 146 self.buffer = [] 147 148 def write(self, bytes): 149 self.buffer.append(bytes) 150 if self.consumer: 151 self.consumer.resumeProducing() 152 153 def registerProducer(self, consumer, streaming): 154 self.consumer = consumer 155 self.consumer.resumeProducing() 156 157 def unregisterProducer(self): 158 self.consumer = None 159 160class MessageProducerTestCase(unittest.TestCase): 161 def testSinglePart(self): 162 body = 'This is body text. Rar.' 163 headers = util.OrderedDict() 164 headers['from'] = 'sender@host' 165 headers['to'] = 'recipient@domain' 166 headers['subject'] = 'booga booga boo' 167 headers['content-type'] = 'text/plain' 168 169 msg = FakeyMessage(headers, (), None, body, 123, None ) 170 171 c = BufferingConsumer() 172 p = imap4.MessageProducer(msg) 173 d = p.beginProducing(c) 174 175 def cbProduced(result): 176 self.assertIdentical(result, p) 177 self.assertEqual( 178 ''.join(c.buffer), 179 180 '{119}\r\n' 181 'From: sender@host\r\n' 182 'To: recipient@domain\r\n' 183 'Subject: booga booga boo\r\n' 184 'Content-Type: text/plain\r\n' 185 '\r\n' 186 + body) 187 return d.addCallback(cbProduced) 188 189 190 def testSingleMultiPart(self): 191 outerBody = '' 192 innerBody = 'Contained body message text. Squarge.' 193 headers = util.OrderedDict() 194 headers['from'] = 'sender@host' 195 headers['to'] = 'recipient@domain' 196 headers['subject'] = 'booga booga boo' 197 headers['content-type'] = 'multipart/alternative; boundary="xyz"' 198 199 innerHeaders = util.OrderedDict() 200 innerHeaders['subject'] = 'this is subject text' 201 innerHeaders['content-type'] = 'text/plain' 202 msg = FakeyMessage(headers, (), None, outerBody, 123, 203 [FakeyMessage(innerHeaders, (), None, innerBody, 204 None, None)], 205 ) 206 207 c = BufferingConsumer() 208 p = imap4.MessageProducer(msg) 209 d = p.beginProducing(c) 210 211 def cbProduced(result): 212 self.failUnlessIdentical(result, p) 213 214 self.assertEqual( 215 ''.join(c.buffer), 216 217 '{239}\r\n' 218 'From: sender@host\r\n' 219 'To: recipient@domain\r\n' 220 'Subject: booga booga boo\r\n' 221 'Content-Type: multipart/alternative; boundary="xyz"\r\n' 222 '\r\n' 223 '\r\n' 224 '--xyz\r\n' 225 'Subject: this is subject text\r\n' 226 'Content-Type: text/plain\r\n' 227 '\r\n' 228 + innerBody 229 + '\r\n--xyz--\r\n') 230 231 return d.addCallback(cbProduced) 232 233 234 def testMultipleMultiPart(self): 235 outerBody = '' 236 innerBody1 = 'Contained body message text. Squarge.' 237 innerBody2 = 'Secondary <i>message</i> text of squarge body.' 238 headers = util.OrderedDict() 239 headers['from'] = 'sender@host' 240 headers['to'] = 'recipient@domain' 241 headers['subject'] = 'booga booga boo' 242 headers['content-type'] = 'multipart/alternative; boundary="xyz"' 243 innerHeaders = util.OrderedDict() 244 innerHeaders['subject'] = 'this is subject text' 245 innerHeaders['content-type'] = 'text/plain' 246 innerHeaders2 = util.OrderedDict() 247 innerHeaders2['subject'] = '<b>this is subject</b>' 248 innerHeaders2['content-type'] = 'text/html' 249 msg = FakeyMessage(headers, (), None, outerBody, 123, [ 250 FakeyMessage(innerHeaders, (), None, innerBody1, None, None), 251 FakeyMessage(innerHeaders2, (), None, innerBody2, None, None) 252 ], 253 ) 254 255 c = BufferingConsumer() 256 p = imap4.MessageProducer(msg) 257 d = p.beginProducing(c) 258 259 def cbProduced(result): 260 self.failUnlessIdentical(result, p) 261 262 self.assertEqual( 263 ''.join(c.buffer), 264 265 '{354}\r\n' 266 'From: sender@host\r\n' 267 'To: recipient@domain\r\n' 268 'Subject: booga booga boo\r\n' 269 'Content-Type: multipart/alternative; boundary="xyz"\r\n' 270 '\r\n' 271 '\r\n' 272 '--xyz\r\n' 273 'Subject: this is subject text\r\n' 274 'Content-Type: text/plain\r\n' 275 '\r\n' 276 + innerBody1 277 + '\r\n--xyz\r\n' 278 'Subject: <b>this is subject</b>\r\n' 279 'Content-Type: text/html\r\n' 280 '\r\n' 281 + innerBody2 282 + '\r\n--xyz--\r\n') 283 return d.addCallback(cbProduced) 284 285 286 287class IMAP4HelperTestCase(unittest.TestCase): 288 """ 289 Tests for various helper utilities in the IMAP4 module. 290 """ 291 292 def test_fileProducer(self): 293 b = (('x' * 1) + ('y' * 1) + ('z' * 1)) * 10 294 c = BufferingConsumer() 295 f = StringIO(b) 296 p = imap4.FileProducer(f) 297 d = p.beginProducing(c) 298 299 def cbProduced(result): 300 self.failUnlessIdentical(result, p) 301 self.assertEqual( 302 ('{%d}\r\n' % len(b))+ b, 303 ''.join(c.buffer)) 304 return d.addCallback(cbProduced) 305 306 307 def test_wildcard(self): 308 cases = [ 309 ['foo/%gum/bar', 310 ['foo/bar', 'oo/lalagum/bar', 'foo/gumx/bar', 'foo/gum/baz'], 311 ['foo/xgum/bar', 'foo/gum/bar'], 312 ], ['foo/x%x/bar', 313 ['foo', 'bar', 'fuz fuz fuz', 'foo/*/bar', 'foo/xyz/bar', 'foo/xx/baz'], 314 ['foo/xyx/bar', 'foo/xx/bar', 'foo/xxxxxxxxxxxxxx/bar'], 315 ], ['foo/xyz*abc/bar', 316 ['foo/xyz/bar', 'foo/abc/bar', 'foo/xyzab/cbar', 'foo/xyza/bcbar'], 317 ['foo/xyzabc/bar', 'foo/xyz/abc/bar', 'foo/xyz/123/abc/bar'], 318 ] 319 ] 320 321 for (wildcard, fail, succeed) in cases: 322 wildcard = imap4.wildcardToRegexp(wildcard, '/') 323 for x in fail: 324 self.failIf(wildcard.match(x)) 325 for x in succeed: 326 self.failUnless(wildcard.match(x)) 327 328 329 def test_wildcardNoDelim(self): 330 cases = [ 331 ['foo/%gum/bar', 332 ['foo/bar', 'oo/lalagum/bar', 'foo/gumx/bar', 'foo/gum/baz'], 333 ['foo/xgum/bar', 'foo/gum/bar', 'foo/x/gum/bar'], 334 ], ['foo/x%x/bar', 335 ['foo', 'bar', 'fuz fuz fuz', 'foo/*/bar', 'foo/xyz/bar', 'foo/xx/baz'], 336 ['foo/xyx/bar', 'foo/xx/bar', 'foo/xxxxxxxxxxxxxx/bar', 'foo/x/x/bar'], 337 ], ['foo/xyz*abc/bar', 338 ['foo/xyz/bar', 'foo/abc/bar', 'foo/xyzab/cbar', 'foo/xyza/bcbar'], 339 ['foo/xyzabc/bar', 'foo/xyz/abc/bar', 'foo/xyz/123/abc/bar'], 340 ] 341 ] 342 343 for (wildcard, fail, succeed) in cases: 344 wildcard = imap4.wildcardToRegexp(wildcard, None) 345 for x in fail: 346 self.failIf(wildcard.match(x), x) 347 for x in succeed: 348 self.failUnless(wildcard.match(x), x) 349 350 351 def test_headerFormatter(self): 352 """ 353 L{imap4._formatHeaders} accepts a C{dict} of header name/value pairs and 354 returns a string representing those headers in the standard multiline, 355 C{":"}-separated format. 356 """ 357 cases = [ 358 ({'Header1': 'Value1', 'Header2': 'Value2'}, 'Header2: Value2\r\nHeader1: Value1\r\n'), 359 ] 360 361 for (input, expected) in cases: 362 output = imap4._formatHeaders(input) 363 self.assertEqual(sorted(output.splitlines(True)), 364 sorted(expected.splitlines(True))) 365 366 367 def test_messageSet(self): 368 m1 = MessageSet() 369 m2 = MessageSet() 370 371 self.assertEqual(m1, m2) 372 373 m1 = m1 + (1, 3) 374 self.assertEqual(len(m1), 3) 375 self.assertEqual(list(m1), [1, 2, 3]) 376 377 m2 = m2 + (1, 3) 378 self.assertEqual(m1, m2) 379 self.assertEqual(list(m1 + m2), [1, 2, 3]) 380 381 382 def test_messageSetStringRepresentationWithWildcards(self): 383 """ 384 In a L{MessageSet}, in the presence of wildcards, if the highest message 385 id is known, the wildcard should get replaced by that high value. 386 """ 387 inputs = [ 388 MessageSet(imap4.parseIdList('*')), 389 MessageSet(imap4.parseIdList('3:*', 6)), 390 MessageSet(imap4.parseIdList('*:2', 6)), 391 ] 392 393 outputs = [ 394 "*", 395 "3:6", 396 "2:6", 397 ] 398 399 for i, o in zip(inputs, outputs): 400 self.assertEqual(str(i), o) 401 402 403 def test_messageSetStringRepresentationWithInversion(self): 404 """ 405 In a L{MessageSet}, inverting the high and low numbers in a range 406 doesn't affect the meaning of the range. For example, 3:2 displays just 407 like 2:3, because according to the RFC they have the same meaning. 408 """ 409 inputs = [ 410 MessageSet(imap4.parseIdList('2:3')), 411 MessageSet(imap4.parseIdList('3:2')), 412 ] 413 414 outputs = [ 415 "2:3", 416 "2:3", 417 ] 418 419 for i, o in zip(inputs, outputs): 420 self.assertEqual(str(i), o) 421 422 423 def test_quotedSplitter(self): 424 cases = [ 425 '''Hello World''', 426 '''Hello "World!"''', 427 '''World "Hello" "How are you?"''', 428 '''"Hello world" How "are you?"''', 429 '''foo bar "baz buz" NIL''', 430 '''foo bar "baz buz" "NIL"''', 431 '''foo NIL "baz buz" bar''', 432 '''foo "NIL" "baz buz" bar''', 433 '''"NIL" bar "baz buz" foo''', 434 'oo \\"oo\\" oo', 435 '"oo \\"oo\\" oo"', 436 'oo \t oo', 437 '"oo \t oo"', 438 'oo \\t oo', 439 '"oo \\t oo"', 440 'oo \o oo', 441 '"oo \o oo"', 442 'oo \\o oo', 443 '"oo \\o oo"', 444 ] 445 446 answers = [ 447 ['Hello', 'World'], 448 ['Hello', 'World!'], 449 ['World', 'Hello', 'How are you?'], 450 ['Hello world', 'How', 'are you?'], 451 ['foo', 'bar', 'baz buz', None], 452 ['foo', 'bar', 'baz buz', 'NIL'], 453 ['foo', None, 'baz buz', 'bar'], 454 ['foo', 'NIL', 'baz buz', 'bar'], 455 ['NIL', 'bar', 'baz buz', 'foo'], 456 ['oo', '"oo"', 'oo'], 457 ['oo "oo" oo'], 458 ['oo', 'oo'], 459 ['oo \t oo'], 460 ['oo', '\\t', 'oo'], 461 ['oo \\t oo'], 462 ['oo', '\o', 'oo'], 463 ['oo \o oo'], 464 ['oo', '\\o', 'oo'], 465 ['oo \\o oo'], 466 467 ] 468 469 errors = [ 470 '"mismatched quote', 471 'mismatched quote"', 472 'mismatched"quote', 473 '"oops here is" another"', 474 ] 475 476 for s in errors: 477 self.assertRaises(imap4.MismatchedQuoting, imap4.splitQuoted, s) 478 479 for (case, expected) in zip(cases, answers): 480 self.assertEqual(imap4.splitQuoted(case), expected) 481 482 483 def test_stringCollapser(self): 484 cases = [ 485 ['a', 'b', 'c', 'd', 'e'], 486 ['a', ' ', '"', 'b', 'c', ' ', '"', ' ', 'd', 'e'], 487 [['a', 'b', 'c'], 'd', 'e'], 488 ['a', ['b', 'c', 'd'], 'e'], 489 ['a', 'b', ['c', 'd', 'e']], 490 ['"', 'a', ' ', '"', ['b', 'c', 'd'], '"', ' ', 'e', '"'], 491 ['a', ['"', ' ', 'b', 'c', ' ', ' ', '"'], 'd', 'e'], 492 ] 493 494 answers = [ 495 ['abcde'], 496 ['a', 'bc ', 'de'], 497 [['abc'], 'de'], 498 ['a', ['bcd'], 'e'], 499 ['ab', ['cde']], 500 ['a ', ['bcd'], ' e'], 501 ['a', [' bc '], 'de'], 502 ] 503 504 for (case, expected) in zip(cases, answers): 505 self.assertEqual(imap4.collapseStrings(case), expected) 506 507 508 def test_parenParser(self): 509 s = '\r\n'.join(['xx'] * 4) 510 cases = [ 511 '(BODY.PEEK[HEADER.FIELDS.NOT (subject bcc cc)] {%d}\r\n%s)' % (len(s), s,), 512 513# '(FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" ' 514# 'RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" ' 515# '"IMAP4rev1 WG mtg summary and minutes" ' 516# '(("Terry Gray" NIL "gray" "cac.washington.edu")) ' 517# '(("Terry Gray" NIL "gray" "cac.washington.edu")) ' 518# '(("Terry Gray" NIL "gray" "cac.washington.edu")) ' 519# '((NIL NIL "imap" "cac.washington.edu")) ' 520# '((NIL NIL "minutes" "CNRI.Reston.VA.US") ' 521# '("John Klensin" NIL "KLENSIN" "INFOODS.MIT.EDU")) NIL NIL ' 522# '"<B27397-0100000@cac.washington.edu>") ' 523# 'BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 3028 92))', 524 525 '(FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" ' 526 'RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" ' 527 '"IMAP4rev1 WG mtg summary and minutes" ' 528 '(("Terry Gray" NIL gray cac.washington.edu)) ' 529 '(("Terry Gray" NIL gray cac.washington.edu)) ' 530 '(("Terry Gray" NIL gray cac.washington.edu)) ' 531 '((NIL NIL imap cac.washington.edu)) ' 532 '((NIL NIL minutes CNRI.Reston.VA.US) ' 533 '("John Klensin" NIL KLENSIN INFOODS.MIT.EDU)) NIL NIL ' 534 '<B27397-0100000@cac.washington.edu>) ' 535 'BODY (TEXT PLAIN (CHARSET US-ASCII) NIL NIL 7BIT 3028 92))', 536 '("oo \\"oo\\" oo")', 537 '("oo \\\\ oo")', 538 '("oo \\ oo")', 539 '("oo \\o")', 540 '("oo \o")', 541 '(oo \o)', 542 '(oo \\o)', 543 544 ] 545 546 answers = [ 547 ['BODY.PEEK', ['HEADER.FIELDS.NOT', ['subject', 'bcc', 'cc']], s], 548 549 ['FLAGS', [r'\Seen'], 'INTERNALDATE', 550 '17-Jul-1996 02:44:25 -0700', 'RFC822.SIZE', '4286', 'ENVELOPE', 551 ['Wed, 17 Jul 1996 02:23:25 -0700 (PDT)', 552 'IMAP4rev1 WG mtg summary and minutes', [["Terry Gray", None, 553 "gray", "cac.washington.edu"]], [["Terry Gray", None, 554 "gray", "cac.washington.edu"]], [["Terry Gray", None, 555 "gray", "cac.washington.edu"]], [[None, None, "imap", 556 "cac.washington.edu"]], [[None, None, "minutes", 557 "CNRI.Reston.VA.US"], ["John Klensin", None, "KLENSIN", 558 "INFOODS.MIT.EDU"]], None, None, 559 "<B27397-0100000@cac.washington.edu>"], "BODY", ["TEXT", "PLAIN", 560 ["CHARSET", "US-ASCII"], None, None, "7BIT", "3028", "92"]], 561 ['oo "oo" oo'], 562 ['oo \\\\ oo'], 563 ['oo \\ oo'], 564 ['oo \\o'], 565 ['oo \o'], 566 ['oo', '\o'], 567 ['oo', '\\o'], 568 ] 569 570 for (case, expected) in zip(cases, answers): 571 self.assertEqual(imap4.parseNestedParens(case), [expected]) 572 573 # XXX This code used to work, but changes occurred within the 574 # imap4.py module which made it no longer necessary for *all* of it 575 # to work. In particular, only the part that makes 576 # 'BODY.PEEK[HEADER.FIELDS.NOT (Subject Bcc Cc)]' come out correctly 577 # no longer needs to work. So, I am loathe to delete the entire 578 # section of the test. --exarkun 579 # 580 581# for (case, expected) in zip(answers, cases): 582# self.assertEqual('(' + imap4.collapseNestedLists(case) + ')', expected) 583 584 585 def test_fetchParserSimple(self): 586 cases = [ 587 ['ENVELOPE', 'Envelope'], 588 ['FLAGS', 'Flags'], 589 ['INTERNALDATE', 'InternalDate'], 590 ['RFC822.HEADER', 'RFC822Header'], 591 ['RFC822.SIZE', 'RFC822Size'], 592 ['RFC822.TEXT', 'RFC822Text'], 593 ['RFC822', 'RFC822'], 594 ['UID', 'UID'], 595 ['BODYSTRUCTURE', 'BodyStructure'], 596 ] 597 598 for (inp, outp) in cases: 599 p = imap4._FetchParser() 600 p.parseString(inp) 601 self.assertEqual(len(p.result), 1) 602 self.failUnless(isinstance(p.result[0], getattr(p, outp))) 603 604 605 def test_fetchParserMacros(self): 606 cases = [ 607 ['ALL', (4, ['flags', 'internaldate', 'rfc822.size', 'envelope'])], 608 ['FULL', (5, ['flags', 'internaldate', 'rfc822.size', 'envelope', 'body'])], 609 ['FAST', (3, ['flags', 'internaldate', 'rfc822.size'])], 610 ] 611 612 for (inp, outp) in cases: 613 p = imap4._FetchParser() 614 p.parseString(inp) 615 self.assertEqual(len(p.result), outp[0]) 616 p = [str(p).lower() for p in p.result] 617 p.sort() 618 outp[1].sort() 619 self.assertEqual(p, outp[1]) 620 621 622 def test_fetchParserBody(self): 623 P = imap4._FetchParser 624 625 p = P() 626 p.parseString('BODY') 627 self.assertEqual(len(p.result), 1) 628 self.failUnless(isinstance(p.result[0], p.Body)) 629 self.assertEqual(p.result[0].peek, False) 630 self.assertEqual(p.result[0].header, None) 631 self.assertEqual(str(p.result[0]), 'BODY') 632 633 p = P() 634 p.parseString('BODY.PEEK') 635 self.assertEqual(len(p.result), 1) 636 self.failUnless(isinstance(p.result[0], p.Body)) 637 self.assertEqual(p.result[0].peek, True) 638 self.assertEqual(str(p.result[0]), 'BODY') 639 640 p = P() 641 p.parseString('BODY[]') 642 self.assertEqual(len(p.result), 1) 643 self.failUnless(isinstance(p.result[0], p.Body)) 644 self.assertEqual(p.result[0].empty, True) 645 self.assertEqual(str(p.result[0]), 'BODY[]') 646 647 p = P() 648 p.parseString('BODY[HEADER]') 649 self.assertEqual(len(p.result), 1) 650 self.failUnless(isinstance(p.result[0], p.Body)) 651 self.assertEqual(p.result[0].peek, False) 652 self.failUnless(isinstance(p.result[0].header, p.Header)) 653 self.assertEqual(p.result[0].header.negate, True) 654 self.assertEqual(p.result[0].header.fields, ()) 655 self.assertEqual(p.result[0].empty, False) 656 self.assertEqual(str(p.result[0]), 'BODY[HEADER]') 657 658 p = P() 659 p.parseString('BODY.PEEK[HEADER]') 660 self.assertEqual(len(p.result), 1) 661 self.failUnless(isinstance(p.result[0], p.Body)) 662 self.assertEqual(p.result[0].peek, True) 663 self.failUnless(isinstance(p.result[0].header, p.Header)) 664 self.assertEqual(p.result[0].header.negate, True) 665 self.assertEqual(p.result[0].header.fields, ()) 666 self.assertEqual(p.result[0].empty, False) 667 self.assertEqual(str(p.result[0]), 'BODY[HEADER]') 668 669 p = P() 670 p.parseString('BODY[HEADER.FIELDS (Subject Cc Message-Id)]') 671 self.assertEqual(len(p.result), 1) 672 self.failUnless(isinstance(p.result[0], p.Body)) 673 self.assertEqual(p.result[0].peek, False) 674 self.failUnless(isinstance(p.result[0].header, p.Header)) 675 self.assertEqual(p.result[0].header.negate, False) 676 self.assertEqual(p.result[0].header.fields, ['SUBJECT', 'CC', 'MESSAGE-ID']) 677 self.assertEqual(p.result[0].empty, False) 678 self.assertEqual(str(p.result[0]), 'BODY[HEADER.FIELDS (Subject Cc Message-Id)]') 679 680 p = P() 681 p.parseString('BODY.PEEK[HEADER.FIELDS (Subject Cc Message-Id)]') 682 self.assertEqual(len(p.result), 1) 683 self.failUnless(isinstance(p.result[0], p.Body)) 684 self.assertEqual(p.result[0].peek, True) 685 self.failUnless(isinstance(p.result[0].header, p.Header)) 686 self.assertEqual(p.result[0].header.negate, False) 687 self.assertEqual(p.result[0].header.fields, ['SUBJECT', 'CC', 'MESSAGE-ID']) 688 self.assertEqual(p.result[0].empty, False) 689 self.assertEqual(str(p.result[0]), 'BODY[HEADER.FIELDS (Subject Cc Message-Id)]') 690 691 p = P() 692 p.parseString('BODY.PEEK[HEADER.FIELDS.NOT (Subject Cc Message-Id)]') 693 self.assertEqual(len(p.result), 1) 694 self.failUnless(isinstance(p.result[0], p.Body)) 695 self.assertEqual(p.result[0].peek, True) 696 self.failUnless(isinstance(p.result[0].header, p.Header)) 697 self.assertEqual(p.result[0].header.negate, True) 698 self.assertEqual(p.result[0].header.fields, ['SUBJECT', 'CC', 'MESSAGE-ID']) 699 self.assertEqual(p.result[0].empty, False) 700 self.assertEqual(str(p.result[0]), 'BODY[HEADER.FIELDS.NOT (Subject Cc Message-Id)]') 701 702 p = P() 703 p.parseString('BODY[1.MIME]<10.50>') 704 self.assertEqual(len(p.result), 1) 705 self.failUnless(isinstance(p.result[0], p.Body)) 706 self.assertEqual(p.result[0].peek, False) 707 self.failUnless(isinstance(p.result[0].mime, p.MIME)) 708 self.assertEqual(p.result[0].part, (0,)) 709 self.assertEqual(p.result[0].partialBegin, 10) 710 self.assertEqual(p.result[0].partialLength, 50) 711 self.assertEqual(p.result[0].empty, False) 712 self.assertEqual(str(p.result[0]), 'BODY[1.MIME]<10.50>') 713 714 p = P() 715 p.parseString('BODY.PEEK[1.3.9.11.HEADER.FIELDS.NOT (Message-Id Date)]<103.69>') 716 self.assertEqual(len(p.result), 1) 717 self.failUnless(isinstance(p.result[0], p.Body)) 718 self.assertEqual(p.result[0].peek, True) 719 self.failUnless(isinstance(p.result[0].header, p.Header)) 720 self.assertEqual(p.result[0].part, (0, 2, 8, 10)) 721 self.assertEqual(p.result[0].header.fields, ['MESSAGE-ID', 'DATE']) 722 self.assertEqual(p.result[0].partialBegin, 103) 723 self.assertEqual(p.result[0].partialLength, 69) 724 self.assertEqual(p.result[0].empty, False) 725 self.assertEqual(str(p.result[0]), 'BODY[1.3.9.11.HEADER.FIELDS.NOT (Message-Id Date)]<103.69>') 726 727 728 def test_files(self): 729 inputStructure = [ 730 'foo', 'bar', 'baz', StringIO('this is a file\r\n'), 'buz' 731 ] 732 733 output = '"foo" "bar" "baz" {16}\r\nthis is a file\r\n "buz"' 734 735 self.assertEqual(imap4.collapseNestedLists(inputStructure), output) 736 737 738 def test_quoteAvoider(self): 739 input = [ 740 'foo', imap4.DontQuoteMe('bar'), "baz", StringIO('this is a file\r\n'), 741 imap4.DontQuoteMe('buz'), "" 742 ] 743 744 output = '"foo" bar "baz" {16}\r\nthis is a file\r\n buz ""' 745 746 self.assertEqual(imap4.collapseNestedLists(input), output) 747 748 749 def test_literals(self): 750 cases = [ 751 ('({10}\r\n0123456789)', [['0123456789']]), 752 ] 753 754 for (case, expected) in cases: 755 self.assertEqual(imap4.parseNestedParens(case), expected) 756 757 758 def test_queryBuilder(self): 759 inputs = [ 760 imap4.Query(flagged=1), 761 imap4.Query(sorted=1, unflagged=1, deleted=1), 762 imap4.Or(imap4.Query(flagged=1), imap4.Query(deleted=1)), 763 imap4.Query(before='today'), 764 imap4.Or( 765 imap4.Query(deleted=1), 766 imap4.Query(unseen=1), 767 imap4.Query(new=1) 768 ), 769 imap4.Or( 770 imap4.Not( 771 imap4.Or( 772 imap4.Query(sorted=1, since='yesterday', smaller=1000), 773 imap4.Query(sorted=1, before='tuesday', larger=10000), 774 imap4.Query(sorted=1, unseen=1, deleted=1, before='today'), 775 imap4.Not( 776 imap4.Query(subject='spam') 777 ), 778 ), 779 ), 780 imap4.Not( 781 imap4.Query(uid='1:5') 782 ), 783 ) 784 ] 785 786 outputs = [ 787 'FLAGGED', 788 '(DELETED UNFLAGGED)', 789 '(OR FLAGGED DELETED)', 790 '(BEFORE "today")', 791 '(OR DELETED (OR UNSEEN NEW))', 792 '(OR (NOT (OR (SINCE "yesterday" SMALLER 1000) ' # Continuing 793 '(OR (BEFORE "tuesday" LARGER 10000) (OR (BEFORE ' # Some more 794 '"today" DELETED UNSEEN) (NOT (SUBJECT "spam")))))) ' # And more 795 '(NOT (UID 1:5)))', 796 ] 797 798 for (query, expected) in zip(inputs, outputs): 799 self.assertEqual(query, expected) 800 801 802 def test_queryKeywordFlagWithQuotes(self): 803 """ 804 When passed the C{keyword} argument, L{imap4.Query} returns an unquoted 805 string. 806 807 @see: U{http://tools.ietf.org/html/rfc3501#section-9} 808 @see: U{http://tools.ietf.org/html/rfc3501#section-6.4.4} 809 """ 810 query = imap4.Query(keyword='twisted') 811 self.assertEqual('(KEYWORD twisted)', query) 812 813 814 def test_queryUnkeywordFlagWithQuotes(self): 815 """ 816 When passed the C{unkeyword} argument, L{imap4.Query} returns an 817 unquoted string. 818 819 @see: U{http://tools.ietf.org/html/rfc3501#section-9} 820 @see: U{http://tools.ietf.org/html/rfc3501#section-6.4.4} 821 """ 822 query = imap4.Query(unkeyword='twisted') 823 self.assertEqual('(UNKEYWORD twisted)', query) 824 825 826 def _keywordFilteringTest(self, keyword): 827 """ 828 Helper to implement tests for value filtering of KEYWORD and UNKEYWORD 829 queries. 830 831 @param keyword: A native string giving the name of the L{imap4.Query} 832 keyword argument to test. 833 """ 834 # Check all the printable exclusions 835 self.assertEqual( 836 '(%s twistedrocks)' % (keyword.upper(),), 837 imap4.Query(**{keyword: r'twisted (){%*"\] rocks'})) 838 839 # Check all the non-printable exclusions 840 self.assertEqual( 841 '(%s twistedrocks)' % (keyword.upper(),), 842 imap4.Query(**{ 843 keyword: 'twisted %s rocks' % ( 844 ''.join(chr(ch) for ch in range(33)),)})) 845 846 847 def test_queryKeywordFlag(self): 848 """ 849 When passed the C{keyword} argument, L{imap4.Query} returns an 850 C{atom} that consists of one or more non-special characters. 851 852 List of the invalid characters: 853 854 ( ) { % * " \ ] CTL SP 855 856 @see: U{ABNF definition of CTL and SP<https://tools.ietf.org/html/rfc2234>} 857 @see: U{IMAP4 grammar<http://tools.ietf.org/html/rfc3501#section-9>} 858 @see: U{IMAP4 SEARCH specification<http://tools.ietf.org/html/rfc3501#section-6.4.4>} 859 """ 860 self._keywordFilteringTest("keyword") 861 862 863 def test_queryUnkeywordFlag(self): 864 """ 865 When passed the C{unkeyword} argument, L{imap4.Query} returns an 866 C{atom} that consists of one or more non-special characters. 867 868 List of the invalid characters: 869 870 ( ) { % * " \ ] CTL SP 871 872 @see: U{ABNF definition of CTL and SP<https://tools.ietf.org/html/rfc2234>} 873 @see: U{IMAP4 grammar<http://tools.ietf.org/html/rfc3501#section-9>} 874 @see: U{IMAP4 SEARCH specification<http://tools.ietf.org/html/rfc3501#section-6.4.4>} 875 """ 876 self._keywordFilteringTest("unkeyword") 877 878 879 def test_invalidIdListParser(self): 880 """ 881 Trying to parse an invalid representation of a sequence range raises an 882 L{IllegalIdentifierError}. 883 """ 884 inputs = [ 885 '*:*', 886 'foo', 887 '4:', 888 'bar:5' 889 ] 890 891 for input in inputs: 892 self.assertRaises(imap4.IllegalIdentifierError, 893 imap4.parseIdList, input, 12345) 894 895 896 def test_invalidIdListParserNonPositive(self): 897 """ 898 Zeroes and negative values are not accepted in id range expressions. RFC 899 3501 states that sequence numbers and sequence ranges consist of 900 non-negative numbers (RFC 3501 section 9, the seq-number grammar item). 901 """ 902 inputs = [ 903 '0:5', 904 '0:0', 905 '*:0', 906 '0', 907 '-3:5', 908 '1:-2', 909 '-1' 910 ] 911 912 for input in inputs: 913 self.assertRaises(imap4.IllegalIdentifierError, 914 imap4.parseIdList, input, 12345) 915 916 917 def test_parseIdList(self): 918 """ 919 The function to parse sequence ranges yields appropriate L{MessageSet} 920 objects. 921 """ 922 inputs = [ 923 '1:*', 924 '5:*', 925 '1:2,5:*', 926 '*', 927 '1', 928 '1,2', 929 '1,3,5', 930 '1:10', 931 '1:10,11', 932 '1:5,10:20', 933 '1,5:10', 934 '1,5:10,15:20', 935 '1:10,15,20:25', 936 '4:2' 937 ] 938 939 outputs = [ 940 MessageSet(1, None), 941 MessageSet(5, None), 942 MessageSet(5, None) + MessageSet(1, 2), 943 MessageSet(None, None), 944 MessageSet(1), 945 MessageSet(1, 2), 946 MessageSet(1) + MessageSet(3) + MessageSet(5), 947 MessageSet(1, 10), 948 MessageSet(1, 11), 949 MessageSet(1, 5) + MessageSet(10, 20), 950 MessageSet(1) + MessageSet(5, 10), 951 MessageSet(1) + MessageSet(5, 10) + MessageSet(15, 20), 952 MessageSet(1, 10) + MessageSet(15) + MessageSet(20, 25), 953 MessageSet(2, 4), 954 ] 955 956 lengths = [ 957 None, None, None, 958 1, 1, 2, 3, 10, 11, 16, 7, 13, 17, 3 959 ] 960 961 for (input, expected) in zip(inputs, outputs): 962 self.assertEqual(imap4.parseIdList(input), expected) 963 964 for (input, expected) in zip(inputs, lengths): 965 if expected is None: 966 self.assertRaises(TypeError, len, imap4.parseIdList(input)) 967 else: 968 L = len(imap4.parseIdList(input)) 969 self.assertEqual(L, expected, 970 "len(%r) = %r != %r" % (input, L, expected)) 971 972class SimpleMailbox: 973 implements(imap4.IMailboxInfo, imap4.IMailbox, imap4.ICloseableMailbox) 974 975 flags = ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag') 976 messages = [] 977 mUID = 0 978 rw = 1 979 closed = False 980 981 def __init__(self): 982 self.listeners = [] 983 self.addListener = self.listeners.append 984 self.removeListener = self.listeners.remove 985 986 def getFlags(self): 987 return self.flags 988 989 def getUIDValidity(self): 990 return 42 991 992 def getUIDNext(self): 993 return len(self.messages) + 1 994 995 def getMessageCount(self): 996 return 9 997 998 def getRecentCount(self): 999 return 3 1000 1001 def getUnseenCount(self): 1002 return 4 1003 1004 def isWriteable(self): 1005 return self.rw 1006 1007 def destroy(self): 1008 pass 1009 1010 def getHierarchicalDelimiter(self): 1011 return '/' 1012 1013 def requestStatus(self, names): 1014 r = {} 1015 if 'MESSAGES' in names: 1016 r['MESSAGES'] = self.getMessageCount() 1017 if 'RECENT' in names: 1018 r['RECENT'] = self.getRecentCount() 1019 if 'UIDNEXT' in names: 1020 r['UIDNEXT'] = self.getMessageCount() + 1 1021 if 'UIDVALIDITY' in names: 1022 r['UIDVALIDITY'] = self.getUID() 1023 if 'UNSEEN' in names: 1024 r['UNSEEN'] = self.getUnseenCount() 1025 return defer.succeed(r) 1026 1027 def addMessage(self, message, flags, date = None): 1028 self.messages.append((message, flags, date, self.mUID)) 1029 self.mUID += 1 1030 return defer.succeed(None) 1031 1032 def expunge(self): 1033 delete = [] 1034 for i in self.messages: 1035 if '\\Deleted' in i[1]: 1036 delete.append(i) 1037 for i in delete: 1038 self.messages.remove(i) 1039 return [i[3] for i in delete] 1040 1041 def close(self): 1042 self.closed = True 1043 1044class Account(imap4.MemoryAccount): 1045 mailboxFactory = SimpleMailbox 1046 def _emptyMailbox(self, name, id): 1047 return self.mailboxFactory() 1048 1049 def select(self, name, rw=1): 1050 mbox = imap4.MemoryAccount.select(self, name) 1051 if mbox is not None: 1052 mbox.rw = rw 1053 return mbox 1054 1055class SimpleServer(imap4.IMAP4Server): 1056 def __init__(self, *args, **kw): 1057 imap4.IMAP4Server.__init__(self, *args, **kw) 1058 realm = TestRealm() 1059 realm.theAccount = Account('testuser') 1060 portal = cred.portal.Portal(realm) 1061 c = cred.checkers.InMemoryUsernamePasswordDatabaseDontUse() 1062 self.checker = c 1063 self.portal = portal 1064 portal.registerChecker(c) 1065 self.timeoutTest = False 1066 1067 def lineReceived(self, line): 1068 if self.timeoutTest: 1069 #Do not send a respones 1070 return 1071 1072 imap4.IMAP4Server.lineReceived(self, line) 1073 1074 _username = 'testuser' 1075 _password = 'password-test' 1076 def authenticateLogin(self, username, password): 1077 if username == self._username and password == self._password: 1078 return imap4.IAccount, self.theAccount, lambda: None 1079 raise cred.error.UnauthorizedLogin() 1080 1081 1082class SimpleClient(imap4.IMAP4Client): 1083 def __init__(self, deferred, contextFactory = None): 1084 imap4.IMAP4Client.__init__(self, contextFactory) 1085 self.deferred = deferred 1086 self.events = [] 1087 1088 def serverGreeting(self, caps): 1089 self.deferred.callback(None) 1090 1091 def modeChanged(self, writeable): 1092 self.events.append(['modeChanged', writeable]) 1093 self.transport.loseConnection() 1094 1095 def flagsChanged(self, newFlags): 1096 self.events.append(['flagsChanged', newFlags]) 1097 self.transport.loseConnection() 1098 1099 def newMessages(self, exists, recent): 1100 self.events.append(['newMessages', exists, recent]) 1101 self.transport.loseConnection() 1102 1103 1104 1105class IMAP4HelperMixin: 1106 1107 serverCTX = None 1108 clientCTX = None 1109 1110 def setUp(self): 1111 d = defer.Deferred() 1112 self.server = SimpleServer(contextFactory=self.serverCTX) 1113 self.client = SimpleClient(d, contextFactory=self.clientCTX) 1114 self.connected = d 1115 1116 SimpleMailbox.messages = [] 1117 theAccount = Account('testuser') 1118 theAccount.mboxType = SimpleMailbox 1119 SimpleServer.theAccount = theAccount 1120 1121 1122 def tearDown(self): 1123 del self.server 1124 del self.client 1125 del self.connected 1126 1127 1128 def _cbStopClient(self, ignore): 1129 self.client.transport.loseConnection() 1130 1131 1132 def _ebGeneral(self, failure): 1133 self.client.transport.loseConnection() 1134 self.server.transport.loseConnection() 1135 log.err(failure, "Problem with %r" % (self.function,)) 1136 1137 1138 def loopback(self): 1139 return loopback.loopbackAsync(self.server, self.client) 1140 1141 1142 1143class IMAP4ServerTestCase(IMAP4HelperMixin, unittest.TestCase): 1144 def testCapability(self): 1145 caps = {} 1146 def getCaps(): 1147 def gotCaps(c): 1148 caps.update(c) 1149 self.server.transport.loseConnection() 1150 return self.client.getCapabilities().addCallback(gotCaps) 1151 d1 = self.connected.addCallback(strip(getCaps)).addErrback(self._ebGeneral) 1152 d = defer.gatherResults([self.loopback(), d1]) 1153 expected = {'IMAP4rev1': None, 'NAMESPACE': None, 'IDLE': None} 1154 return d.addCallback(lambda _: self.assertEqual(expected, caps)) 1155 1156 def testCapabilityWithAuth(self): 1157 caps = {} 1158 self.server.challengers['CRAM-MD5'] = cred.credentials.CramMD5Credentials 1159 def getCaps(): 1160 def gotCaps(c): 1161 caps.update(c) 1162 self.server.transport.loseConnection() 1163 return self.client.getCapabilities().addCallback(gotCaps) 1164 d1 = self.connected.addCallback(strip(getCaps)).addErrback(self._ebGeneral) 1165 d = defer.gatherResults([self.loopback(), d1]) 1166 1167 expCap = {'IMAP4rev1': None, 'NAMESPACE': None, 1168 'IDLE': None, 'AUTH': ['CRAM-MD5']} 1169 1170 return d.addCallback(lambda _: self.assertEqual(expCap, caps)) 1171 1172 def testLogout(self): 1173 self.loggedOut = 0 1174 def logout(): 1175 def setLoggedOut(): 1176 self.loggedOut = 1 1177 self.client.logout().addCallback(strip(setLoggedOut)) 1178 self.connected.addCallback(strip(logout)).addErrback(self._ebGeneral) 1179 d = self.loopback() 1180 return d.addCallback(lambda _: self.assertEqual(self.loggedOut, 1)) 1181 1182 def testNoop(self): 1183 self.responses = None 1184 def noop(): 1185 def setResponses(responses): 1186 self.responses = responses 1187 self.server.transport.loseConnection() 1188 self.client.noop().addCallback(setResponses) 1189 self.connected.addCallback(strip(noop)).addErrback(self._ebGeneral) 1190 d = self.loopback() 1191 return d.addCallback(lambda _: self.assertEqual(self.responses, [])) 1192 1193 def testLogin(self): 1194 def login(): 1195 d = self.client.login('testuser', 'password-test') 1196 d.addCallback(self._cbStopClient) 1197 d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) 1198 d = defer.gatherResults([d1, self.loopback()]) 1199 return d.addCallback(self._cbTestLogin) 1200 1201 def _cbTestLogin(self, ignored): 1202 self.assertEqual(self.server.account, SimpleServer.theAccount) 1203 self.assertEqual(self.server.state, 'auth') 1204 1205 def testFailedLogin(self): 1206 def login(): 1207 d = self.client.login('testuser', 'wrong-password') 1208 d.addBoth(self._cbStopClient) 1209 1210 d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) 1211 d2 = self.loopback() 1212 d = defer.gatherResults([d1, d2]) 1213 return d.addCallback(self._cbTestFailedLogin) 1214 1215 def _cbTestFailedLogin(self, ignored): 1216 self.assertEqual(self.server.account, None) 1217 self.assertEqual(self.server.state, 'unauth') 1218 1219 1220 def testLoginRequiringQuoting(self): 1221 self.server._username = '{test}user' 1222 self.server._password = '{test}password' 1223 1224 def login(): 1225 d = self.client.login('{test}user', '{test}password') 1226 d.addBoth(self._cbStopClient) 1227 1228 d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) 1229 d = defer.gatherResults([self.loopback(), d1]) 1230 return d.addCallback(self._cbTestLoginRequiringQuoting) 1231 1232 def _cbTestLoginRequiringQuoting(self, ignored): 1233 self.assertEqual(self.server.account, SimpleServer.theAccount) 1234 self.assertEqual(self.server.state, 'auth') 1235 1236 1237 def testNamespace(self): 1238 self.namespaceArgs = None 1239 def login(): 1240 return self.client.login('testuser', 'password-test') 1241 def namespace(): 1242 def gotNamespace(args): 1243 self.namespaceArgs = args 1244 self._cbStopClient(None) 1245 return self.client.namespace().addCallback(gotNamespace) 1246 1247 d1 = self.connected.addCallback(strip(login)) 1248 d1.addCallback(strip(namespace)) 1249 d1.addErrback(self._ebGeneral) 1250 d2 = self.loopback() 1251 d = defer.gatherResults([d1, d2]) 1252 d.addCallback(lambda _: self.assertEqual(self.namespaceArgs, 1253 [[['', '/']], [], []])) 1254 return d 1255 1256 def testSelect(self): 1257 SimpleServer.theAccount.addMailbox('test-mailbox') 1258 self.selectedArgs = None 1259 def login(): 1260 return self.client.login('testuser', 'password-test') 1261 def select(): 1262 def selected(args): 1263 self.selectedArgs = args 1264 self._cbStopClient(None) 1265 d = self.client.select('test-mailbox') 1266 d.addCallback(selected) 1267 return d 1268 1269 d1 = self.connected.addCallback(strip(login)) 1270 d1.addCallback(strip(select)) 1271 d1.addErrback(self._ebGeneral) 1272 d2 = self.loopback() 1273 return defer.gatherResults([d1, d2]).addCallback(self._cbTestSelect) 1274 1275 def _cbTestSelect(self, ignored): 1276 mbox = SimpleServer.theAccount.mailboxes['TEST-MAILBOX'] 1277 self.assertEqual(self.server.mbox, mbox) 1278 self.assertEqual(self.selectedArgs, { 1279 'EXISTS': 9, 'RECENT': 3, 'UIDVALIDITY': 42, 1280 'FLAGS': ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag'), 1281 'READ-WRITE': 1 1282 }) 1283 1284 1285 def test_examine(self): 1286 """ 1287 L{IMAP4Client.examine} issues an I{EXAMINE} command to the server and 1288 returns a L{Deferred} which fires with a C{dict} with as many of the 1289 following keys as the server includes in its response: C{'FLAGS'}, 1290 C{'EXISTS'}, C{'RECENT'}, C{'UNSEEN'}, C{'READ-WRITE'}, C{'READ-ONLY'}, 1291 C{'UIDVALIDITY'}, and C{'PERMANENTFLAGS'}. 1292 1293 Unfortunately the server doesn't generate all of these so it's hard to 1294 test the client's handling of them here. See 1295 L{IMAP4ClientExamineTests} below. 1296 1297 See U{RFC 3501<http://www.faqs.org/rfcs/rfc3501.html>}, section 6.3.2, 1298 for details. 1299 """ 1300 SimpleServer.theAccount.addMailbox('test-mailbox') 1301 self.examinedArgs = None 1302 def login(): 1303 return self.client.login('testuser', 'password-test') 1304 def examine(): 1305 def examined(args): 1306 self.examinedArgs = args 1307 self._cbStopClient(None) 1308 d = self.client.examine('test-mailbox') 1309 d.addCallback(examined) 1310 return d 1311 1312 d1 = self.connected.addCallback(strip(login)) 1313 d1.addCallback(strip(examine)) 1314 d1.addErrback(self._ebGeneral) 1315 d2 = self.loopback() 1316 d = defer.gatherResults([d1, d2]) 1317 return d.addCallback(self._cbTestExamine) 1318 1319 1320 def _cbTestExamine(self, ignored): 1321 mbox = SimpleServer.theAccount.mailboxes['TEST-MAILBOX'] 1322 self.assertEqual(self.server.mbox, mbox) 1323 self.assertEqual(self.examinedArgs, { 1324 'EXISTS': 9, 'RECENT': 3, 'UIDVALIDITY': 42, 1325 'FLAGS': ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag'), 1326 'READ-WRITE': False}) 1327 1328 1329 def testCreate(self): 1330 succeed = ('testbox', 'test/box', 'test/', 'test/box/box', 'INBOX') 1331 fail = ('testbox', 'test/box') 1332 1333 def cb(): self.result.append(1) 1334 def eb(failure): self.result.append(0) 1335 1336 def login(): 1337 return self.client.login('testuser', 'password-test') 1338 def create(): 1339 for name in succeed + fail: 1340 d = self.client.create(name) 1341 d.addCallback(strip(cb)).addErrback(eb) 1342 d.addCallbacks(self._cbStopClient, self._ebGeneral) 1343 1344 self.result = [] 1345 d1 = self.connected.addCallback(strip(login)).addCallback(strip(create)) 1346 d2 = self.loopback() 1347 d = defer.gatherResults([d1, d2]) 1348 return d.addCallback(self._cbTestCreate, succeed, fail) 1349 1350 def _cbTestCreate(self, ignored, succeed, fail): 1351 self.assertEqual(self.result, [1] * len(succeed) + [0] * len(fail)) 1352 mbox = SimpleServer.theAccount.mailboxes.keys() 1353 answers = ['inbox', 'testbox', 'test/box', 'test', 'test/box/box'] 1354 mbox.sort() 1355 answers.sort() 1356 self.assertEqual(mbox, [a.upper() for a in answers]) 1357 1358 def testDelete(self): 1359 SimpleServer.theAccount.addMailbox('delete/me') 1360 1361 def login(): 1362 return self.client.login('testuser', 'password-test') 1363 def delete(): 1364 return self.client.delete('delete/me') 1365 d1 = self.connected.addCallback(strip(login)) 1366 d1.addCallbacks(strip(delete), self._ebGeneral) 1367 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1368 d2 = self.loopback() 1369 d = defer.gatherResults([d1, d2]) 1370 d.addCallback(lambda _: 1371 self.assertEqual(SimpleServer.theAccount.mailboxes.keys(), [])) 1372 return d 1373 1374 def testIllegalInboxDelete(self): 1375 self.stashed = None 1376 def login(): 1377 return self.client.login('testuser', 'password-test') 1378 def delete(): 1379 return self.client.delete('inbox') 1380 def stash(result): 1381 self.stashed = result 1382 1383 d1 = self.connected.addCallback(strip(login)) 1384 d1.addCallbacks(strip(delete), self._ebGeneral) 1385 d1.addBoth(stash) 1386 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1387 d2 = self.loopback() 1388 d = defer.gatherResults([d1, d2]) 1389 d.addCallback(lambda _: self.failUnless(isinstance(self.stashed, 1390 failure.Failure))) 1391 return d 1392 1393 1394 def testNonExistentDelete(self): 1395 def login(): 1396 return self.client.login('testuser', 'password-test') 1397 def delete(): 1398 return self.client.delete('delete/me') 1399 def deleteFailed(failure): 1400 self.failure = failure 1401 1402 self.failure = None 1403 d1 = self.connected.addCallback(strip(login)) 1404 d1.addCallback(strip(delete)).addErrback(deleteFailed) 1405 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1406 d2 = self.loopback() 1407 d = defer.gatherResults([d1, d2]) 1408 d.addCallback(lambda _: self.assertEqual(str(self.failure.value), 1409 'No such mailbox')) 1410 return d 1411 1412 1413 def testIllegalDelete(self): 1414 m = SimpleMailbox() 1415 m.flags = (r'\Noselect',) 1416 SimpleServer.theAccount.addMailbox('delete', m) 1417 SimpleServer.theAccount.addMailbox('delete/me') 1418 1419 def login(): 1420 return self.client.login('testuser', 'password-test') 1421 def delete(): 1422 return self.client.delete('delete') 1423 def deleteFailed(failure): 1424 self.failure = failure 1425 1426 self.failure = None 1427 d1 = self.connected.addCallback(strip(login)) 1428 d1.addCallback(strip(delete)).addErrback(deleteFailed) 1429 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1430 d2 = self.loopback() 1431 d = defer.gatherResults([d1, d2]) 1432 expected = "Hierarchically inferior mailboxes exist and \\Noselect is set" 1433 d.addCallback(lambda _: 1434 self.assertEqual(str(self.failure.value), expected)) 1435 return d 1436 1437 def testRename(self): 1438 SimpleServer.theAccount.addMailbox('oldmbox') 1439 def login(): 1440 return self.client.login('testuser', 'password-test') 1441 def rename(): 1442 return self.client.rename('oldmbox', 'newname') 1443 1444 d1 = self.connected.addCallback(strip(login)) 1445 d1.addCallbacks(strip(rename), self._ebGeneral) 1446 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1447 d2 = self.loopback() 1448 d = defer.gatherResults([d1, d2]) 1449 d.addCallback(lambda _: 1450 self.assertEqual(SimpleServer.theAccount.mailboxes.keys(), 1451 ['NEWNAME'])) 1452 return d 1453 1454 def testIllegalInboxRename(self): 1455 self.stashed = None 1456 def login(): 1457 return self.client.login('testuser', 'password-test') 1458 def rename(): 1459 return self.client.rename('inbox', 'frotz') 1460 def stash(stuff): 1461 self.stashed = stuff 1462 1463 d1 = self.connected.addCallback(strip(login)) 1464 d1.addCallbacks(strip(rename), self._ebGeneral) 1465 d1.addBoth(stash) 1466 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1467 d2 = self.loopback() 1468 d = defer.gatherResults([d1, d2]) 1469 d.addCallback(lambda _: 1470 self.failUnless(isinstance(self.stashed, failure.Failure))) 1471 return d 1472 1473 def testHierarchicalRename(self): 1474 SimpleServer.theAccount.create('oldmbox/m1') 1475 SimpleServer.theAccount.create('oldmbox/m2') 1476 def login(): 1477 return self.client.login('testuser', 'password-test') 1478 def rename(): 1479 return self.client.rename('oldmbox', 'newname') 1480 1481 d1 = self.connected.addCallback(strip(login)) 1482 d1.addCallbacks(strip(rename), self._ebGeneral) 1483 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1484 d2 = self.loopback() 1485 d = defer.gatherResults([d1, d2]) 1486 return d.addCallback(self._cbTestHierarchicalRename) 1487 1488 def _cbTestHierarchicalRename(self, ignored): 1489 mboxes = SimpleServer.theAccount.mailboxes.keys() 1490 expected = ['newname', 'newname/m1', 'newname/m2'] 1491 mboxes.sort() 1492 self.assertEqual(mboxes, [s.upper() for s in expected]) 1493 1494 def testSubscribe(self): 1495 def login(): 1496 return self.client.login('testuser', 'password-test') 1497 def subscribe(): 1498 return self.client.subscribe('this/mbox') 1499 1500 d1 = self.connected.addCallback(strip(login)) 1501 d1.addCallbacks(strip(subscribe), self._ebGeneral) 1502 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1503 d2 = self.loopback() 1504 d = defer.gatherResults([d1, d2]) 1505 d.addCallback(lambda _: 1506 self.assertEqual(SimpleServer.theAccount.subscriptions, 1507 ['THIS/MBOX'])) 1508 return d 1509 1510 def testUnsubscribe(self): 1511 SimpleServer.theAccount.subscriptions = ['THIS/MBOX', 'THAT/MBOX'] 1512 def login(): 1513 return self.client.login('testuser', 'password-test') 1514 def unsubscribe(): 1515 return self.client.unsubscribe('this/mbox') 1516 1517 d1 = self.connected.addCallback(strip(login)) 1518 d1.addCallbacks(strip(unsubscribe), self._ebGeneral) 1519 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1520 d2 = self.loopback() 1521 d = defer.gatherResults([d1, d2]) 1522 d.addCallback(lambda _: 1523 self.assertEqual(SimpleServer.theAccount.subscriptions, 1524 ['THAT/MBOX'])) 1525 return d 1526 1527 def _listSetup(self, f): 1528 SimpleServer.theAccount.addMailbox('root/subthing') 1529 SimpleServer.theAccount.addMailbox('root/another-thing') 1530 SimpleServer.theAccount.addMailbox('non-root/subthing') 1531 1532 def login(): 1533 return self.client.login('testuser', 'password-test') 1534 def listed(answers): 1535 self.listed = answers 1536 1537 self.listed = None 1538 d1 = self.connected.addCallback(strip(login)) 1539 d1.addCallbacks(strip(f), self._ebGeneral) 1540 d1.addCallbacks(listed, self._ebGeneral) 1541 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1542 d2 = self.loopback() 1543 return defer.gatherResults([d1, d2]).addCallback(lambda _: self.listed) 1544 1545 def testList(self): 1546 def list(): 1547 return self.client.list('root', '%') 1548 d = self._listSetup(list) 1549 d.addCallback(lambda listed: self.assertEqual( 1550 sortNest(listed), 1551 sortNest([ 1552 (SimpleMailbox.flags, "/", "ROOT/SUBTHING"), 1553 (SimpleMailbox.flags, "/", "ROOT/ANOTHER-THING") 1554 ]) 1555 )) 1556 return d 1557 1558 def testLSub(self): 1559 SimpleServer.theAccount.subscribe('ROOT/SUBTHING') 1560 def lsub(): 1561 return self.client.lsub('root', '%') 1562 d = self._listSetup(lsub) 1563 d.addCallback(self.assertEqual, 1564 [(SimpleMailbox.flags, "/", "ROOT/SUBTHING")]) 1565 return d 1566 1567 def testStatus(self): 1568 SimpleServer.theAccount.addMailbox('root/subthing') 1569 def login(): 1570 return self.client.login('testuser', 'password-test') 1571 def status(): 1572 return self.client.status('root/subthing', 'MESSAGES', 'UIDNEXT', 'UNSEEN') 1573 def statused(result): 1574 self.statused = result 1575 1576 self.statused = None 1577 d1 = self.connected.addCallback(strip(login)) 1578 d1.addCallbacks(strip(status), self._ebGeneral) 1579 d1.addCallbacks(statused, self._ebGeneral) 1580 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1581 d2 = self.loopback() 1582 d = defer.gatherResults([d1, d2]) 1583 d.addCallback(lambda _: self.assertEqual( 1584 self.statused, 1585 {'MESSAGES': 9, 'UIDNEXT': '10', 'UNSEEN': 4} 1586 )) 1587 return d 1588 1589 def testFailedStatus(self): 1590 def login(): 1591 return self.client.login('testuser', 'password-test') 1592 def status(): 1593 return self.client.status('root/nonexistent', 'MESSAGES', 'UIDNEXT', 'UNSEEN') 1594 def statused(result): 1595 self.statused = result 1596 def failed(failure): 1597 self.failure = failure 1598 1599 self.statused = self.failure = None 1600 d1 = self.connected.addCallback(strip(login)) 1601 d1.addCallbacks(strip(status), self._ebGeneral) 1602 d1.addCallbacks(statused, failed) 1603 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1604 d2 = self.loopback() 1605 return defer.gatherResults([d1, d2]).addCallback(self._cbTestFailedStatus) 1606 1607 def _cbTestFailedStatus(self, ignored): 1608 self.assertEqual( 1609 self.statused, None 1610 ) 1611 self.assertEqual( 1612 self.failure.value.args, 1613 ('Could not open mailbox',) 1614 ) 1615 1616 def testFullAppend(self): 1617 infile = util.sibpath(__file__, 'rfc822.message') 1618 message = open(infile) 1619 SimpleServer.theAccount.addMailbox('root/subthing') 1620 def login(): 1621 return self.client.login('testuser', 'password-test') 1622 def append(): 1623 return self.client.append( 1624 'root/subthing', 1625 message, 1626 ('\\SEEN', '\\DELETED'), 1627 'Tue, 17 Jun 2003 11:22:16 -0600 (MDT)', 1628 ) 1629 1630 d1 = self.connected.addCallback(strip(login)) 1631 d1.addCallbacks(strip(append), self._ebGeneral) 1632 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1633 d2 = self.loopback() 1634 d = defer.gatherResults([d1, d2]) 1635 return d.addCallback(self._cbTestFullAppend, infile) 1636 1637 def _cbTestFullAppend(self, ignored, infile): 1638 mb = SimpleServer.theAccount.mailboxes['ROOT/SUBTHING'] 1639 self.assertEqual(1, len(mb.messages)) 1640 self.assertEqual( 1641 (['\\SEEN', '\\DELETED'], 'Tue, 17 Jun 2003 11:22:16 -0600 (MDT)', 0), 1642 mb.messages[0][1:] 1643 ) 1644 self.assertEqual(open(infile).read(), mb.messages[0][0].getvalue()) 1645 1646 def testPartialAppend(self): 1647 infile = util.sibpath(__file__, 'rfc822.message') 1648 message = open(infile) 1649 SimpleServer.theAccount.addMailbox('PARTIAL/SUBTHING') 1650 def login(): 1651 return self.client.login('testuser', 'password-test') 1652 def append(): 1653 message = file(infile) 1654 return self.client.sendCommand( 1655 imap4.Command( 1656 'APPEND', 1657 'PARTIAL/SUBTHING (\\SEEN) "Right now" {%d}' % os.path.getsize(infile), 1658 (), self.client._IMAP4Client__cbContinueAppend, message 1659 ) 1660 ) 1661 d1 = self.connected.addCallback(strip(login)) 1662 d1.addCallbacks(strip(append), self._ebGeneral) 1663 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1664 d2 = self.loopback() 1665 d = defer.gatherResults([d1, d2]) 1666 return d.addCallback(self._cbTestPartialAppend, infile) 1667 1668 def _cbTestPartialAppend(self, ignored, infile): 1669 mb = SimpleServer.theAccount.mailboxes['PARTIAL/SUBTHING'] 1670 self.assertEqual(1, len(mb.messages)) 1671 self.assertEqual( 1672 (['\\SEEN'], 'Right now', 0), 1673 mb.messages[0][1:] 1674 ) 1675 self.assertEqual(open(infile).read(), mb.messages[0][0].getvalue()) 1676 1677 def testCheck(self): 1678 SimpleServer.theAccount.addMailbox('root/subthing') 1679 def login(): 1680 return self.client.login('testuser', 'password-test') 1681 def select(): 1682 return self.client.select('root/subthing') 1683 def check(): 1684 return self.client.check() 1685 1686 d = self.connected.addCallback(strip(login)) 1687 d.addCallbacks(strip(select), self._ebGeneral) 1688 d.addCallbacks(strip(check), self._ebGeneral) 1689 d.addCallbacks(self._cbStopClient, self._ebGeneral) 1690 return self.loopback() 1691 1692 # Okay, that was fun 1693 1694 def testClose(self): 1695 m = SimpleMailbox() 1696 m.messages = [ 1697 ('Message 1', ('\\Deleted', 'AnotherFlag'), None, 0), 1698 ('Message 2', ('AnotherFlag',), None, 1), 1699 ('Message 3', ('\\Deleted',), None, 2), 1700 ] 1701 SimpleServer.theAccount.addMailbox('mailbox', m) 1702 def login(): 1703 return self.client.login('testuser', 'password-test') 1704 def select(): 1705 return self.client.select('mailbox') 1706 def close(): 1707 return self.client.close() 1708 1709 d = self.connected.addCallback(strip(login)) 1710 d.addCallbacks(strip(select), self._ebGeneral) 1711 d.addCallbacks(strip(close), self._ebGeneral) 1712 d.addCallbacks(self._cbStopClient, self._ebGeneral) 1713 d2 = self.loopback() 1714 return defer.gatherResults([d, d2]).addCallback(self._cbTestClose, m) 1715 1716 def _cbTestClose(self, ignored, m): 1717 self.assertEqual(len(m.messages), 1) 1718 self.assertEqual(m.messages[0], ('Message 2', ('AnotherFlag',), None, 1)) 1719 self.failUnless(m.closed) 1720 1721 def testExpunge(self): 1722 m = SimpleMailbox() 1723 m.messages = [ 1724 ('Message 1', ('\\Deleted', 'AnotherFlag'), None, 0), 1725 ('Message 2', ('AnotherFlag',), None, 1), 1726 ('Message 3', ('\\Deleted',), None, 2), 1727 ] 1728 SimpleServer.theAccount.addMailbox('mailbox', m) 1729 def login(): 1730 return self.client.login('testuser', 'password-test') 1731 def select(): 1732 return self.client.select('mailbox') 1733 def expunge(): 1734 return self.client.expunge() 1735 def expunged(results): 1736 self.failIf(self.server.mbox is None) 1737 self.results = results 1738 1739 self.results = None 1740 d1 = self.connected.addCallback(strip(login)) 1741 d1.addCallbacks(strip(select), self._ebGeneral) 1742 d1.addCallbacks(strip(expunge), self._ebGeneral) 1743 d1.addCallbacks(expunged, self._ebGeneral) 1744 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1745 d2 = self.loopback() 1746 d = defer.gatherResults([d1, d2]) 1747 return d.addCallback(self._cbTestExpunge, m) 1748 1749 def _cbTestExpunge(self, ignored, m): 1750 self.assertEqual(len(m.messages), 1) 1751 self.assertEqual(m.messages[0], ('Message 2', ('AnotherFlag',), None, 1)) 1752 1753 self.assertEqual(self.results, [0, 2]) 1754 1755 1756 1757class IMAP4ServerSearchTestCase(IMAP4HelperMixin, unittest.TestCase): 1758 """ 1759 Tests for the behavior of the search_* functions in L{imap4.IMAP4Server}. 1760 """ 1761 def setUp(self): 1762 IMAP4HelperMixin.setUp(self) 1763 self.earlierQuery = ["10-Dec-2009"] 1764 self.sameDateQuery = ["13-Dec-2009"] 1765 self.laterQuery = ["16-Dec-2009"] 1766 self.seq = 0 1767 self.msg = FakeyMessage({"date" : "Mon, 13 Dec 2009 21:25:10 GMT"}, [], 1768 '', '', 1234, None) 1769 1770 1771 def test_searchSentBefore(self): 1772 """ 1773 L{imap4.IMAP4Server.search_SENTBEFORE} returns True if the message date 1774 is earlier than the query date. 1775 """ 1776 self.assertFalse( 1777 self.server.search_SENTBEFORE(self.earlierQuery, self.seq, self.msg)) 1778 self.assertTrue( 1779 self.server.search_SENTBEFORE(self.laterQuery, self.seq, self.msg)) 1780 1781 def test_searchWildcard(self): 1782 """ 1783 L{imap4.IMAP4Server.search_UID} returns True if the message UID is in 1784 the search range. 1785 """ 1786 self.assertFalse( 1787 self.server.search_UID(['2:3'], self.seq, self.msg, (1, 1234))) 1788 # 2:* should get translated to 2:<max UID> and then to 1:2 1789 self.assertTrue( 1790 self.server.search_UID(['2:*'], self.seq, self.msg, (1, 1234))) 1791 self.assertTrue( 1792 self.server.search_UID(['*'], self.seq, self.msg, (1, 1234))) 1793 1794 def test_searchWildcardHigh(self): 1795 """ 1796 L{imap4.IMAP4Server.search_UID} should return True if there is a 1797 wildcard, because a wildcard means "highest UID in the mailbox". 1798 """ 1799 self.assertTrue( 1800 self.server.search_UID(['1235:*'], self.seq, self.msg, (1234, 1))) 1801 1802 def test_reversedSearchTerms(self): 1803 """ 1804 L{imap4.IMAP4Server.search_SENTON} returns True if the message date is 1805 the same as the query date. 1806 """ 1807 msgset = imap4.parseIdList('4:2') 1808 self.assertEqual(list(msgset), [2, 3, 4]) 1809 1810 def test_searchSentOn(self): 1811 """ 1812 L{imap4.IMAP4Server.search_SENTON} returns True if the message date is 1813 the same as the query date. 1814 """ 1815 self.assertFalse( 1816 self.server.search_SENTON(self.earlierQuery, self.seq, self.msg)) 1817 self.assertTrue( 1818 self.server.search_SENTON(self.sameDateQuery, self.seq, self.msg)) 1819 self.assertFalse( 1820 self.server.search_SENTON(self.laterQuery, self.seq, self.msg)) 1821 1822 1823 def test_searchSentSince(self): 1824 """ 1825 L{imap4.IMAP4Server.search_SENTSINCE} returns True if the message date 1826 is later than the query date. 1827 """ 1828 self.assertTrue( 1829 self.server.search_SENTSINCE(self.earlierQuery, self.seq, self.msg)) 1830 self.assertFalse( 1831 self.server.search_SENTSINCE(self.laterQuery, self.seq, self.msg)) 1832 1833 1834 def test_searchOr(self): 1835 """ 1836 L{imap4.IMAP4Server.search_OR} returns true if either of the two 1837 expressions supplied to it returns true and returns false if neither 1838 does. 1839 """ 1840 self.assertTrue( 1841 self.server.search_OR( 1842 ["SENTSINCE"] + self.earlierQuery + 1843 ["SENTSINCE"] + self.laterQuery, 1844 self.seq, self.msg, (None, None))) 1845 self.assertTrue( 1846 self.server.search_OR( 1847 ["SENTSINCE"] + self.laterQuery + 1848 ["SENTSINCE"] + self.earlierQuery, 1849 self.seq, self.msg, (None, None))) 1850 self.assertFalse( 1851 self.server.search_OR( 1852 ["SENTON"] + self.laterQuery + 1853 ["SENTSINCE"] + self.laterQuery, 1854 self.seq, self.msg, (None, None))) 1855 1856 1857 def test_searchNot(self): 1858 """ 1859 L{imap4.IMAP4Server.search_NOT} returns the negation of the result 1860 of the expression supplied to it. 1861 """ 1862 self.assertFalse(self.server.search_NOT( 1863 ["SENTSINCE"] + self.earlierQuery, self.seq, self.msg, 1864 (None, None))) 1865 self.assertTrue(self.server.search_NOT( 1866 ["SENTON"] + self.laterQuery, self.seq, self.msg, 1867 (None, None))) 1868 1869 1870 1871class TestRealm: 1872 theAccount = None 1873 1874 def requestAvatar(self, avatarId, mind, *interfaces): 1875 return imap4.IAccount, self.theAccount, lambda: None 1876 1877class TestChecker: 1878 credentialInterfaces = (cred.credentials.IUsernameHashedPassword, cred.credentials.IUsernamePassword) 1879 1880 users = { 1881 'testuser': 'secret' 1882 } 1883 1884 def requestAvatarId(self, credentials): 1885 if credentials.username in self.users: 1886 return defer.maybeDeferred( 1887 credentials.checkPassword, self.users[credentials.username] 1888 ).addCallback(self._cbCheck, credentials.username) 1889 1890 def _cbCheck(self, result, username): 1891 if result: 1892 return username 1893 raise cred.error.UnauthorizedLogin() 1894 1895class AuthenticatorTestCase(IMAP4HelperMixin, unittest.TestCase): 1896 def setUp(self): 1897 IMAP4HelperMixin.setUp(self) 1898 1899 realm = TestRealm() 1900 realm.theAccount = Account('testuser') 1901 portal = cred.portal.Portal(realm) 1902 portal.registerChecker(TestChecker()) 1903 self.server.portal = portal 1904 1905 self.authenticated = 0 1906 self.account = realm.theAccount 1907 1908 def testCramMD5(self): 1909 self.server.challengers['CRAM-MD5'] = cred.credentials.CramMD5Credentials 1910 cAuth = imap4.CramMD5ClientAuthenticator('testuser') 1911 self.client.registerAuthenticator(cAuth) 1912 1913 def auth(): 1914 return self.client.authenticate('secret') 1915 def authed(): 1916 self.authenticated = 1 1917 1918 d1 = self.connected.addCallback(strip(auth)) 1919 d1.addCallbacks(strip(authed), self._ebGeneral) 1920 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1921 d2 = self.loopback() 1922 d = defer.gatherResults([d1, d2]) 1923 return d.addCallback(self._cbTestCramMD5) 1924 1925 def _cbTestCramMD5(self, ignored): 1926 self.assertEqual(self.authenticated, 1) 1927 self.assertEqual(self.server.account, self.account) 1928 1929 def testFailedCramMD5(self): 1930 self.server.challengers['CRAM-MD5'] = cred.credentials.CramMD5Credentials 1931 cAuth = imap4.CramMD5ClientAuthenticator('testuser') 1932 self.client.registerAuthenticator(cAuth) 1933 1934 def misauth(): 1935 return self.client.authenticate('not the secret') 1936 def authed(): 1937 self.authenticated = 1 1938 def misauthed(): 1939 self.authenticated = -1 1940 1941 d1 = self.connected.addCallback(strip(misauth)) 1942 d1.addCallbacks(strip(authed), strip(misauthed)) 1943 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1944 d = defer.gatherResults([self.loopback(), d1]) 1945 return d.addCallback(self._cbTestFailedCramMD5) 1946 1947 def _cbTestFailedCramMD5(self, ignored): 1948 self.assertEqual(self.authenticated, -1) 1949 self.assertEqual(self.server.account, None) 1950 1951 def testLOGIN(self): 1952 self.server.challengers['LOGIN'] = imap4.LOGINCredentials 1953 cAuth = imap4.LOGINAuthenticator('testuser') 1954 self.client.registerAuthenticator(cAuth) 1955 1956 def auth(): 1957 return self.client.authenticate('secret') 1958 def authed(): 1959 self.authenticated = 1 1960 1961 d1 = self.connected.addCallback(strip(auth)) 1962 d1.addCallbacks(strip(authed), self._ebGeneral) 1963 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1964 d = defer.gatherResults([self.loopback(), d1]) 1965 return d.addCallback(self._cbTestLOGIN) 1966 1967 def _cbTestLOGIN(self, ignored): 1968 self.assertEqual(self.authenticated, 1) 1969 self.assertEqual(self.server.account, self.account) 1970 1971 def testFailedLOGIN(self): 1972 self.server.challengers['LOGIN'] = imap4.LOGINCredentials 1973 cAuth = imap4.LOGINAuthenticator('testuser') 1974 self.client.registerAuthenticator(cAuth) 1975 1976 def misauth(): 1977 return self.client.authenticate('not the secret') 1978 def authed(): 1979 self.authenticated = 1 1980 def misauthed(): 1981 self.authenticated = -1 1982 1983 d1 = self.connected.addCallback(strip(misauth)) 1984 d1.addCallbacks(strip(authed), strip(misauthed)) 1985 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 1986 d = defer.gatherResults([self.loopback(), d1]) 1987 return d.addCallback(self._cbTestFailedLOGIN) 1988 1989 def _cbTestFailedLOGIN(self, ignored): 1990 self.assertEqual(self.authenticated, -1) 1991 self.assertEqual(self.server.account, None) 1992 1993 def testPLAIN(self): 1994 self.server.challengers['PLAIN'] = imap4.PLAINCredentials 1995 cAuth = imap4.PLAINAuthenticator('testuser') 1996 self.client.registerAuthenticator(cAuth) 1997 1998 def auth(): 1999 return self.client.authenticate('secret') 2000 def authed(): 2001 self.authenticated = 1 2002 2003 d1 = self.connected.addCallback(strip(auth)) 2004 d1.addCallbacks(strip(authed), self._ebGeneral) 2005 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 2006 d = defer.gatherResults([self.loopback(), d1]) 2007 return d.addCallback(self._cbTestPLAIN) 2008 2009 def _cbTestPLAIN(self, ignored): 2010 self.assertEqual(self.authenticated, 1) 2011 self.assertEqual(self.server.account, self.account) 2012 2013 def testFailedPLAIN(self): 2014 self.server.challengers['PLAIN'] = imap4.PLAINCredentials 2015 cAuth = imap4.PLAINAuthenticator('testuser') 2016 self.client.registerAuthenticator(cAuth) 2017 2018 def misauth(): 2019 return self.client.authenticate('not the secret') 2020 def authed(): 2021 self.authenticated = 1 2022 def misauthed(): 2023 self.authenticated = -1 2024 2025 d1 = self.connected.addCallback(strip(misauth)) 2026 d1.addCallbacks(strip(authed), strip(misauthed)) 2027 d1.addCallbacks(self._cbStopClient, self._ebGeneral) 2028 d = defer.gatherResults([self.loopback(), d1]) 2029 return d.addCallback(self._cbTestFailedPLAIN) 2030 2031 def _cbTestFailedPLAIN(self, ignored): 2032 self.assertEqual(self.authenticated, -1) 2033 self.assertEqual(self.server.account, None) 2034 2035 2036 2037class SASLPLAINTestCase(unittest.TestCase): 2038 """ 2039 Tests for I{SASL PLAIN} authentication, as implemented by 2040 L{imap4.PLAINAuthenticator} and L{imap4.PLAINCredentials}. 2041 2042 @see: U{http://www.faqs.org/rfcs/rfc2595.html} 2043 @see: U{http://www.faqs.org/rfcs/rfc4616.html} 2044 """ 2045 def test_authenticatorChallengeResponse(self): 2046 """ 2047 L{PLAINAuthenticator.challengeResponse} returns challenge strings of 2048 the form:: 2049 2050 NUL<authn-id>NUL<secret> 2051 """ 2052 username = 'testuser' 2053 secret = 'secret' 2054 chal = 'challenge' 2055 cAuth = imap4.PLAINAuthenticator(username) 2056 response = cAuth.challengeResponse(secret, chal) 2057 self.assertEqual(response, '\0%s\0%s' % (username, secret)) 2058 2059 2060 def test_credentialsSetResponse(self): 2061 """ 2062 L{PLAINCredentials.setResponse} parses challenge strings of the 2063 form:: 2064 2065 NUL<authn-id>NUL<secret> 2066 """ 2067 cred = imap4.PLAINCredentials() 2068 cred.setResponse('\0testuser\0secret') 2069 self.assertEqual(cred.username, 'testuser') 2070 self.assertEqual(cred.password, 'secret') 2071 2072 2073 def test_credentialsInvalidResponse(self): 2074 """ 2075 L{PLAINCredentials.setResponse} raises L{imap4.IllegalClientResponse} 2076 when passed a string not of the expected form. 2077 """ 2078 cred = imap4.PLAINCredentials() 2079 self.assertRaises( 2080 imap4.IllegalClientResponse, cred.setResponse, 'hello') 2081 self.assertRaises( 2082 imap4.IllegalClientResponse, cred.setResponse, 'hello\0world') 2083 self.assertRaises( 2084 imap4.IllegalClientResponse, cred.setResponse, 2085 'hello\0world\0Zoom!\0') 2086 2087 2088 2089class UnsolicitedResponseTestCase(IMAP4HelperMixin, unittest.TestCase): 2090 def testReadWrite(self): 2091 def login(): 2092 return self.client.login('testuser', 'password-test') 2093 def loggedIn(): 2094 self.server.modeChanged(1) 2095 2096 d1 = self.connected.addCallback(strip(login)) 2097 d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) 2098 d = defer.gatherResults([self.loopback(), d1]) 2099 return d.addCallback(self._cbTestReadWrite) 2100 2101 def _cbTestReadWrite(self, ignored): 2102 E = self.client.events 2103 self.assertEqual(E, [['modeChanged', 1]]) 2104 2105 def testReadOnly(self): 2106 def login(): 2107 return self.client.login('testuser', 'password-test') 2108 def loggedIn(): 2109 self.server.modeChanged(0) 2110 2111 d1 = self.connected.addCallback(strip(login)) 2112 d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) 2113 d = defer.gatherResults([self.loopback(), d1]) 2114 return d.addCallback(self._cbTestReadOnly) 2115 2116 def _cbTestReadOnly(self, ignored): 2117 E = self.client.events 2118 self.assertEqual(E, [['modeChanged', 0]]) 2119 2120 def testFlagChange(self): 2121 flags = { 2122 1: ['\\Answered', '\\Deleted'], 2123 5: [], 2124 10: ['\\Recent'] 2125 } 2126 def login(): 2127 return self.client.login('testuser', 'password-test') 2128 def loggedIn(): 2129 self.server.flagsChanged(flags) 2130 2131 d1 = self.connected.addCallback(strip(login)) 2132 d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) 2133 d = defer.gatherResults([self.loopback(), d1]) 2134 return d.addCallback(self._cbTestFlagChange, flags) 2135 2136 def _cbTestFlagChange(self, ignored, flags): 2137 E = self.client.events 2138 expect = [['flagsChanged', {x[0]: x[1]}] for x in flags.items()] 2139 E.sort() 2140 expect.sort() 2141 self.assertEqual(E, expect) 2142 2143 def testNewMessages(self): 2144 def login(): 2145 return self.client.login('testuser', 'password-test') 2146 def loggedIn(): 2147 self.server.newMessages(10, None) 2148 2149 d1 = self.connected.addCallback(strip(login)) 2150 d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) 2151 d = defer.gatherResults([self.loopback(), d1]) 2152 return d.addCallback(self._cbTestNewMessages) 2153 2154 def _cbTestNewMessages(self, ignored): 2155 E = self.client.events 2156 self.assertEqual(E, [['newMessages', 10, None]]) 2157 2158 def testNewRecentMessages(self): 2159 def login(): 2160 return self.client.login('testuser', 'password-test') 2161 def loggedIn(): 2162 self.server.newMessages(None, 10) 2163 2164 d1 = self.connected.addCallback(strip(login)) 2165 d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) 2166 d = defer.gatherResults([self.loopback(), d1]) 2167 return d.addCallback(self._cbTestNewRecentMessages) 2168 2169 def _cbTestNewRecentMessages(self, ignored): 2170 E = self.client.events 2171 self.assertEqual(E, [['newMessages', None, 10]]) 2172 2173 def testNewMessagesAndRecent(self): 2174 def login(): 2175 return self.client.login('testuser', 'password-test') 2176 def loggedIn(): 2177 self.server.newMessages(20, 10) 2178 2179 d1 = self.connected.addCallback(strip(login)) 2180 d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) 2181 d = defer.gatherResults([self.loopback(), d1]) 2182 return d.addCallback(self._cbTestNewMessagesAndRecent) 2183 2184 def _cbTestNewMessagesAndRecent(self, ignored): 2185 E = self.client.events 2186 self.assertEqual(E, [['newMessages', 20, None], ['newMessages', None, 10]]) 2187 2188 2189class ClientCapabilityTests(unittest.TestCase): 2190 """ 2191 Tests for issuance of the CAPABILITY command and handling of its response. 2192 """ 2193 def setUp(self): 2194 """ 2195 Create an L{imap4.IMAP4Client} connected to a L{StringTransport}. 2196 """ 2197 self.transport = StringTransport() 2198 self.protocol = imap4.IMAP4Client() 2199 self.protocol.makeConnection(self.transport) 2200 self.protocol.dataReceived('* OK [IMAP4rev1]\r\n') 2201 2202 2203 def test_simpleAtoms(self): 2204 """ 2205 A capability response consisting only of atoms without C{'='} in them 2206 should result in a dict mapping those atoms to C{None}. 2207 """ 2208 capabilitiesResult = self.protocol.getCapabilities(useCache=False) 2209 self.protocol.dataReceived('* CAPABILITY IMAP4rev1 LOGINDISABLED\r\n') 2210 self.protocol.dataReceived('0001 OK Capability completed.\r\n') 2211 def gotCapabilities(capabilities): 2212 self.assertEqual( 2213 capabilities, {'IMAP4rev1': None, 'LOGINDISABLED': None}) 2214 capabilitiesResult.addCallback(gotCapabilities) 2215 return capabilitiesResult 2216 2217 2218 def test_categoryAtoms(self): 2219 """ 2220 A capability response consisting of atoms including C{'='} should have 2221 those atoms split on that byte and have capabilities in the same 2222 category aggregated into lists in the resulting dictionary. 2223 2224 (n.b. - I made up the word "category atom"; the protocol has no notion 2225 of structure here, but rather allows each capability to define the 2226 semantics of its entry in the capability response in a freeform manner. 2227 If I had realized this earlier, the API for capabilities would look 2228 different. As it is, we can hope that no one defines any crazy 2229 semantics which are incompatible with this API, or try to figure out a 2230 better API when someone does. -exarkun) 2231 """ 2232 capabilitiesResult = self.protocol.getCapabilities(useCache=False) 2233 self.protocol.dataReceived('* CAPABILITY IMAP4rev1 AUTH=LOGIN AUTH=PLAIN\r\n') 2234 self.protocol.dataReceived('0001 OK Capability completed.\r\n') 2235 def gotCapabilities(capabilities): 2236 self.assertEqual( 2237 capabilities, {'IMAP4rev1': None, 'AUTH': ['LOGIN', 'PLAIN']}) 2238 capabilitiesResult.addCallback(gotCapabilities) 2239 return capabilitiesResult 2240 2241 2242 def test_mixedAtoms(self): 2243 """ 2244 A capability response consisting of both simple and category atoms of 2245 the same type should result in a list containing C{None} as well as the 2246 values for the category. 2247 """ 2248 capabilitiesResult = self.protocol.getCapabilities(useCache=False) 2249 # Exercise codepath for both orderings of =-having and =-missing 2250 # capabilities. 2251 self.protocol.dataReceived( 2252 '* CAPABILITY IMAP4rev1 FOO FOO=BAR BAR=FOO BAR\r\n') 2253 self.protocol.dataReceived('0001 OK Capability completed.\r\n') 2254 def gotCapabilities(capabilities): 2255 self.assertEqual(capabilities, {'IMAP4rev1': None, 2256 'FOO': [None, 'BAR'], 2257 'BAR': ['FOO', None]}) 2258 capabilitiesResult.addCallback(gotCapabilities) 2259 return capabilitiesResult 2260 2261 2262 2263class StillSimplerClient(imap4.IMAP4Client): 2264 """ 2265 An IMAP4 client which keeps track of unsolicited flag changes. 2266 """ 2267 def __init__(self): 2268 imap4.IMAP4Client.__init__(self) 2269 self.flags = {} 2270 2271 2272 def flagsChanged(self, newFlags): 2273 self.flags.update(newFlags) 2274 2275 2276 2277class HandCraftedTestCase(IMAP4HelperMixin, unittest.TestCase): 2278 def testTrailingLiteral(self): 2279 transport = StringTransport() 2280 c = imap4.IMAP4Client() 2281 c.makeConnection(transport) 2282 c.lineReceived('* OK [IMAP4rev1]') 2283 2284 def cbSelect(ignored): 2285 d = c.fetchMessage('1') 2286 c.dataReceived('* 1 FETCH (RFC822 {10}\r\n0123456789\r\n RFC822.SIZE 10)\r\n') 2287 c.dataReceived('0003 OK FETCH\r\n') 2288 return d 2289 2290 def cbLogin(ignored): 2291 d = c.select('inbox') 2292 c.lineReceived('0002 OK SELECT') 2293 d.addCallback(cbSelect) 2294 return d 2295 2296 d = c.login('blah', 'blah') 2297 c.dataReceived('0001 OK LOGIN\r\n') 2298 d.addCallback(cbLogin) 2299 return d 2300 2301 2302 def testPathelogicalScatteringOfLiterals(self): 2303 self.server.checker.addUser('testuser', 'password-test') 2304 transport = StringTransport() 2305 self.server.makeConnection(transport) 2306 2307 transport.clear() 2308 self.server.dataReceived("01 LOGIN {8}\r\n") 2309 self.assertEqual(transport.value(), "+ Ready for 8 octets of text\r\n") 2310 2311 transport.clear() 2312 self.server.dataReceived("testuser {13}\r\n") 2313 self.assertEqual(transport.value(), "+ Ready for 13 octets of text\r\n") 2314 2315 transport.clear() 2316 self.server.dataReceived("password-test\r\n") 2317 self.assertEqual(transport.value(), "01 OK LOGIN succeeded\r\n") 2318 self.assertEqual(self.server.state, 'auth') 2319 2320 self.server.connectionLost(error.ConnectionDone("Connection done.")) 2321 2322 2323 def test_unsolicitedResponseMixedWithSolicitedResponse(self): 2324 """ 2325 If unsolicited data is received along with solicited data in the 2326 response to a I{FETCH} command issued by L{IMAP4Client.fetchSpecific}, 2327 the unsolicited data is passed to the appropriate callback and not 2328 included in the result with wihch the L{Deferred} returned by 2329 L{IMAP4Client.fetchSpecific} fires. 2330 """ 2331 transport = StringTransport() 2332 c = StillSimplerClient() 2333 c.makeConnection(transport) 2334 c.lineReceived('* OK [IMAP4rev1]') 2335 2336 def login(): 2337 d = c.login('blah', 'blah') 2338 c.dataReceived('0001 OK LOGIN\r\n') 2339 return d 2340 def select(): 2341 d = c.select('inbox') 2342 c.lineReceived('0002 OK SELECT') 2343 return d 2344 def fetch(): 2345 d = c.fetchSpecific('1:*', 2346 headerType='HEADER.FIELDS', 2347 headerArgs=['SUBJECT']) 2348 c.dataReceived('* 1 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {38}\r\n') 2349 c.dataReceived('Subject: Suprise for your woman...\r\n') 2350 c.dataReceived('\r\n') 2351 c.dataReceived(')\r\n') 2352 c.dataReceived('* 1 FETCH (FLAGS (\Seen))\r\n') 2353 c.dataReceived('* 2 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {75}\r\n') 2354 c.dataReceived('Subject: What you been doing. Order your meds here . ,. handcuff madsen\r\n') 2355 c.dataReceived('\r\n') 2356 c.dataReceived(')\r\n') 2357 c.dataReceived('0003 OK FETCH completed\r\n') 2358 return d 2359 def test(res): 2360 self.assertEqual(res, { 2361 1: [['BODY', ['HEADER.FIELDS', ['SUBJECT']], 2362 'Subject: Suprise for your woman...\r\n\r\n']], 2363 2: [['BODY', ['HEADER.FIELDS', ['SUBJECT']], 2364 'Subject: What you been doing. Order your meds here . ,. handcuff madsen\r\n\r\n']] 2365 }) 2366 2367 self.assertEqual(c.flags, {1: ['\\Seen']}) 2368 2369 return login( 2370 ).addCallback(strip(select) 2371 ).addCallback(strip(fetch) 2372 ).addCallback(test) 2373 2374 2375 def test_literalWithoutPrecedingWhitespace(self): 2376 """ 2377 Literals should be recognized even when they are not preceded by 2378 whitespace. 2379 """ 2380 transport = StringTransport() 2381 protocol = imap4.IMAP4Client() 2382 2383 protocol.makeConnection(transport) 2384 protocol.lineReceived('* OK [IMAP4rev1]') 2385 2386 def login(): 2387 d = protocol.login('blah', 'blah') 2388 protocol.dataReceived('0001 OK LOGIN\r\n') 2389 return d 2390 def select(): 2391 d = protocol.select('inbox') 2392 protocol.lineReceived('0002 OK SELECT') 2393 return d 2394 def fetch(): 2395 d = protocol.fetchSpecific('1:*', 2396 headerType='HEADER.FIELDS', 2397 headerArgs=['SUBJECT']) 2398 protocol.dataReceived( 2399 '* 1 FETCH (BODY[HEADER.FIELDS ({7}\r\nSUBJECT)] "Hello")\r\n') 2400 protocol.dataReceived('0003 OK FETCH completed\r\n') 2401 return d 2402 def test(result): 2403 self.assertEqual( 2404 result, {1: [['BODY', ['HEADER.FIELDS', ['SUBJECT']], 'Hello']]}) 2405 2406 d = login() 2407 d.addCallback(strip(select)) 2408 d.addCallback(strip(fetch)) 2409 d.addCallback(test) 2410 return d 2411 2412 2413 def test_nonIntegerLiteralLength(self): 2414 """ 2415 If the server sends a literal length which cannot be parsed as an 2416 integer, L{IMAP4Client.lineReceived} should cause the protocol to be 2417 disconnected by raising L{imap4.IllegalServerResponse}. 2418 """ 2419 transport = StringTransport() 2420 protocol = imap4.IMAP4Client() 2421 2422 protocol.makeConnection(transport) 2423 protocol.lineReceived('* OK [IMAP4rev1]') 2424 2425 def login(): 2426 d = protocol.login('blah', 'blah') 2427 protocol.dataReceived('0001 OK LOGIN\r\n') 2428 return d 2429 def select(): 2430 d = protocol.select('inbox') 2431 protocol.lineReceived('0002 OK SELECT') 2432 return d 2433 def fetch(): 2434 d = protocol.fetchSpecific('1:*', 2435 headerType='HEADER.FIELDS', 2436 headerArgs=['SUBJECT']) 2437 self.assertRaises( 2438 imap4.IllegalServerResponse, 2439 protocol.dataReceived, 2440 '* 1 FETCH {xyz}\r\n...') 2441 d = login() 2442 d.addCallback(strip(select)) 2443 d.addCallback(strip(fetch)) 2444 return d 2445 2446 2447 def test_flagsChangedInsideFetchSpecificResponse(self): 2448 """ 2449 Any unrequested flag information received along with other requested 2450 information in an untagged I{FETCH} received in response to a request 2451 issued with L{IMAP4Client.fetchSpecific} is passed to the 2452 C{flagsChanged} callback. 2453 """ 2454 transport = StringTransport() 2455 c = StillSimplerClient() 2456 c.makeConnection(transport) 2457 c.lineReceived('* OK [IMAP4rev1]') 2458 2459 def login(): 2460 d = c.login('blah', 'blah') 2461 c.dataReceived('0001 OK LOGIN\r\n') 2462 return d 2463 def select(): 2464 d = c.select('inbox') 2465 c.lineReceived('0002 OK SELECT') 2466 return d 2467 def fetch(): 2468 d = c.fetchSpecific('1:*', 2469 headerType='HEADER.FIELDS', 2470 headerArgs=['SUBJECT']) 2471 # This response includes FLAGS after the requested data. 2472 c.dataReceived('* 1 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {22}\r\n') 2473 c.dataReceived('Subject: subject one\r\n') 2474 c.dataReceived(' FLAGS (\\Recent))\r\n') 2475 # And this one includes it before! Either is possible. 2476 c.dataReceived('* 2 FETCH (FLAGS (\\Seen) BODY[HEADER.FIELDS ("SUBJECT")] {22}\r\n') 2477 c.dataReceived('Subject: subject two\r\n') 2478 c.dataReceived(')\r\n') 2479 c.dataReceived('0003 OK FETCH completed\r\n') 2480 return d 2481 2482 def test(res): 2483 self.assertEqual(res, { 2484 1: [['BODY', ['HEADER.FIELDS', ['SUBJECT']], 2485 'Subject: subject one\r\n']], 2486 2: [['BODY', ['HEADER.FIELDS', ['SUBJECT']], 2487 'Subject: subject two\r\n']] 2488 }) 2489 2490 self.assertEqual(c.flags, {1: ['\\Recent'], 2: ['\\Seen']}) 2491 2492 return login( 2493 ).addCallback(strip(select) 2494 ).addCallback(strip(fetch) 2495 ).addCallback(test) 2496 2497 2498 def test_flagsChangedInsideFetchMessageResponse(self): 2499 """ 2500 Any unrequested flag information received along with other requested 2501 information in an untagged I{FETCH} received in response to a request 2502 issued with L{IMAP4Client.fetchMessage} is passed to the 2503 C{flagsChanged} callback. 2504 """ 2505 transport = StringTransport() 2506 c = StillSimplerClient() 2507 c.makeConnection(transport) 2508 c.lineReceived('* OK [IMAP4rev1]') 2509 2510 def login(): 2511 d = c.login('blah', 'blah') 2512 c.dataReceived('0001 OK LOGIN\r\n') 2513 return d 2514 def select(): 2515 d = c.select('inbox') 2516 c.lineReceived('0002 OK SELECT') 2517 return d 2518 def fetch(): 2519 d = c.fetchMessage('1:*') 2520 c.dataReceived('* 1 FETCH (RFC822 {24}\r\n') 2521 c.dataReceived('Subject: first subject\r\n') 2522 c.dataReceived(' FLAGS (\Seen))\r\n') 2523 c.dataReceived('* 2 FETCH (FLAGS (\Recent \Seen) RFC822 {25}\r\n') 2524 c.dataReceived('Subject: second subject\r\n') 2525 c.dataReceived(')\r\n') 2526 c.dataReceived('0003 OK FETCH completed\r\n') 2527 return d 2528 2529 def test(res): 2530 self.assertEqual(res, { 2531 1: {'RFC822': 'Subject: first subject\r\n'}, 2532 2: {'RFC822': 'Subject: second subject\r\n'}}) 2533 2534 self.assertEqual( 2535 c.flags, {1: ['\\Seen'], 2: ['\\Recent', '\\Seen']}) 2536 2537 return login( 2538 ).addCallback(strip(select) 2539 ).addCallback(strip(fetch) 2540 ).addCallback(test) 2541 2542 2543 def test_authenticationChallengeDecodingException(self): 2544 """ 2545 When decoding a base64 encoded authentication message from the server, 2546 decoding errors are logged and then the client closes the connection. 2547 """ 2548 transport = StringTransportWithDisconnection() 2549 protocol = imap4.IMAP4Client() 2550 transport.protocol = protocol 2551 2552 protocol.makeConnection(transport) 2553 protocol.lineReceived( 2554 '* OK [CAPABILITY IMAP4rev1 IDLE NAMESPACE AUTH=CRAM-MD5] ' 2555 'Twisted IMAP4rev1 Ready') 2556 cAuth = imap4.CramMD5ClientAuthenticator('testuser') 2557 protocol.registerAuthenticator(cAuth) 2558 2559 d = protocol.authenticate('secret') 2560 # Should really be something describing the base64 decode error. See 2561 # #6021. 2562 self.assertFailure(d, error.ConnectionDone) 2563 2564 protocol.dataReceived('+ Something bad! and bad\r\n') 2565 2566 # This should not really be logged. See #6021. 2567 logged = self.flushLoggedErrors(imap4.IllegalServerResponse) 2568 self.assertEqual(len(logged), 1) 2569 self.assertEqual(logged[0].value.args[0], "Something bad! and bad") 2570 return d 2571 2572 2573 2574class PreauthIMAP4ClientMixin: 2575 """ 2576 Mixin for L{unittest.TestCase} subclasses which provides a C{setUp} method 2577 which creates an L{IMAP4Client} connected to a L{StringTransport} and puts 2578 it into the I{authenticated} state. 2579 2580 @ivar transport: A L{StringTransport} to which C{client} is connected. 2581 @ivar client: An L{IMAP4Client} which is connected to C{transport}. 2582 """ 2583 clientProtocol = imap4.IMAP4Client 2584 2585 def setUp(self): 2586 """ 2587 Create an IMAP4Client connected to a fake transport and in the 2588 authenticated state. 2589 """ 2590 self.transport = StringTransport() 2591 self.client = self.clientProtocol() 2592 self.client.makeConnection(self.transport) 2593 self.client.dataReceived('* PREAUTH Hello unittest\r\n') 2594 2595 2596 def _extractDeferredResult(self, d): 2597 """ 2598 Synchronously extract the result of the given L{Deferred}. Fail the 2599 test if that is not possible. 2600 """ 2601 result = [] 2602 error = [] 2603 d.addCallbacks(result.append, error.append) 2604 if result: 2605 return result[0] 2606 elif error: 2607 error[0].raiseException() 2608 else: 2609 self.fail("Expected result not available") 2610 2611 2612 2613class SelectionTestsMixin(PreauthIMAP4ClientMixin): 2614 """ 2615 Mixin for test cases which defines tests which apply to both I{EXAMINE} and 2616 I{SELECT} support. 2617 """ 2618 def _examineOrSelect(self): 2619 """ 2620 Issue either an I{EXAMINE} or I{SELECT} command (depending on 2621 C{self.method}), assert that the correct bytes are written to the 2622 transport, and return the L{Deferred} returned by whichever method was 2623 called. 2624 """ 2625 d = getattr(self.client, self.method)('foobox') 2626 self.assertEqual( 2627 self.transport.value(), '0001 %s foobox\r\n' % (self.command,)) 2628 return d 2629 2630 2631 def _response(self, *lines): 2632 """ 2633 Deliver the given (unterminated) response lines to C{self.client} and 2634 then deliver a tagged SELECT or EXAMINE completion line to finish the 2635 SELECT or EXAMINE response. 2636 """ 2637 for line in lines: 2638 self.client.dataReceived(line + '\r\n') 2639 self.client.dataReceived( 2640 '0001 OK [READ-ONLY] %s completed\r\n' % (self.command,)) 2641 2642 2643 def test_exists(self): 2644 """ 2645 If the server response to a I{SELECT} or I{EXAMINE} command includes an 2646 I{EXISTS} response, the L{Deferred} return by L{IMAP4Client.select} or 2647 L{IMAP4Client.examine} fires with a C{dict} including the value 2648 associated with the C{'EXISTS'} key. 2649 """ 2650 d = self._examineOrSelect() 2651 self._response('* 3 EXISTS') 2652 self.assertEqual( 2653 self._extractDeferredResult(d), 2654 {'READ-WRITE': False, 'EXISTS': 3}) 2655 2656 2657 def test_nonIntegerExists(self): 2658 """ 2659 If the server returns a non-integer EXISTS value in its response to a 2660 I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by 2661 L{IMAP4Client.select} or L{IMAP4Client.examine} fails with 2662 L{IllegalServerResponse}. 2663 """ 2664 d = self._examineOrSelect() 2665 self._response('* foo EXISTS') 2666 self.assertRaises( 2667 imap4.IllegalServerResponse, self._extractDeferredResult, d) 2668 2669 2670 def test_recent(self): 2671 """ 2672 If the server response to a I{SELECT} or I{EXAMINE} command includes an 2673 I{RECENT} response, the L{Deferred} return by L{IMAP4Client.select} or 2674 L{IMAP4Client.examine} fires with a C{dict} including the value 2675 associated with the C{'RECENT'} key. 2676 """ 2677 d = self._examineOrSelect() 2678 self._response('* 5 RECENT') 2679 self.assertEqual( 2680 self._extractDeferredResult(d), 2681 {'READ-WRITE': False, 'RECENT': 5}) 2682 2683 2684 def test_nonIntegerRecent(self): 2685 """ 2686 If the server returns a non-integer RECENT value in its response to a 2687 I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by 2688 L{IMAP4Client.select} or L{IMAP4Client.examine} fails with 2689 L{IllegalServerResponse}. 2690 """ 2691 d = self._examineOrSelect() 2692 self._response('* foo RECENT') 2693 self.assertRaises( 2694 imap4.IllegalServerResponse, self._extractDeferredResult, d) 2695 2696 2697 def test_unseen(self): 2698 """ 2699 If the server response to a I{SELECT} or I{EXAMINE} command includes an 2700 I{UNSEEN} response, the L{Deferred} returned by L{IMAP4Client.select} or 2701 L{IMAP4Client.examine} fires with a C{dict} including the value 2702 associated with the C{'UNSEEN'} key. 2703 """ 2704 d = self._examineOrSelect() 2705 self._response('* OK [UNSEEN 8] Message 8 is first unseen') 2706 self.assertEqual( 2707 self._extractDeferredResult(d), 2708 {'READ-WRITE': False, 'UNSEEN': 8}) 2709 2710 2711 def test_nonIntegerUnseen(self): 2712 """ 2713 If the server returns a non-integer UNSEEN value in its response to a 2714 I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by 2715 L{IMAP4Client.select} or L{IMAP4Client.examine} fails with 2716 L{IllegalServerResponse}. 2717 """ 2718 d = self._examineOrSelect() 2719 self._response('* OK [UNSEEN foo] Message foo is first unseen') 2720 self.assertRaises( 2721 imap4.IllegalServerResponse, self._extractDeferredResult, d) 2722 2723 2724 def test_uidvalidity(self): 2725 """ 2726 If the server response to a I{SELECT} or I{EXAMINE} command includes an 2727 I{UIDVALIDITY} response, the L{Deferred} returned by 2728 L{IMAP4Client.select} or L{IMAP4Client.examine} fires with a C{dict} 2729 including the value associated with the C{'UIDVALIDITY'} key. 2730 """ 2731 d = self._examineOrSelect() 2732 self._response('* OK [UIDVALIDITY 12345] UIDs valid') 2733 self.assertEqual( 2734 self._extractDeferredResult(d), 2735 {'READ-WRITE': False, 'UIDVALIDITY': 12345}) 2736 2737 2738 def test_nonIntegerUIDVALIDITY(self): 2739 """ 2740 If the server returns a non-integer UIDVALIDITY value in its response to 2741 a I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by 2742 L{IMAP4Client.select} or L{IMAP4Client.examine} fails with 2743 L{IllegalServerResponse}. 2744 """ 2745 d = self._examineOrSelect() 2746 self._response('* OK [UIDVALIDITY foo] UIDs valid') 2747 self.assertRaises( 2748 imap4.IllegalServerResponse, self._extractDeferredResult, d) 2749 2750 2751 def test_uidnext(self): 2752 """ 2753 If the server response to a I{SELECT} or I{EXAMINE} command includes an 2754 I{UIDNEXT} response, the L{Deferred} returned by L{IMAP4Client.select} 2755 or L{IMAP4Client.examine} fires with a C{dict} including the value 2756 associated with the C{'UIDNEXT'} key. 2757 """ 2758 d = self._examineOrSelect() 2759 self._response('* OK [UIDNEXT 4392] Predicted next UID') 2760 self.assertEqual( 2761 self._extractDeferredResult(d), 2762 {'READ-WRITE': False, 'UIDNEXT': 4392}) 2763 2764 2765 def test_nonIntegerUIDNEXT(self): 2766 """ 2767 If the server returns a non-integer UIDNEXT value in its response to a 2768 I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by 2769 L{IMAP4Client.select} or L{IMAP4Client.examine} fails with 2770 L{IllegalServerResponse}. 2771 """ 2772 d = self._examineOrSelect() 2773 self._response('* OK [UIDNEXT foo] Predicted next UID') 2774 self.assertRaises( 2775 imap4.IllegalServerResponse, self._extractDeferredResult, d) 2776 2777 2778 def test_flags(self): 2779 """ 2780 If the server response to a I{SELECT} or I{EXAMINE} command includes an 2781 I{FLAGS} response, the L{Deferred} returned by L{IMAP4Client.select} or 2782 L{IMAP4Client.examine} fires with a C{dict} including the value 2783 associated with the C{'FLAGS'} key. 2784 """ 2785 d = self._examineOrSelect() 2786 self._response( 2787 '* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)') 2788 self.assertEqual( 2789 self._extractDeferredResult(d), { 2790 'READ-WRITE': False, 2791 'FLAGS': ('\\Answered', '\\Flagged', '\\Deleted', '\\Seen', 2792 '\\Draft')}) 2793 2794 2795 def test_permanentflags(self): 2796 """ 2797 If the server response to a I{SELECT} or I{EXAMINE} command includes an 2798 I{FLAGS} response, the L{Deferred} returned by L{IMAP4Client.select} or 2799 L{IMAP4Client.examine} fires with a C{dict} including the value 2800 associated with the C{'FLAGS'} key. 2801 """ 2802 d = self._examineOrSelect() 2803 self._response( 2804 '* OK [PERMANENTFLAGS (\\Starred)] Just one permanent flag in ' 2805 'that list up there') 2806 self.assertEqual( 2807 self._extractDeferredResult(d), { 2808 'READ-WRITE': False, 2809 'PERMANENTFLAGS': ('\\Starred',)}) 2810 2811 2812 def test_unrecognizedOk(self): 2813 """ 2814 If the server response to a I{SELECT} or I{EXAMINE} command includes an 2815 I{OK} with unrecognized response code text, parsing does not fail. 2816 """ 2817 d = self._examineOrSelect() 2818 self._response( 2819 '* OK [X-MADE-UP] I just made this response text up.') 2820 # The value won't show up in the result. It would be okay if it did 2821 # someday, perhaps. This shouldn't ever happen, though. 2822 self.assertEqual( 2823 self._extractDeferredResult(d), {'READ-WRITE': False}) 2824 2825 2826 def test_bareOk(self): 2827 """ 2828 If the server response to a I{SELECT} or I{EXAMINE} command includes an 2829 I{OK} with no response code text, parsing does not fail. 2830 """ 2831 d = self._examineOrSelect() 2832 self._response('* OK') 2833 self.assertEqual( 2834 self._extractDeferredResult(d), {'READ-WRITE': False}) 2835 2836 2837 2838class IMAP4ClientExamineTests(SelectionTestsMixin, unittest.TestCase): 2839 """ 2840 Tests for the L{IMAP4Client.examine} method. 2841 2842 An example of usage of the EXAMINE command from RFC 3501, section 6.3.2:: 2843 2844 S: * 17 EXISTS 2845 S: * 2 RECENT 2846 S: * OK [UNSEEN 8] Message 8 is first unseen 2847 S: * OK [UIDVALIDITY 3857529045] UIDs valid 2848 S: * OK [UIDNEXT 4392] Predicted next UID 2849 S: * FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft) 2850 S: * OK [PERMANENTFLAGS ()] No permanent flags permitted 2851 S: A932 OK [READ-ONLY] EXAMINE completed 2852 """ 2853 method = 'examine' 2854 command = 'EXAMINE' 2855 2856 2857 2858 2859class IMAP4ClientSelectTests(SelectionTestsMixin, unittest.TestCase): 2860 """ 2861 Tests for the L{IMAP4Client.select} method. 2862 2863 An example of usage of the SELECT command from RFC 3501, section 6.3.1:: 2864 2865 C: A142 SELECT INBOX 2866 S: * 172 EXISTS 2867 S: * 1 RECENT 2868 S: * OK [UNSEEN 12] Message 12 is first unseen 2869 S: * OK [UIDVALIDITY 3857529045] UIDs valid 2870 S: * OK [UIDNEXT 4392] Predicted next UID 2871 S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) 2872 S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited 2873 S: A142 OK [READ-WRITE] SELECT completed 2874 """ 2875 method = 'select' 2876 command = 'SELECT' 2877 2878 2879 2880class IMAP4ClientExpungeTests(PreauthIMAP4ClientMixin, unittest.TestCase): 2881 """ 2882 Tests for the L{IMAP4Client.expunge} method. 2883 2884 An example of usage of the EXPUNGE command from RFC 3501, section 6.4.3:: 2885 2886 C: A202 EXPUNGE 2887 S: * 3 EXPUNGE 2888 S: * 3 EXPUNGE 2889 S: * 5 EXPUNGE 2890 S: * 8 EXPUNGE 2891 S: A202 OK EXPUNGE completed 2892 """ 2893 def _expunge(self): 2894 d = self.client.expunge() 2895 self.assertEqual(self.transport.value(), '0001 EXPUNGE\r\n') 2896 self.transport.clear() 2897 return d 2898 2899 2900 def _response(self, sequenceNumbers): 2901 for number in sequenceNumbers: 2902 self.client.lineReceived('* %s EXPUNGE' % (number,)) 2903 self.client.lineReceived('0001 OK EXPUNGE COMPLETED') 2904 2905 2906 def test_expunge(self): 2907 """ 2908 L{IMAP4Client.expunge} sends the I{EXPUNGE} command and returns a 2909 L{Deferred} which fires with a C{list} of message sequence numbers 2910 given by the server's response. 2911 """ 2912 d = self._expunge() 2913 self._response([3, 3, 5, 8]) 2914 self.assertEqual(self._extractDeferredResult(d), [3, 3, 5, 8]) 2915 2916 2917 def test_nonIntegerExpunged(self): 2918 """ 2919 If the server responds with a non-integer where a message sequence 2920 number is expected, the L{Deferred} returned by L{IMAP4Client.expunge} 2921 fails with L{IllegalServerResponse}. 2922 """ 2923 d = self._expunge() 2924 self._response([3, 3, 'foo', 8]) 2925 self.assertRaises( 2926 imap4.IllegalServerResponse, self._extractDeferredResult, d) 2927 2928 2929 2930class IMAP4ClientSearchTests(PreauthIMAP4ClientMixin, unittest.TestCase): 2931 """ 2932 Tests for the L{IMAP4Client.search} method. 2933 2934 An example of usage of the SEARCH command from RFC 3501, section 6.4.4:: 2935 2936 C: A282 SEARCH FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith" 2937 S: * SEARCH 2 84 882 2938 S: A282 OK SEARCH completed 2939 C: A283 SEARCH TEXT "string not in mailbox" 2940 S: * SEARCH 2941 S: A283 OK SEARCH completed 2942 C: A284 SEARCH CHARSET UTF-8 TEXT {6} 2943 C: XXXXXX 2944 S: * SEARCH 43 2945 S: A284 OK SEARCH completed 2946 """ 2947 def _search(self): 2948 d = self.client.search(imap4.Query(text="ABCDEF")) 2949 self.assertEqual( 2950 self.transport.value(), '0001 SEARCH (TEXT "ABCDEF")\r\n') 2951 return d 2952 2953 2954 def _response(self, messageNumbers): 2955 self.client.lineReceived( 2956 "* SEARCH " + " ".join(map(str, messageNumbers))) 2957 self.client.lineReceived("0001 OK SEARCH completed") 2958 2959 2960 def test_search(self): 2961 """ 2962 L{IMAP4Client.search} sends the I{SEARCH} command and returns a 2963 L{Deferred} which fires with a C{list} of message sequence numbers 2964 given by the server's response. 2965 """ 2966 d = self._search() 2967 self._response([2, 5, 10]) 2968 self.assertEqual(self._extractDeferredResult(d), [2, 5, 10]) 2969 2970 2971 def test_nonIntegerFound(self): 2972 """ 2973 If the server responds with a non-integer where a message sequence 2974 number is expected, the L{Deferred} returned by L{IMAP4Client.search} 2975 fails with L{IllegalServerResponse}. 2976 """ 2977 d = self._search() 2978 self._response([2, "foo", 10]) 2979 self.assertRaises( 2980 imap4.IllegalServerResponse, self._extractDeferredResult, d) 2981 2982 2983 2984class IMAP4ClientFetchTests(PreauthIMAP4ClientMixin, unittest.TestCase): 2985 """ 2986 Tests for the L{IMAP4Client.fetch} method. 2987 2988 See RFC 3501, section 6.4.5. 2989 """ 2990 def test_fetchUID(self): 2991 """ 2992 L{IMAP4Client.fetchUID} sends the I{FETCH UID} command and returns a 2993 L{Deferred} which fires with a C{dict} mapping message sequence numbers 2994 to C{dict}s mapping C{'UID'} to that message's I{UID} in the server's 2995 response. 2996 """ 2997 d = self.client.fetchUID('1:7') 2998 self.assertEqual(self.transport.value(), '0001 FETCH 1:7 (UID)\r\n') 2999 self.client.lineReceived('* 2 FETCH (UID 22)') 3000 self.client.lineReceived('* 3 FETCH (UID 23)') 3001 self.client.lineReceived('* 4 FETCH (UID 24)') 3002 self.client.lineReceived('* 5 FETCH (UID 25)') 3003 self.client.lineReceived('0001 OK FETCH completed') 3004 self.assertEqual( 3005 self._extractDeferredResult(d), { 3006 2: {'UID': '22'}, 3007 3: {'UID': '23'}, 3008 4: {'UID': '24'}, 3009 5: {'UID': '25'}}) 3010 3011 3012 def test_fetchUIDNonIntegerFound(self): 3013 """ 3014 If the server responds with a non-integer where a message sequence 3015 number is expected, the L{Deferred} returned by L{IMAP4Client.fetchUID} 3016 fails with L{IllegalServerResponse}. 3017 """ 3018 d = self.client.fetchUID('1') 3019 self.assertEqual(self.transport.value(), '0001 FETCH 1 (UID)\r\n') 3020 self.client.lineReceived('* foo FETCH (UID 22)') 3021 self.client.lineReceived('0001 OK FETCH completed') 3022 self.assertRaises( 3023 imap4.IllegalServerResponse, self._extractDeferredResult, d) 3024 3025 3026 def test_incompleteFetchUIDResponse(self): 3027 """ 3028 If the server responds with an incomplete I{FETCH} response line, the 3029 L{Deferred} returned by L{IMAP4Client.fetchUID} fails with 3030 L{IllegalServerResponse}. 3031 """ 3032 d = self.client.fetchUID('1:7') 3033 self.assertEqual(self.transport.value(), '0001 FETCH 1:7 (UID)\r\n') 3034 self.client.lineReceived('* 2 FETCH (UID 22)') 3035 self.client.lineReceived('* 3 FETCH (UID)') 3036 self.client.lineReceived('* 4 FETCH (UID 24)') 3037 self.client.lineReceived('0001 OK FETCH completed') 3038 self.assertRaises( 3039 imap4.IllegalServerResponse, self._extractDeferredResult, d) 3040 3041 3042 def test_fetchBody(self): 3043 """ 3044 L{IMAP4Client.fetchBody} sends the I{FETCH BODY} command and returns a 3045 L{Deferred} which fires with a C{dict} mapping message sequence numbers 3046 to C{dict}s mapping C{'RFC822.TEXT'} to that message's body as given in 3047 the server's response. 3048 """ 3049 d = self.client.fetchBody('3') 3050 self.assertEqual( 3051 self.transport.value(), '0001 FETCH 3 (RFC822.TEXT)\r\n') 3052 self.client.lineReceived('* 3 FETCH (RFC822.TEXT "Message text")') 3053 self.client.lineReceived('0001 OK FETCH completed') 3054 self.assertEqual( 3055 self._extractDeferredResult(d), 3056 {3: {'RFC822.TEXT': 'Message text'}}) 3057 3058 3059 def test_fetchSpecific(self): 3060 """ 3061 L{IMAP4Client.fetchSpecific} sends the I{BODY[]} command if no 3062 parameters beyond the message set to retrieve are given. It returns a 3063 L{Deferred} which fires with a C{dict} mapping message sequence numbers 3064 to C{list}s of corresponding message data given by the server's 3065 response. 3066 """ 3067 d = self.client.fetchSpecific('7') 3068 self.assertEqual( 3069 self.transport.value(), '0001 FETCH 7 BODY[]\r\n') 3070 self.client.lineReceived('* 7 FETCH (BODY[] "Some body")') 3071 self.client.lineReceived('0001 OK FETCH completed') 3072 self.assertEqual( 3073 self._extractDeferredResult(d), {7: [['BODY', [], "Some body"]]}) 3074 3075 3076 def test_fetchSpecificPeek(self): 3077 """ 3078 L{IMAP4Client.fetchSpecific} issues a I{BODY.PEEK[]} command if passed 3079 C{True} for the C{peek} parameter. 3080 """ 3081 d = self.client.fetchSpecific('6', peek=True) 3082 self.assertEqual( 3083 self.transport.value(), '0001 FETCH 6 BODY.PEEK[]\r\n') 3084 # BODY.PEEK responses are just BODY 3085 self.client.lineReceived('* 6 FETCH (BODY[] "Some body")') 3086 self.client.lineReceived('0001 OK FETCH completed') 3087 self.assertEqual( 3088 self._extractDeferredResult(d), {6: [['BODY', [], "Some body"]]}) 3089 3090 3091 def test_fetchSpecificNumbered(self): 3092 """ 3093 L{IMAP4Client.fetchSpecific}, when passed a sequence for for 3094 C{headerNumber}, sends the I{BODY[N.M]} command. It returns a 3095 L{Deferred} which fires with a C{dict} mapping message sequence numbers 3096 to C{list}s of corresponding message data given by the server's 3097 response. 3098 """ 3099 d = self.client.fetchSpecific('7', headerNumber=(1, 2, 3)) 3100 self.assertEqual( 3101 self.transport.value(), '0001 FETCH 7 BODY[1.2.3]\r\n') 3102 self.client.lineReceived('* 7 FETCH (BODY[1.2.3] "Some body")') 3103 self.client.lineReceived('0001 OK FETCH completed') 3104 self.assertEqual( 3105 self._extractDeferredResult(d), 3106 {7: [['BODY', ['1.2.3'], "Some body"]]}) 3107 3108 3109 def test_fetchSpecificText(self): 3110 """ 3111 L{IMAP4Client.fetchSpecific}, when passed C{'TEXT'} for C{headerType}, 3112 sends the I{BODY[TEXT]} command. It returns a L{Deferred} which fires 3113 with a C{dict} mapping message sequence numbers to C{list}s of 3114 corresponding message data given by the server's response. 3115 """ 3116 d = self.client.fetchSpecific('8', headerType='TEXT') 3117 self.assertEqual( 3118 self.transport.value(), '0001 FETCH 8 BODY[TEXT]\r\n') 3119 self.client.lineReceived('* 8 FETCH (BODY[TEXT] "Some body")') 3120 self.client.lineReceived('0001 OK FETCH completed') 3121 self.assertEqual( 3122 self._extractDeferredResult(d), 3123 {8: [['BODY', ['TEXT'], "Some body"]]}) 3124 3125 3126 def test_fetchSpecificNumberedText(self): 3127 """ 3128 If passed a value for the C{headerNumber} parameter and C{'TEXT'} for 3129 the C{headerType} parameter, L{IMAP4Client.fetchSpecific} sends a 3130 I{BODY[number.TEXT]} request and returns a L{Deferred} which fires with 3131 a C{dict} mapping message sequence numbers to C{list}s of message data 3132 given by the server's response. 3133 """ 3134 d = self.client.fetchSpecific('4', headerType='TEXT', headerNumber=7) 3135 self.assertEqual( 3136 self.transport.value(), '0001 FETCH 4 BODY[7.TEXT]\r\n') 3137 self.client.lineReceived('* 4 FETCH (BODY[7.TEXT] "Some body")') 3138 self.client.lineReceived('0001 OK FETCH completed') 3139 self.assertEqual( 3140 self._extractDeferredResult(d), 3141 {4: [['BODY', ['7.TEXT'], "Some body"]]}) 3142 3143 3144 def test_incompleteFetchSpecificTextResponse(self): 3145 """ 3146 If the server responds to a I{BODY[TEXT]} request with a I{FETCH} line 3147 which is truncated after the I{BODY[TEXT]} tokens, the L{Deferred} 3148 returned by L{IMAP4Client.fetchUID} fails with 3149 L{IllegalServerResponse}. 3150 """ 3151 d = self.client.fetchSpecific('8', headerType='TEXT') 3152 self.assertEqual( 3153 self.transport.value(), '0001 FETCH 8 BODY[TEXT]\r\n') 3154 self.client.lineReceived('* 8 FETCH (BODY[TEXT])') 3155 self.client.lineReceived('0001 OK FETCH completed') 3156 self.assertRaises( 3157 imap4.IllegalServerResponse, self._extractDeferredResult, d) 3158 3159 3160 def test_fetchSpecificMIME(self): 3161 """ 3162 L{IMAP4Client.fetchSpecific}, when passed C{'MIME'} for C{headerType}, 3163 sends the I{BODY[MIME]} command. It returns a L{Deferred} which fires 3164 with a C{dict} mapping message sequence numbers to C{list}s of 3165 corresponding message data given by the server's response. 3166 """ 3167 d = self.client.fetchSpecific('8', headerType='MIME') 3168 self.assertEqual( 3169 self.transport.value(), '0001 FETCH 8 BODY[MIME]\r\n') 3170 self.client.lineReceived('* 8 FETCH (BODY[MIME] "Some body")') 3171 self.client.lineReceived('0001 OK FETCH completed') 3172 self.assertEqual( 3173 self._extractDeferredResult(d), 3174 {8: [['BODY', ['MIME'], "Some body"]]}) 3175 3176 3177 def test_fetchSpecificPartial(self): 3178 """ 3179 L{IMAP4Client.fetchSpecific}, when passed C{offset} and C{length}, 3180 sends a partial content request (like I{BODY[TEXT]<offset.length>}). 3181 It returns a L{Deferred} which fires with a C{dict} mapping message 3182 sequence numbers to C{list}s of corresponding message data given by the 3183 server's response. 3184 """ 3185 d = self.client.fetchSpecific( 3186 '9', headerType='TEXT', offset=17, length=3) 3187 self.assertEqual( 3188 self.transport.value(), '0001 FETCH 9 BODY[TEXT]<17.3>\r\n') 3189 self.client.lineReceived('* 9 FETCH (BODY[TEXT]<17> "foo")') 3190 self.client.lineReceived('0001 OK FETCH completed') 3191 self.assertEqual( 3192 self._extractDeferredResult(d), 3193 {9: [['BODY', ['TEXT'], '<17>', 'foo']]}) 3194 3195 3196 def test_incompleteFetchSpecificPartialResponse(self): 3197 """ 3198 If the server responds to a I{BODY[TEXT]} request with a I{FETCH} line 3199 which is truncated after the I{BODY[TEXT]<offset>} tokens, the 3200 L{Deferred} returned by L{IMAP4Client.fetchUID} fails with 3201 L{IllegalServerResponse}. 3202 """ 3203 d = self.client.fetchSpecific('8', headerType='TEXT') 3204 self.assertEqual( 3205 self.transport.value(), '0001 FETCH 8 BODY[TEXT]\r\n') 3206 self.client.lineReceived('* 8 FETCH (BODY[TEXT]<17>)') 3207 self.client.lineReceived('0001 OK FETCH completed') 3208 self.assertRaises( 3209 imap4.IllegalServerResponse, self._extractDeferredResult, d) 3210 3211 3212 def test_fetchSpecificHTML(self): 3213 """ 3214 If the body of a message begins with I{<} and ends with I{>} (as, 3215 for example, HTML bodies typically will), this is still interpreted 3216 as the body by L{IMAP4Client.fetchSpecific} (and particularly, not 3217 as a length indicator for a response to a request for a partial 3218 body). 3219 """ 3220 d = self.client.fetchSpecific('7') 3221 self.assertEqual( 3222 self.transport.value(), '0001 FETCH 7 BODY[]\r\n') 3223 self.client.lineReceived('* 7 FETCH (BODY[] "<html>test</html>")') 3224 self.client.lineReceived('0001 OK FETCH completed') 3225 self.assertEqual( 3226 self._extractDeferredResult(d), {7: [['BODY', [], "<html>test</html>"]]}) 3227 3228 3229 3230class IMAP4ClientStoreTests(PreauthIMAP4ClientMixin, unittest.TestCase): 3231 """ 3232 Tests for the L{IMAP4Client.setFlags}, L{IMAP4Client.addFlags}, and 3233 L{IMAP4Client.removeFlags} methods. 3234 3235 An example of usage of the STORE command, in terms of which these three 3236 methods are implemented, from RFC 3501, section 6.4.6:: 3237 3238 C: A003 STORE 2:4 +FLAGS (\Deleted) 3239 S: * 2 FETCH (FLAGS (\Deleted \Seen)) 3240 S: * 3 FETCH (FLAGS (\Deleted)) 3241 S: * 4 FETCH (FLAGS (\Deleted \Flagged \Seen)) 3242 S: A003 OK STORE completed 3243 """ 3244 clientProtocol = StillSimplerClient 3245 3246 def _flagsTest(self, method, item): 3247 """ 3248 Test a non-silent flag modifying method. Call the method, assert that 3249 the correct bytes are sent, deliver a I{FETCH} response, and assert 3250 that the result of the Deferred returned by the method is correct. 3251 3252 @param method: The name of the method to test. 3253 @param item: The data item which is expected to be specified. 3254 """ 3255 d = getattr(self.client, method)('3', ('\\Read', '\\Seen'), False) 3256 self.assertEqual( 3257 self.transport.value(), 3258 '0001 STORE 3 ' + item + ' (\\Read \\Seen)\r\n') 3259 self.client.lineReceived('* 3 FETCH (FLAGS (\\Read \\Seen))') 3260 self.client.lineReceived('0001 OK STORE completed') 3261 self.assertEqual( 3262 self._extractDeferredResult(d), 3263 {3: {'FLAGS': ['\\Read', '\\Seen']}}) 3264 3265 3266 def _flagsSilentlyTest(self, method, item): 3267 """ 3268 Test a silent flag modifying method. Call the method, assert that the 3269 correct bytes are sent, deliver an I{OK} response, and assert that the 3270 result of the Deferred returned by the method is correct. 3271 3272 @param method: The name of the method to test. 3273 @param item: The data item which is expected to be specified. 3274 """ 3275 d = getattr(self.client, method)('3', ('\\Read', '\\Seen'), True) 3276 self.assertEqual( 3277 self.transport.value(), 3278 '0001 STORE 3 ' + item + ' (\\Read \\Seen)\r\n') 3279 self.client.lineReceived('0001 OK STORE completed') 3280 self.assertEqual(self._extractDeferredResult(d), {}) 3281 3282 3283 def _flagsSilentlyWithUnsolicitedDataTest(self, method, item): 3284 """ 3285 Test unsolicited data received in response to a silent flag modifying 3286 method. Call the method, assert that the correct bytes are sent, 3287 deliver the unsolicited I{FETCH} response, and assert that the result 3288 of the Deferred returned by the method is correct. 3289 3290 @param method: The name of the method to test. 3291 @param item: The data item which is expected to be specified. 3292 """ 3293 d = getattr(self.client, method)('3', ('\\Read', '\\Seen'), True) 3294 self.assertEqual( 3295 self.transport.value(), 3296 '0001 STORE 3 ' + item + ' (\\Read \\Seen)\r\n') 3297 self.client.lineReceived('* 2 FETCH (FLAGS (\\Read \\Seen))') 3298 self.client.lineReceived('0001 OK STORE completed') 3299 self.assertEqual(self._extractDeferredResult(d), {}) 3300 self.assertEqual(self.client.flags, {2: ['\\Read', '\\Seen']}) 3301 3302 3303 def test_setFlags(self): 3304 """ 3305 When passed a C{False} value for the C{silent} parameter, 3306 L{IMAP4Client.setFlags} sends the I{STORE} command with a I{FLAGS} data 3307 item and returns a L{Deferred} which fires with a C{dict} mapping 3308 message sequence numbers to C{dict}s mapping C{'FLAGS'} to the new 3309 flags of those messages. 3310 """ 3311 self._flagsTest('setFlags', 'FLAGS') 3312 3313 3314 def test_setFlagsSilently(self): 3315 """ 3316 When passed a C{True} value for the C{silent} parameter, 3317 L{IMAP4Client.setFlags} sends the I{STORE} command with a 3318 I{FLAGS.SILENT} data item and returns a L{Deferred} which fires with an 3319 empty dictionary. 3320 """ 3321 self._flagsSilentlyTest('setFlags', 'FLAGS.SILENT') 3322 3323 3324 def test_setFlagsSilentlyWithUnsolicitedData(self): 3325 """ 3326 If unsolicited flag data is received in response to a I{STORE} 3327 I{FLAGS.SILENT} request, that data is passed to the C{flagsChanged} 3328 callback. 3329 """ 3330 self._flagsSilentlyWithUnsolicitedDataTest('setFlags', 'FLAGS.SILENT') 3331 3332 3333 def test_addFlags(self): 3334 """ 3335 L{IMAP4Client.addFlags} is like L{IMAP4Client.setFlags}, but sends 3336 I{+FLAGS} instead of I{FLAGS}. 3337 """ 3338 self._flagsTest('addFlags', '+FLAGS') 3339 3340 3341 def test_addFlagsSilently(self): 3342 """ 3343 L{IMAP4Client.addFlags} with a C{True} value for C{silent} behaves like 3344 L{IMAP4Client.setFlags} with a C{True} value for C{silent}, but it 3345 sends I{+FLAGS.SILENT} instead of I{FLAGS.SILENT}. 3346 """ 3347 self._flagsSilentlyTest('addFlags', '+FLAGS.SILENT') 3348 3349 3350 def test_addFlagsSilentlyWithUnsolicitedData(self): 3351 """ 3352 L{IMAP4Client.addFlags} behaves like L{IMAP4Client.setFlags} when used 3353 in silent mode and unsolicited data is received. 3354 """ 3355 self._flagsSilentlyWithUnsolicitedDataTest('addFlags', '+FLAGS.SILENT') 3356 3357 3358 def test_removeFlags(self): 3359 """ 3360 L{IMAP4Client.removeFlags} is like L{IMAP4Client.setFlags}, but sends 3361 I{-FLAGS} instead of I{FLAGS}. 3362 """ 3363 self._flagsTest('removeFlags', '-FLAGS') 3364 3365 3366 def test_removeFlagsSilently(self): 3367 """ 3368 L{IMAP4Client.removeFlags} with a C{True} value for C{silent} behaves 3369 like L{IMAP4Client.setFlags} with a C{True} value for C{silent}, but it 3370 sends I{-FLAGS.SILENT} instead of I{FLAGS.SILENT}. 3371 """ 3372 self._flagsSilentlyTest('removeFlags', '-FLAGS.SILENT') 3373 3374 3375 def test_removeFlagsSilentlyWithUnsolicitedData(self): 3376 """ 3377 L{IMAP4Client.removeFlags} behaves like L{IMAP4Client.setFlags} when 3378 used in silent mode and unsolicited data is received. 3379 """ 3380 self._flagsSilentlyWithUnsolicitedDataTest('removeFlags', '-FLAGS.SILENT') 3381 3382 3383 3384class FakeyServer(imap4.IMAP4Server): 3385 state = 'select' 3386 timeout = None 3387 3388 def sendServerGreeting(self): 3389 pass 3390 3391class FakeyMessage(util.FancyStrMixin): 3392 implements(imap4.IMessage) 3393 3394 showAttributes = ('headers', 'flags', 'date', 'body', 'uid') 3395 3396 def __init__(self, headers, flags, date, body, uid, subpart): 3397 self.headers = headers 3398 self.flags = flags 3399 self._body = body 3400 self.size = len(body) 3401 self.date = date 3402 self.uid = uid 3403 self.subpart = subpart 3404 3405 def getHeaders(self, negate, *names): 3406 self.got_headers = negate, names 3407 return self.headers 3408 3409 def getFlags(self): 3410 return self.flags 3411 3412 def getInternalDate(self): 3413 return self.date 3414 3415 def getBodyFile(self): 3416 return StringIO(self._body) 3417 3418 def getSize(self): 3419 return self.size 3420 3421 def getUID(self): 3422 return self.uid 3423 3424 def isMultipart(self): 3425 return self.subpart is not None 3426 3427 def getSubPart(self, part): 3428 self.got_subpart = part 3429 return self.subpart[part] 3430 3431class NewStoreTestCase(unittest.TestCase, IMAP4HelperMixin): 3432 result = None 3433 storeArgs = None 3434 3435 def setUp(self): 3436 self.received_messages = self.received_uid = None 3437 3438 self.server = imap4.IMAP4Server() 3439 self.server.state = 'select' 3440 self.server.mbox = self 3441 self.connected = defer.Deferred() 3442 self.client = SimpleClient(self.connected) 3443 3444 def addListener(self, x): 3445 pass 3446 def removeListener(self, x): 3447 pass 3448 3449 def store(self, *args, **kw): 3450 self.storeArgs = args, kw 3451 return self.response 3452 3453 def _storeWork(self): 3454 def connected(): 3455 return self.function(self.messages, self.flags, self.silent, self.uid) 3456 def result(R): 3457 self.result = R 3458 3459 self.connected.addCallback(strip(connected) 3460 ).addCallback(result 3461 ).addCallback(self._cbStopClient 3462 ).addErrback(self._ebGeneral) 3463 3464 def check(ignored): 3465 self.assertEqual(self.result, self.expected) 3466 self.assertEqual(self.storeArgs, self.expectedArgs) 3467 d = loopback.loopbackTCP(self.server, self.client, noisy=False) 3468 d.addCallback(check) 3469 return d 3470 3471 def testSetFlags(self, uid=0): 3472 self.function = self.client.setFlags 3473 self.messages = '1,5,9' 3474 self.flags = ['\\A', '\\B', 'C'] 3475 self.silent = False 3476 self.uid = uid 3477 self.response = { 3478 1: ['\\A', '\\B', 'C'], 3479 5: ['\\A', '\\B', 'C'], 3480 9: ['\\A', '\\B', 'C'], 3481 } 3482 self.expected = { 3483 1: {'FLAGS': ['\\A', '\\B', 'C']}, 3484 5: {'FLAGS': ['\\A', '\\B', 'C']}, 3485 9: {'FLAGS': ['\\A', '\\B', 'C']}, 3486 } 3487 msg = imap4.MessageSet() 3488 msg.add(1) 3489 msg.add(5) 3490 msg.add(9) 3491 self.expectedArgs = ((msg, ['\\A', '\\B', 'C'], 0), {'uid': 0}) 3492 return self._storeWork() 3493 3494 3495 3496class GetBodyStructureTests(unittest.TestCase): 3497 """ 3498 Tests for L{imap4.getBodyStructure}, a helper for constructing a list which 3499 directly corresponds to the wire information needed for a I{BODY} or 3500 I{BODYSTRUCTURE} response. 3501 """ 3502 def test_singlePart(self): 3503 """ 3504 L{imap4.getBodyStructure} accepts a L{IMessagePart} provider and returns 3505 a list giving the basic fields for the I{BODY} response for that 3506 message. 3507 """ 3508 body = 'hello, world' 3509 major = 'image' 3510 minor = 'jpeg' 3511 charset = 'us-ascii' 3512 identifier = 'some kind of id' 3513 description = 'great justice' 3514 encoding = 'maximum' 3515 msg = FakeyMessage({ 3516 'content-type': '%s/%s; charset=%s; x=y' % ( 3517 major, minor, charset), 3518 'content-id': identifier, 3519 'content-description': description, 3520 'content-transfer-encoding': encoding, 3521 }, (), '', body, 123, None) 3522 structure = imap4.getBodyStructure(msg) 3523 self.assertEqual( 3524 [major, minor, ["charset", charset, 'x', 'y'], identifier, 3525 description, encoding, len(body)], 3526 structure) 3527 3528 3529 def test_singlePartExtended(self): 3530 """ 3531 L{imap4.getBodyStructure} returns a list giving the basic and extended 3532 fields for a I{BODYSTRUCTURE} response if passed C{True} for the 3533 C{extended} parameter. 3534 """ 3535 body = 'hello, world' 3536 major = 'image' 3537 minor = 'jpeg' 3538 charset = 'us-ascii' 3539 identifier = 'some kind of id' 3540 description = 'great justice' 3541 encoding = 'maximum' 3542 md5 = 'abcdefabcdef' 3543 msg = FakeyMessage({ 3544 'content-type': '%s/%s; charset=%s; x=y' % ( 3545 major, minor, charset), 3546 'content-id': identifier, 3547 'content-description': description, 3548 'content-transfer-encoding': encoding, 3549 'content-md5': md5, 3550 'content-disposition': 'attachment; name=foo; size=bar', 3551 'content-language': 'fr', 3552 'content-location': 'France', 3553 }, (), '', body, 123, None) 3554 structure = imap4.getBodyStructure(msg, extended=True) 3555 self.assertEqual( 3556 [major, minor, ["charset", charset, 'x', 'y'], identifier, 3557 description, encoding, len(body), md5, 3558 ['attachment', ['name', 'foo', 'size', 'bar']], 'fr', 'France'], 3559 structure) 3560 3561 3562 def test_singlePartWithMissing(self): 3563 """ 3564 For fields with no information contained in the message headers, 3565 L{imap4.getBodyStructure} fills in C{None} values in its result. 3566 """ 3567 major = 'image' 3568 minor = 'jpeg' 3569 body = 'hello, world' 3570 msg = FakeyMessage({ 3571 'content-type': '%s/%s' % (major, minor), 3572 }, (), '', body, 123, None) 3573 structure = imap4.getBodyStructure(msg, extended=True) 3574 self.assertEqual( 3575 [major, minor, None, None, None, None, len(body), None, None, 3576 None, None], 3577 structure) 3578 3579 3580 def test_textPart(self): 3581 """ 3582 For a I{text/*} message, the number of lines in the message body are 3583 included after the common single-part basic fields. 3584 """ 3585 body = 'hello, world\nhow are you?\ngoodbye\n' 3586 major = 'text' 3587 minor = 'jpeg' 3588 charset = 'us-ascii' 3589 identifier = 'some kind of id' 3590 description = 'great justice' 3591 encoding = 'maximum' 3592 msg = FakeyMessage({ 3593 'content-type': '%s/%s; charset=%s; x=y' % ( 3594 major, minor, charset), 3595 'content-id': identifier, 3596 'content-description': description, 3597 'content-transfer-encoding': encoding, 3598 }, (), '', body, 123, None) 3599 structure = imap4.getBodyStructure(msg) 3600 self.assertEqual( 3601 [major, minor, ["charset", charset, 'x', 'y'], identifier, 3602 description, encoding, len(body), len(body.splitlines())], 3603 structure) 3604 3605 3606 def test_rfc822Message(self): 3607 """ 3608 For a I{message/rfc822} message, the common basic fields are followed 3609 by information about the contained message. 3610 """ 3611 body = 'hello, world\nhow are you?\ngoodbye\n' 3612 major = 'text' 3613 minor = 'jpeg' 3614 charset = 'us-ascii' 3615 identifier = 'some kind of id' 3616 description = 'great justice' 3617 encoding = 'maximum' 3618 msg = FakeyMessage({ 3619 'content-type': '%s/%s; charset=%s; x=y' % ( 3620 major, minor, charset), 3621 'from': 'Alice <alice@example.com>', 3622 'to': 'Bob <bob@example.com>', 3623 'content-id': identifier, 3624 'content-description': description, 3625 'content-transfer-encoding': encoding, 3626 }, (), '', body, 123, None) 3627 3628 container = FakeyMessage({ 3629 'content-type': 'message/rfc822', 3630 }, (), '', '', 123, [msg]) 3631 3632 structure = imap4.getBodyStructure(container) 3633 self.assertEqual( 3634 ['message', 'rfc822', None, None, None, None, 0, 3635 imap4.getEnvelope(msg), imap4.getBodyStructure(msg), 3], 3636 structure) 3637 3638 3639 def test_multiPart(self): 3640 """ 3641 For a I{multipart/*} message, L{imap4.getBodyStructure} returns a list 3642 containing the body structure information for each part of the message 3643 followed by an element giving the MIME subtype of the message. 3644 """ 3645 oneSubPart = FakeyMessage({ 3646 'content-type': 'image/jpeg; x=y', 3647 'content-id': 'some kind of id', 3648 'content-description': 'great justice', 3649 'content-transfer-encoding': 'maximum', 3650 }, (), '', 'hello world', 123, None) 3651 3652 anotherSubPart = FakeyMessage({ 3653 'content-type': 'text/plain; charset=us-ascii', 3654 }, (), '', 'some stuff', 321, None) 3655 3656 container = FakeyMessage({ 3657 'content-type': 'multipart/related', 3658 }, (), '', '', 555, [oneSubPart, anotherSubPart]) 3659 3660 self.assertEqual( 3661 [imap4.getBodyStructure(oneSubPart), 3662 imap4.getBodyStructure(anotherSubPart), 3663 'related'], 3664 imap4.getBodyStructure(container)) 3665 3666 3667 def test_multiPartExtended(self): 3668 """ 3669 When passed a I{multipart/*} message and C{True} for the C{extended} 3670 argument, L{imap4.getBodyStructure} includes extended structure 3671 information from the parts of the multipart message and extended 3672 structure information about the multipart message itself. 3673 """ 3674 oneSubPart = FakeyMessage({ 3675 'content-type': 'image/jpeg; x=y', 3676 'content-id': 'some kind of id', 3677 'content-description': 'great justice', 3678 'content-transfer-encoding': 'maximum', 3679 }, (), '', 'hello world', 123, None) 3680 3681 anotherSubPart = FakeyMessage({ 3682 'content-type': 'text/plain; charset=us-ascii', 3683 }, (), '', 'some stuff', 321, None) 3684 3685 container = FakeyMessage({ 3686 'content-type': 'multipart/related; foo=bar', 3687 'content-language': 'es', 3688 'content-location': 'Spain', 3689 'content-disposition': 'attachment; name=monkeys', 3690 }, (), '', '', 555, [oneSubPart, anotherSubPart]) 3691 3692 self.assertEqual( 3693 [imap4.getBodyStructure(oneSubPart, extended=True), 3694 imap4.getBodyStructure(anotherSubPart, extended=True), 3695 'related', ['foo', 'bar'], ['attachment', ['name', 'monkeys']], 3696 'es', 'Spain'], 3697 imap4.getBodyStructure(container, extended=True)) 3698 3699 3700 3701class NewFetchTestCase(unittest.TestCase, IMAP4HelperMixin): 3702 def setUp(self): 3703 self.received_messages = self.received_uid = None 3704 self.result = None 3705 3706 self.server = imap4.IMAP4Server() 3707 self.server.state = 'select' 3708 self.server.mbox = self 3709 self.connected = defer.Deferred() 3710 self.client = SimpleClient(self.connected) 3711 3712 def addListener(self, x): 3713 pass 3714 def removeListener(self, x): 3715 pass 3716 3717 def fetch(self, messages, uid): 3718 self.received_messages = messages 3719 self.received_uid = uid 3720 return iter(zip(range(len(self.msgObjs)), self.msgObjs)) 3721 3722 def _fetchWork(self, uid): 3723 if uid: 3724 for (i, msg) in zip(range(len(self.msgObjs)), self.msgObjs): 3725 self.expected[i]['UID'] = str(msg.getUID()) 3726 3727 def result(R): 3728 self.result = R 3729 3730 self.connected.addCallback(lambda _: self.function(self.messages, uid) 3731 ).addCallback(result 3732 ).addCallback(self._cbStopClient 3733 ).addErrback(self._ebGeneral) 3734 3735 d = loopback.loopbackTCP(self.server, self.client, noisy=False) 3736 d.addCallback(lambda x : self.assertEqual(self.result, self.expected)) 3737 return d 3738 3739 def testFetchUID(self): 3740 self.function = lambda m, u: self.client.fetchUID(m) 3741 3742 self.messages = '7' 3743 self.msgObjs = [ 3744 FakeyMessage({}, (), '', '', 12345, None), 3745 FakeyMessage({}, (), '', '', 999, None), 3746 FakeyMessage({}, (), '', '', 10101, None), 3747 ] 3748 self.expected = { 3749 0: {'UID': '12345'}, 3750 1: {'UID': '999'}, 3751 2: {'UID': '10101'}, 3752 } 3753 return self._fetchWork(0) 3754 3755 def testFetchFlags(self, uid=0): 3756 self.function = self.client.fetchFlags 3757 self.messages = '9' 3758 self.msgObjs = [ 3759 FakeyMessage({}, ['FlagA', 'FlagB', '\\FlagC'], '', '', 54321, None), 3760 FakeyMessage({}, ['\\FlagC', 'FlagA', 'FlagB'], '', '', 12345, None), 3761 ] 3762 self.expected = { 3763 0: {'FLAGS': ['FlagA', 'FlagB', '\\FlagC']}, 3764 1: {'FLAGS': ['\\FlagC', 'FlagA', 'FlagB']}, 3765 } 3766 return self._fetchWork(uid) 3767 3768 def testFetchFlagsUID(self): 3769 return self.testFetchFlags(1) 3770 3771 def testFetchInternalDate(self, uid=0): 3772 self.function = self.client.fetchInternalDate 3773 self.messages = '13' 3774 self.msgObjs = [ 3775 FakeyMessage({}, (), 'Fri, 02 Nov 2003 21:25:10 GMT', '', 23232, None), 3776 FakeyMessage({}, (), 'Thu, 29 Dec 2013 11:31:52 EST', '', 101, None), 3777 FakeyMessage({}, (), 'Mon, 10 Mar 1992 02:44:30 CST', '', 202, None), 3778 FakeyMessage({}, (), 'Sat, 11 Jan 2000 14:40:24 PST', '', 303, None), 3779 ] 3780 self.expected = { 3781 0: {'INTERNALDATE': '02-Nov-2003 21:25:10 +0000'}, 3782 1: {'INTERNALDATE': '29-Dec-2013 11:31:52 -0500'}, 3783 2: {'INTERNALDATE': '10-Mar-1992 02:44:30 -0600'}, 3784 3: {'INTERNALDATE': '11-Jan-2000 14:40:24 -0800'}, 3785 } 3786 return self._fetchWork(uid) 3787 3788 def testFetchInternalDateUID(self): 3789 return self.testFetchInternalDate(1) 3790 3791 3792 def test_fetchInternalDateLocaleIndependent(self): 3793 """ 3794 The month name in the date is locale independent. 3795 """ 3796 # Fake that we're in a language where December is not Dec 3797 currentLocale = locale.setlocale(locale.LC_ALL, None) 3798 locale.setlocale(locale.LC_ALL, "es_AR.UTF8") 3799 self.addCleanup(locale.setlocale, locale.LC_ALL, currentLocale) 3800 return self.testFetchInternalDate(1) 3801 3802 # if alternate locale is not available, the previous test will be skipped, 3803 # please install this locale for it to run. Avoid using locale.getlocale to 3804 # learn the current locale; its values don't round-trip well on all 3805 # platforms. Fortunately setlocale returns a value which does round-trip 3806 # well. 3807 currentLocale = locale.setlocale(locale.LC_ALL, None) 3808 try: 3809 locale.setlocale(locale.LC_ALL, "es_AR.UTF8") 3810 except locale.Error: 3811 test_fetchInternalDateLocaleIndependent.skip = ( 3812 "The es_AR.UTF8 locale is not installed.") 3813 else: 3814 locale.setlocale(locale.LC_ALL, currentLocale) 3815 3816 3817 def testFetchEnvelope(self, uid=0): 3818 self.function = self.client.fetchEnvelope 3819 self.messages = '15' 3820 self.msgObjs = [ 3821 FakeyMessage({ 3822 'from': 'user@domain', 'to': 'resu@domain', 3823 'date': 'thursday', 'subject': 'it is a message', 3824 'message-id': 'id-id-id-yayaya'}, (), '', '', 65656, 3825 None), 3826 ] 3827 self.expected = { 3828 0: {'ENVELOPE': 3829 ['thursday', 'it is a message', 3830 [[None, None, 'user', 'domain']], 3831 [[None, None, 'user', 'domain']], 3832 [[None, None, 'user', 'domain']], 3833 [[None, None, 'resu', 'domain']], 3834 None, None, None, 'id-id-id-yayaya'] 3835 } 3836 } 3837 return self._fetchWork(uid) 3838 3839 def testFetchEnvelopeUID(self): 3840 return self.testFetchEnvelope(1) 3841 3842 3843 def test_fetchBodyStructure(self, uid=0): 3844 """ 3845 L{IMAP4Client.fetchBodyStructure} issues a I{FETCH BODYSTRUCTURE} 3846 command and returns a Deferred which fires with a structure giving the 3847 result of parsing the server's response. The structure is a list 3848 reflecting the parenthesized data sent by the server, as described by 3849 RFC 3501, section 7.4.2. 3850 """ 3851 self.function = self.client.fetchBodyStructure 3852 self.messages = '3:9,10:*' 3853 self.msgObjs = [FakeyMessage({ 3854 'content-type': 'text/plain; name=thing; key="value"', 3855 'content-id': 'this-is-the-content-id', 3856 'content-description': 'describing-the-content-goes-here!', 3857 'content-transfer-encoding': '8BIT', 3858 'content-md5': 'abcdef123456', 3859 'content-disposition': 'attachment; filename=monkeys', 3860 'content-language': 'es', 3861 'content-location': 'http://example.com/monkeys', 3862 }, (), '', 'Body\nText\nGoes\nHere\n', 919293, None)] 3863 self.expected = {0: {'BODYSTRUCTURE': [ 3864 'text', 'plain', ['key', 'value', 'name', 'thing'], 3865 'this-is-the-content-id', 'describing-the-content-goes-here!', 3866 '8BIT', '20', '4', 'abcdef123456', 3867 ['attachment', ['filename', 'monkeys']], 'es', 3868 'http://example.com/monkeys']}} 3869 return self._fetchWork(uid) 3870 3871 3872 def testFetchBodyStructureUID(self): 3873 """ 3874 If passed C{True} for the C{uid} argument, C{fetchBodyStructure} can 3875 also issue a I{UID FETCH BODYSTRUCTURE} command. 3876 """ 3877 return self.test_fetchBodyStructure(1) 3878 3879 3880 def test_fetchBodyStructureMultipart(self, uid=0): 3881 """ 3882 L{IMAP4Client.fetchBodyStructure} can also parse the response to a 3883 I{FETCH BODYSTRUCTURE} command for a multipart message. 3884 """ 3885 self.function = self.client.fetchBodyStructure 3886 self.messages = '3:9,10:*' 3887 innerMessage = FakeyMessage({ 3888 'content-type': 'text/plain; name=thing; key="value"', 3889 'content-id': 'this-is-the-content-id', 3890 'content-description': 'describing-the-content-goes-here!', 3891 'content-transfer-encoding': '8BIT', 3892 'content-language': 'fr', 3893 'content-md5': '123456abcdef', 3894 'content-disposition': 'inline', 3895 'content-location': 'outer space', 3896 }, (), '', 'Body\nText\nGoes\nHere\n', 919293, None) 3897 self.msgObjs = [FakeyMessage({ 3898 'content-type': 'multipart/mixed; boundary="xyz"', 3899 'content-language': 'en', 3900 'content-location': 'nearby', 3901 }, (), '', '', 919293, [innerMessage])] 3902 self.expected = {0: {'BODYSTRUCTURE': [ 3903 ['text', 'plain', ['key', 'value', 'name', 'thing'], 3904 'this-is-the-content-id', 'describing-the-content-goes-here!', 3905 '8BIT', '20', '4', '123456abcdef', ['inline', None], 'fr', 3906 'outer space'], 3907 'mixed', ['boundary', 'xyz'], None, 'en', 'nearby' 3908 ]}} 3909 return self._fetchWork(uid) 3910 3911 3912 def testFetchSimplifiedBody(self, uid=0): 3913 self.function = self.client.fetchSimplifiedBody 3914 self.messages = '21' 3915 self.msgObjs = [FakeyMessage({}, (), '', 'Yea whatever', 91825, 3916 [FakeyMessage({'content-type': 'image/jpg'}, (), '', 3917 'Body Body Body', None, None 3918 )] 3919 )] 3920 self.expected = {0: 3921 {'BODY': 3922 [None, None, None, None, None, None, 3923 '12' 3924 ] 3925 } 3926 } 3927 3928 return self._fetchWork(uid) 3929 3930 def testFetchSimplifiedBodyUID(self): 3931 return self.testFetchSimplifiedBody(1) 3932 3933 def testFetchSimplifiedBodyText(self, uid=0): 3934 self.function = self.client.fetchSimplifiedBody 3935 self.messages = '21' 3936 self.msgObjs = [FakeyMessage({'content-type': 'text/plain'}, 3937 (), '', 'Yea whatever', 91825, None)] 3938 self.expected = {0: 3939 {'BODY': 3940 ['text', 'plain', None, None, None, None, 3941 '12', '1' 3942 ] 3943 } 3944 } 3945 3946 return self._fetchWork(uid) 3947 3948 def testFetchSimplifiedBodyTextUID(self): 3949 return self.testFetchSimplifiedBodyText(1) 3950 3951 def testFetchSimplifiedBodyRFC822(self, uid=0): 3952 self.function = self.client.fetchSimplifiedBody 3953 self.messages = '21' 3954 self.msgObjs = [FakeyMessage({'content-type': 'message/rfc822'}, 3955 (), '', 'Yea whatever', 91825, 3956 [FakeyMessage({'content-type': 'image/jpg'}, (), '', 3957 'Body Body Body', None, None 3958 )] 3959 )] 3960 self.expected = {0: 3961 {'BODY': 3962 ['message', 'rfc822', None, None, None, None, 3963 '12', [None, None, [[None, None, None]], 3964 [[None, None, None]], None, None, None, 3965 None, None, None], ['image', 'jpg', None, 3966 None, None, None, '14'], '1' 3967 ] 3968 } 3969 } 3970 3971 return self._fetchWork(uid) 3972 3973 def testFetchSimplifiedBodyRFC822UID(self): 3974 return self.testFetchSimplifiedBodyRFC822(1) 3975 3976 3977 def test_fetchSimplifiedBodyMultipart(self): 3978 """ 3979 L{IMAP4Client.fetchSimplifiedBody} returns a dictionary mapping message 3980 sequence numbers to fetch responses for the corresponding messages. In 3981 particular, for a multipart message, the value in the dictionary maps 3982 the string C{"BODY"} to a list giving the body structure information for 3983 that message, in the form of a list of subpart body structure 3984 information followed by the subtype of the message (eg C{"alternative"} 3985 for a I{multipart/alternative} message). This structure is self-similar 3986 in the case where a subpart is itself multipart. 3987 """ 3988 self.function = self.client.fetchSimplifiedBody 3989 self.messages = '21' 3990 3991 # A couple non-multipart messages to use as the inner-most payload 3992 singles = [ 3993 FakeyMessage( 3994 {'content-type': 'text/plain'}, 3995 (), 'date', 'Stuff', 54321, None), 3996 FakeyMessage( 3997 {'content-type': 'text/html'}, 3998 (), 'date', 'Things', 32415, None)] 3999 4000 # A multipart/alternative message containing the above non-multipart 4001 # messages. This will be the payload of the outer-most message. 4002 alternative = FakeyMessage( 4003 {'content-type': 'multipart/alternative'}, 4004 (), '', 'Irrelevant', 12345, singles) 4005 4006 # The outer-most message, also with a multipart type, containing just 4007 # the single middle message. 4008 mixed = FakeyMessage( 4009 # The message is multipart/mixed 4010 {'content-type': 'multipart/mixed'}, 4011 (), '', 'RootOf', 98765, [alternative]) 4012 4013 self.msgObjs = [mixed] 4014 4015 self.expected = { 4016 0: {'BODY': [ 4017 [['text', 'plain', None, None, None, None, '5', '1'], 4018 ['text', 'html', None, None, None, None, '6', '1'], 4019 'alternative'], 4020 'mixed']}} 4021 4022 return self._fetchWork(False) 4023 4024 4025 def testFetchMessage(self, uid=0): 4026 self.function = self.client.fetchMessage 4027 self.messages = '1,3,7,10101' 4028 self.msgObjs = [ 4029 FakeyMessage({'Header': 'Value'}, (), '', 'BODY TEXT\r\n', 91, None), 4030 ] 4031 self.expected = { 4032 0: {'RFC822': 'Header: Value\r\n\r\nBODY TEXT\r\n'} 4033 } 4034 return self._fetchWork(uid) 4035 4036 def testFetchMessageUID(self): 4037 return self.testFetchMessage(1) 4038 4039 def testFetchHeaders(self, uid=0): 4040 self.function = self.client.fetchHeaders 4041 self.messages = '9,6,2' 4042 self.msgObjs = [ 4043 FakeyMessage({'H1': 'V1', 'H2': 'V2'}, (), '', '', 99, None), 4044 ] 4045 self.expected = { 4046 0: {'RFC822.HEADER': imap4._formatHeaders({'H1': 'V1', 'H2': 'V2'})}, 4047 } 4048 return self._fetchWork(uid) 4049 4050 def testFetchHeadersUID(self): 4051 return self.testFetchHeaders(1) 4052 4053 def testFetchBody(self, uid=0): 4054 self.function = self.client.fetchBody 4055 self.messages = '1,2,3,4,5,6,7' 4056 self.msgObjs = [ 4057 FakeyMessage({'Header': 'Value'}, (), '', 'Body goes here\r\n', 171, None), 4058 ] 4059 self.expected = { 4060 0: {'RFC822.TEXT': 'Body goes here\r\n'}, 4061 } 4062 return self._fetchWork(uid) 4063 4064 def testFetchBodyUID(self): 4065 return self.testFetchBody(1) 4066 4067 def testFetchBodyParts(self): 4068 """ 4069 Test the server's handling of requests for specific body sections. 4070 """ 4071 self.function = self.client.fetchSpecific 4072 self.messages = '1' 4073 outerBody = '' 4074 innerBody1 = 'Contained body message text. Squarge.' 4075 innerBody2 = 'Secondary <i>message</i> text of squarge body.' 4076 headers = util.OrderedDict() 4077 headers['from'] = 'sender@host' 4078 headers['to'] = 'recipient@domain' 4079 headers['subject'] = 'booga booga boo' 4080 headers['content-type'] = 'multipart/alternative; boundary="xyz"' 4081 innerHeaders = util.OrderedDict() 4082 innerHeaders['subject'] = 'this is subject text' 4083 innerHeaders['content-type'] = 'text/plain' 4084 innerHeaders2 = util.OrderedDict() 4085 innerHeaders2['subject'] = '<b>this is subject</b>' 4086 innerHeaders2['content-type'] = 'text/html' 4087 self.msgObjs = [FakeyMessage( 4088 headers, (), None, outerBody, 123, 4089 [FakeyMessage(innerHeaders, (), None, innerBody1, None, None), 4090 FakeyMessage(innerHeaders2, (), None, innerBody2, None, None)])] 4091 self.expected = { 4092 0: [['BODY', ['1'], 'Contained body message text. Squarge.']]} 4093 4094 def result(R): 4095 self.result = R 4096 4097 self.connected.addCallback( 4098 lambda _: self.function(self.messages, headerNumber=1)) 4099 self.connected.addCallback(result) 4100 self.connected.addCallback(self._cbStopClient) 4101 self.connected.addErrback(self._ebGeneral) 4102 4103 d = loopback.loopbackTCP(self.server, self.client, noisy=False) 4104 d.addCallback(lambda ign: self.assertEqual(self.result, self.expected)) 4105 return d 4106 4107 4108 def test_fetchBodyPartOfNonMultipart(self): 4109 """ 4110 Single-part messages have an implicit first part which clients 4111 should be able to retrieve explicitly. Test that a client 4112 requesting part 1 of a text/plain message receives the body of the 4113 text/plain part. 4114 """ 4115 self.function = self.client.fetchSpecific 4116 self.messages = '1' 4117 parts = [1] 4118 outerBody = 'DA body' 4119 headers = util.OrderedDict() 4120 headers['from'] = 'sender@host' 4121 headers['to'] = 'recipient@domain' 4122 headers['subject'] = 'booga booga boo' 4123 headers['content-type'] = 'text/plain' 4124 self.msgObjs = [FakeyMessage( 4125 headers, (), None, outerBody, 123, None)] 4126 4127 self.expected = {0: [['BODY', ['1'], 'DA body']]} 4128 4129 def result(R): 4130 self.result = R 4131 4132 self.connected.addCallback( 4133 lambda _: self.function(self.messages, headerNumber=parts)) 4134 self.connected.addCallback(result) 4135 self.connected.addCallback(self._cbStopClient) 4136 self.connected.addErrback(self._ebGeneral) 4137 4138 d = loopback.loopbackTCP(self.server, self.client, noisy=False) 4139 d.addCallback(lambda ign: self.assertEqual(self.result, self.expected)) 4140 return d 4141 4142 4143 def testFetchSize(self, uid=0): 4144 self.function = self.client.fetchSize 4145 self.messages = '1:100,2:*' 4146 self.msgObjs = [ 4147 FakeyMessage({}, (), '', 'x' * 20, 123, None), 4148 ] 4149 self.expected = { 4150 0: {'RFC822.SIZE': '20'}, 4151 } 4152 return self._fetchWork(uid) 4153 4154 def testFetchSizeUID(self): 4155 return self.testFetchSize(1) 4156 4157 def testFetchFull(self, uid=0): 4158 self.function = self.client.fetchFull 4159 self.messages = '1,3' 4160 self.msgObjs = [ 4161 FakeyMessage({}, ('\\XYZ', '\\YZX', 'Abc'), 4162 'Sun, 25 Jul 2010 06:20:30 -0400 (EDT)', 4163 'xyz' * 2, 654, None), 4164 FakeyMessage({}, ('\\One', '\\Two', 'Three'), 4165 'Mon, 14 Apr 2003 19:43:44 -0400', 4166 'abc' * 4, 555, None), 4167 ] 4168 self.expected = { 4169 0: {'FLAGS': ['\\XYZ', '\\YZX', 'Abc'], 4170 'INTERNALDATE': '25-Jul-2010 06:20:30 -0400', 4171 'RFC822.SIZE': '6', 4172 'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None], 4173 'BODY': [None, None, None, None, None, None, '6']}, 4174 1: {'FLAGS': ['\\One', '\\Two', 'Three'], 4175 'INTERNALDATE': '14-Apr-2003 19:43:44 -0400', 4176 'RFC822.SIZE': '12', 4177 'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None], 4178 'BODY': [None, None, None, None, None, None, '12']}, 4179 } 4180 return self._fetchWork(uid) 4181 4182 def testFetchFullUID(self): 4183 return self.testFetchFull(1) 4184 4185 def testFetchAll(self, uid=0): 4186 self.function = self.client.fetchAll 4187 self.messages = '1,2:3' 4188 self.msgObjs = [ 4189 FakeyMessage({}, (), 'Mon, 14 Apr 2003 19:43:44 +0400', 4190 'Lalala', 10101, None), 4191 FakeyMessage({}, (), 'Tue, 15 Apr 2003 19:43:44 +0200', 4192 'Alalal', 20202, None), 4193 ] 4194 self.expected = { 4195 0: {'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None], 4196 'RFC822.SIZE': '6', 4197 'INTERNALDATE': '14-Apr-2003 19:43:44 +0400', 4198 'FLAGS': []}, 4199 1: {'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None], 4200 'RFC822.SIZE': '6', 4201 'INTERNALDATE': '15-Apr-2003 19:43:44 +0200', 4202 'FLAGS': []}, 4203 } 4204 return self._fetchWork(uid) 4205 4206 def testFetchAllUID(self): 4207 return self.testFetchAll(1) 4208 4209 def testFetchFast(self, uid=0): 4210 self.function = self.client.fetchFast 4211 self.messages = '1' 4212 self.msgObjs = [ 4213 FakeyMessage({}, ('\\X',), '19 Mar 2003 19:22:21 -0500', '', 9, None), 4214 ] 4215 self.expected = { 4216 0: {'FLAGS': ['\\X'], 4217 'INTERNALDATE': '19-Mar-2003 19:22:21 -0500', 4218 'RFC822.SIZE': '0'}, 4219 } 4220 return self._fetchWork(uid) 4221 4222 def testFetchFastUID(self): 4223 return self.testFetchFast(1) 4224 4225 4226 4227class DefaultSearchTestCase(IMAP4HelperMixin, unittest.TestCase): 4228 """ 4229 Test the behavior of the server's SEARCH implementation, particularly in 4230 the face of unhandled search terms. 4231 """ 4232 def setUp(self): 4233 self.server = imap4.IMAP4Server() 4234 self.server.state = 'select' 4235 self.server.mbox = self 4236 self.connected = defer.Deferred() 4237 self.client = SimpleClient(self.connected) 4238 self.msgObjs = [ 4239 FakeyMessage({}, (), '', '', 999, None), 4240 FakeyMessage({}, (), '', '', 10101, None), 4241 FakeyMessage({}, (), '', '', 12345, None), 4242 FakeyMessage({}, (), '', '', 20001, None), 4243 FakeyMessage({}, (), '', '', 20002, None), 4244 ] 4245 4246 4247 def fetch(self, messages, uid): 4248 """ 4249 Pretend to be a mailbox and let C{self.server} lookup messages on me. 4250 """ 4251 return zip(range(1, len(self.msgObjs) + 1), self.msgObjs) 4252 4253 4254 def _messageSetSearchTest(self, queryTerms, expectedMessages): 4255 """ 4256 Issue a search with given query and verify that the returned messages 4257 match the given expected messages. 4258 4259 @param queryTerms: A string giving the search query. 4260 @param expectedMessages: A list of the message sequence numbers 4261 expected as the result of the search. 4262 @return: A L{Deferred} which fires when the test is complete. 4263 """ 4264 def search(): 4265 return self.client.search(queryTerms) 4266 4267 d = self.connected.addCallback(strip(search)) 4268 def searched(results): 4269 self.assertEqual(results, expectedMessages) 4270 d.addCallback(searched) 4271 d.addCallback(self._cbStopClient) 4272 d.addErrback(self._ebGeneral) 4273 self.loopback() 4274 return d 4275 4276 4277 def test_searchMessageSet(self): 4278 """ 4279 Test that a search which starts with a message set properly limits 4280 the search results to messages in that set. 4281 """ 4282 return self._messageSetSearchTest('1', [1]) 4283 4284 4285 def test_searchMessageSetWithStar(self): 4286 """ 4287 If the search filter ends with a star, all the message from the 4288 starting point are returned. 4289 """ 4290 return self._messageSetSearchTest('2:*', [2, 3, 4, 5]) 4291 4292 4293 def test_searchMessageSetWithStarFirst(self): 4294 """ 4295 If the search filter starts with a star, the result should be identical 4296 with if the filter would end with a star. 4297 """ 4298 return self._messageSetSearchTest('*:2', [2, 3, 4, 5]) 4299 4300 4301 def test_searchMessageSetUIDWithStar(self): 4302 """ 4303 If the search filter ends with a star, all the message from the 4304 starting point are returned (also for the SEARCH UID case). 4305 """ 4306 return self._messageSetSearchTest('UID 10000:*', [2, 3, 4, 5]) 4307 4308 4309 def test_searchMessageSetUIDWithStarFirst(self): 4310 """ 4311 If the search filter starts with a star, the result should be identical 4312 with if the filter would end with a star (also for the SEARCH UID case). 4313 """ 4314 return self._messageSetSearchTest('UID *:10000', [2, 3, 4, 5]) 4315 4316 4317 def test_searchMessageSetUIDWithStarAndHighStart(self): 4318 """ 4319 A search filter of 1234:* should include the UID of the last message in 4320 the mailbox, even if its UID is less than 1234. 4321 """ 4322 # in our fake mbox the highest message UID is 20002 4323 return self._messageSetSearchTest('UID 30000:*', [5]) 4324 4325 4326 def test_searchMessageSetWithList(self): 4327 """ 4328 If the search filter contains nesting terms, one of which includes a 4329 message sequence set with a wildcard, IT ALL WORKS GOOD. 4330 """ 4331 # 6 is bigger than the biggest message sequence number, but that's 4332 # okay, because N:* includes the biggest message sequence number even 4333 # if N is bigger than that (read the rfc nub). 4334 return self._messageSetSearchTest('(6:*)', [5]) 4335 4336 4337 def test_searchOr(self): 4338 """ 4339 If the search filter contains an I{OR} term, all messages 4340 which match either subexpression are returned. 4341 """ 4342 return self._messageSetSearchTest('OR 1 2', [1, 2]) 4343 4344 4345 def test_searchOrMessageSet(self): 4346 """ 4347 If the search filter contains an I{OR} term with a 4348 subexpression which includes a message sequence set wildcard, 4349 all messages in that set are considered for inclusion in the 4350 results. 4351 """ 4352 return self._messageSetSearchTest('OR 2:* 2:*', [2, 3, 4, 5]) 4353 4354 4355 def test_searchNot(self): 4356 """ 4357 If the search filter contains a I{NOT} term, all messages 4358 which do not match the subexpression are returned. 4359 """ 4360 return self._messageSetSearchTest('NOT 3', [1, 2, 4, 5]) 4361 4362 4363 def test_searchNotMessageSet(self): 4364 """ 4365 If the search filter contains a I{NOT} term with a 4366 subexpression which includes a message sequence set wildcard, 4367 no messages in that set are considered for inclusion in the 4368 result. 4369 """ 4370 return self._messageSetSearchTest('NOT 2:*', [1]) 4371 4372 4373 def test_searchAndMessageSet(self): 4374 """ 4375 If the search filter contains multiple terms implicitly 4376 conjoined with a message sequence set wildcard, only the 4377 intersection of the results of each term are returned. 4378 """ 4379 return self._messageSetSearchTest('2:* 3', [3]) 4380 4381 def test_searchInvalidCriteria(self): 4382 """ 4383 If the search criteria is not a valid key, a NO result is returned to 4384 the client (resulting in an error callback), and an IllegalQueryError is 4385 logged on the server side. 4386 """ 4387 queryTerms = 'FOO' 4388 def search(): 4389 return self.client.search(queryTerms) 4390 4391 d = self.connected.addCallback(strip(search)) 4392 d = self.assertFailure(d, imap4.IMAP4Exception) 4393 4394 def errorReceived(results): 4395 """ 4396 Verify that the server logs an IllegalQueryError and the 4397 client raises an IMAP4Exception with 'Search failed:...' 4398 """ 4399 self.client.transport.loseConnection() 4400 self.server.transport.loseConnection() 4401 4402 # Check what the server logs 4403 errors = self.flushLoggedErrors(imap4.IllegalQueryError) 4404 self.assertEqual(len(errors), 1) 4405 4406 # Verify exception given to client has the correct message 4407 self.assertEqual( 4408 "SEARCH failed: Invalid search command FOO", str(results)) 4409 4410 d.addCallback(errorReceived) 4411 d.addErrback(self._ebGeneral) 4412 self.loopback() 4413 return d 4414 4415 4416 4417class FetchSearchStoreTestCase(unittest.TestCase, IMAP4HelperMixin): 4418 implements(imap4.ISearchableMailbox) 4419 4420 def setUp(self): 4421 self.expected = self.result = None 4422 self.server_received_query = None 4423 self.server_received_uid = None 4424 self.server_received_parts = None 4425 self.server_received_messages = None 4426 4427 self.server = imap4.IMAP4Server() 4428 self.server.state = 'select' 4429 self.server.mbox = self 4430 self.connected = defer.Deferred() 4431 self.client = SimpleClient(self.connected) 4432 4433 def search(self, query, uid): 4434 # Look for a specific bad query, so we can verify we handle it properly 4435 if query == ['FOO']: 4436 raise imap4.IllegalQueryError("FOO is not a valid search criteria") 4437 4438 self.server_received_query = query 4439 self.server_received_uid = uid 4440 return self.expected 4441 4442 def addListener(self, *a, **kw): 4443 pass 4444 removeListener = addListener 4445 4446 def _searchWork(self, uid): 4447 def search(): 4448 return self.client.search(self.query, uid=uid) 4449 def result(R): 4450 self.result = R 4451 4452 self.connected.addCallback(strip(search) 4453 ).addCallback(result 4454 ).addCallback(self._cbStopClient 4455 ).addErrback(self._ebGeneral) 4456 4457 def check(ignored): 4458 # Ensure no short-circuiting wierdness is going on 4459 self.failIf(self.result is self.expected) 4460 4461 self.assertEqual(self.result, self.expected) 4462 self.assertEqual(self.uid, self.server_received_uid) 4463 self.assertEqual( 4464 imap4.parseNestedParens(self.query), 4465 self.server_received_query 4466 ) 4467 d = loopback.loopbackTCP(self.server, self.client, noisy=False) 4468 d.addCallback(check) 4469 return d 4470 4471 def testSearch(self): 4472 self.query = imap4.Or( 4473 imap4.Query(header=('subject', 'substring')), 4474 imap4.Query(larger=1024, smaller=4096), 4475 ) 4476 self.expected = [1, 4, 5, 7] 4477 self.uid = 0 4478 return self._searchWork(0) 4479 4480 def testUIDSearch(self): 4481 self.query = imap4.Or( 4482 imap4.Query(header=('subject', 'substring')), 4483 imap4.Query(larger=1024, smaller=4096), 4484 ) 4485 self.uid = 1 4486 self.expected = [1, 2, 3] 4487 return self._searchWork(1) 4488 4489 def getUID(self, msg): 4490 try: 4491 return self.expected[msg]['UID'] 4492 except (TypeError, IndexError): 4493 return self.expected[msg-1] 4494 except KeyError: 4495 return 42 4496 4497 def fetch(self, messages, uid): 4498 self.server_received_uid = uid 4499 self.server_received_messages = str(messages) 4500 return self.expected 4501 4502 def _fetchWork(self, fetch): 4503 def result(R): 4504 self.result = R 4505 4506 self.connected.addCallback(strip(fetch) 4507 ).addCallback(result 4508 ).addCallback(self._cbStopClient 4509 ).addErrback(self._ebGeneral) 4510 4511 def check(ignored): 4512 # Ensure no short-circuiting wierdness is going on 4513 self.failIf(self.result is self.expected) 4514 4515 self.parts and self.parts.sort() 4516 self.server_received_parts and self.server_received_parts.sort() 4517 4518 if self.uid: 4519 for (k, v) in self.expected.items(): 4520 v['UID'] = str(k) 4521 4522 self.assertEqual(self.result, self.expected) 4523 self.assertEqual(self.uid, self.server_received_uid) 4524 self.assertEqual(self.parts, self.server_received_parts) 4525 self.assertEqual(imap4.parseIdList(self.messages), 4526 imap4.parseIdList(self.server_received_messages)) 4527 4528 d = loopback.loopbackTCP(self.server, self.client, noisy=False) 4529 d.addCallback(check) 4530 return d 4531 4532 4533 def test_invalidTerm(self): 4534 """ 4535 If, as part of a search, an ISearchableMailbox raises an 4536 IllegalQueryError (e.g. due to invalid search criteria), client sees a 4537 failure response, and an IllegalQueryError is logged on the server. 4538 """ 4539 query = 'FOO' 4540 4541 def search(): 4542 return self.client.search(query) 4543 4544 d = self.connected.addCallback(strip(search)) 4545 d = self.assertFailure(d, imap4.IMAP4Exception) 4546 4547 def errorReceived(results): 4548 """ 4549 Verify that the server logs an IllegalQueryError and the 4550 client raises an IMAP4Exception with 'Search failed:...' 4551 """ 4552 self.client.transport.loseConnection() 4553 self.server.transport.loseConnection() 4554 4555 # Check what the server logs 4556 errors = self.flushLoggedErrors(imap4.IllegalQueryError) 4557 self.assertEqual(len(errors), 1) 4558 4559 # Verify exception given to client has the correct message 4560 self.assertEqual( 4561 "SEARCH failed: FOO is not a valid search criteria", 4562 str(results)) 4563 4564 d.addCallback(errorReceived) 4565 d.addErrback(self._ebGeneral) 4566 self.loopback() 4567 return d 4568 4569 4570 4571class FakeMailbox: 4572 def __init__(self): 4573 self.args = [] 4574 def addMessage(self, body, flags, date): 4575 self.args.append((body, flags, date)) 4576 return defer.succeed(None) 4577 4578class FeaturefulMessage: 4579 implements(imap4.IMessageFile) 4580 4581 def getFlags(self): 4582 return 'flags' 4583 4584 def getInternalDate(self): 4585 return 'internaldate' 4586 4587 def open(self): 4588 return StringIO("open") 4589 4590class MessageCopierMailbox: 4591 implements(imap4.IMessageCopier) 4592 4593 def __init__(self): 4594 self.msgs = [] 4595 4596 def copy(self, msg): 4597 self.msgs.append(msg) 4598 return len(self.msgs) 4599 4600class CopyWorkerTestCase(unittest.TestCase): 4601 def testFeaturefulMessage(self): 4602 s = imap4.IMAP4Server() 4603 4604 # Yes. I am grabbing this uber-non-public method to test it. 4605 # It is complex. It needs to be tested directly! 4606 # Perhaps it should be refactored, simplified, or split up into 4607 # not-so-private components, but that is a task for another day. 4608 4609 # Ha ha! Addendum! Soon it will be split up, and this test will 4610 # be re-written to just use the default adapter for IMailbox to 4611 # IMessageCopier and call .copy on that adapter. 4612 f = s._IMAP4Server__cbCopy 4613 4614 m = FakeMailbox() 4615 d = f([(i, FeaturefulMessage()) for i in range(1, 11)], 'tag', m) 4616 4617 def cbCopy(results): 4618 for a in m.args: 4619 self.assertEqual(a[0].read(), "open") 4620 self.assertEqual(a[1], "flags") 4621 self.assertEqual(a[2], "internaldate") 4622 4623 for (status, result) in results: 4624 self.failUnless(status) 4625 self.assertEqual(result, None) 4626 4627 return d.addCallback(cbCopy) 4628 4629 4630 def testUnfeaturefulMessage(self): 4631 s = imap4.IMAP4Server() 4632 4633 # See above comment 4634 f = s._IMAP4Server__cbCopy 4635 4636 m = FakeMailbox() 4637 msgs = [FakeyMessage({'Header-Counter': str(i)}, (), 'Date', 'Body %d' % (i,), i + 10, None) for i in range(1, 11)] 4638 d = f([im for im in zip(range(1, 11), msgs)], 'tag', m) 4639 4640 def cbCopy(results): 4641 seen = [] 4642 for a in m.args: 4643 seen.append(a[0].read()) 4644 self.assertEqual(a[1], ()) 4645 self.assertEqual(a[2], "Date") 4646 4647 seen.sort() 4648 exp = ["Header-Counter: %d\r\n\r\nBody %d" % (i, i) for i in range(1, 11)] 4649 exp.sort() 4650 self.assertEqual(seen, exp) 4651 4652 for (status, result) in results: 4653 self.failUnless(status) 4654 self.assertEqual(result, None) 4655 4656 return d.addCallback(cbCopy) 4657 4658 def testMessageCopier(self): 4659 s = imap4.IMAP4Server() 4660 4661 # See above comment 4662 f = s._IMAP4Server__cbCopy 4663 4664 m = MessageCopierMailbox() 4665 msgs = [object() for i in range(1, 11)] 4666 d = f([im for im in zip(range(1, 11), msgs)], 'tag', m) 4667 4668 def cbCopy(results): 4669 self.assertEqual(results, zip([1] * 10, range(1, 11))) 4670 for (orig, new) in zip(msgs, m.msgs): 4671 self.assertIdentical(orig, new) 4672 4673 return d.addCallback(cbCopy) 4674 4675 4676class TLSTestCase(IMAP4HelperMixin, unittest.TestCase): 4677 serverCTX = ServerTLSContext and ServerTLSContext() 4678 clientCTX = ClientTLSContext and ClientTLSContext() 4679 4680 def loopback(self): 4681 return loopback.loopbackTCP(self.server, self.client, noisy=False) 4682 4683 def testAPileOfThings(self): 4684 SimpleServer.theAccount.addMailbox('inbox') 4685 called = [] 4686 def login(): 4687 called.append(None) 4688 return self.client.login('testuser', 'password-test') 4689 def list(): 4690 called.append(None) 4691 return self.client.list('inbox', '%') 4692 def status(): 4693 called.append(None) 4694 return self.client.status('inbox', 'UIDNEXT') 4695 def examine(): 4696 called.append(None) 4697 return self.client.examine('inbox') 4698 def logout(): 4699 called.append(None) 4700 return self.client.logout() 4701 4702 self.client.requireTransportSecurity = True 4703 4704 methods = [login, list, status, examine, logout] 4705 map(self.connected.addCallback, map(strip, methods)) 4706 self.connected.addCallbacks(self._cbStopClient, self._ebGeneral) 4707 def check(ignored): 4708 self.assertEqual(self.server.startedTLS, True) 4709 self.assertEqual(self.client.startedTLS, True) 4710 self.assertEqual(len(called), len(methods)) 4711 d = self.loopback() 4712 d.addCallback(check) 4713 return d 4714 4715 def testLoginLogin(self): 4716 self.server.checker.addUser('testuser', 'password-test') 4717 success = [] 4718 self.client.registerAuthenticator(imap4.LOGINAuthenticator('testuser')) 4719 self.connected.addCallback( 4720 lambda _: self.client.authenticate('password-test') 4721 ).addCallback( 4722 lambda _: self.client.logout() 4723 ).addCallback(success.append 4724 ).addCallback(self._cbStopClient 4725 ).addErrback(self._ebGeneral) 4726 4727 d = self.loopback() 4728 d.addCallback(lambda x : self.assertEqual(len(success), 1)) 4729 return d 4730 4731 4732 def test_startTLS(self): 4733 """ 4734 L{IMAP4Client.startTLS} triggers TLS negotiation and returns a 4735 L{Deferred} which fires after the client's transport is using 4736 encryption. 4737 """ 4738 success = [] 4739 self.connected.addCallback(lambda _: self.client.startTLS()) 4740 def checkSecure(ignored): 4741 self.assertTrue( 4742 interfaces.ISSLTransport.providedBy(self.client.transport)) 4743 self.connected.addCallback(checkSecure) 4744 self.connected.addCallback(self._cbStopClient) 4745 self.connected.addCallback(success.append) 4746 self.connected.addErrback(self._ebGeneral) 4747 4748 d = self.loopback() 4749 d.addCallback(lambda x : self.failUnless(success)) 4750 return defer.gatherResults([d, self.connected]) 4751 4752 4753 def testFailedStartTLS(self): 4754 failure = [] 4755 def breakServerTLS(ign): 4756 self.server.canStartTLS = False 4757 4758 self.connected.addCallback(breakServerTLS) 4759 self.connected.addCallback(lambda ign: self.client.startTLS()) 4760 self.connected.addErrback(lambda err: failure.append(err.trap(imap4.IMAP4Exception))) 4761 self.connected.addCallback(self._cbStopClient) 4762 self.connected.addErrback(self._ebGeneral) 4763 4764 def check(ignored): 4765 self.failUnless(failure) 4766 self.assertIdentical(failure[0], imap4.IMAP4Exception) 4767 return self.loopback().addCallback(check) 4768 4769 4770 4771class SlowMailbox(SimpleMailbox): 4772 howSlow = 2 4773 callLater = None 4774 fetchDeferred = None 4775 4776 # Not a very nice implementation of fetch(), but it'll 4777 # do for the purposes of testing. 4778 def fetch(self, messages, uid): 4779 d = defer.Deferred() 4780 self.callLater(self.howSlow, d.callback, ()) 4781 self.fetchDeferred.callback(None) 4782 return d 4783 4784class Timeout(IMAP4HelperMixin, unittest.TestCase): 4785 4786 def test_serverTimeout(self): 4787 """ 4788 The *client* has a timeout mechanism which will close connections that 4789 are inactive for a period. 4790 """ 4791 c = Clock() 4792 self.server.timeoutTest = True 4793 self.client.timeout = 5 #seconds 4794 self.client.callLater = c.callLater 4795 self.selectedArgs = None 4796 4797 def login(): 4798 d = self.client.login('testuser', 'password-test') 4799 c.advance(5) 4800 d.addErrback(timedOut) 4801 return d 4802 4803 def timedOut(failure): 4804 self._cbStopClient(None) 4805 failure.trap(error.TimeoutError) 4806 4807 d = self.connected.addCallback(strip(login)) 4808 d.addErrback(self._ebGeneral) 4809 return defer.gatherResults([d, self.loopback()]) 4810 4811 4812 def test_longFetchDoesntTimeout(self): 4813 """ 4814 The connection timeout does not take effect during fetches. 4815 """ 4816 c = Clock() 4817 SlowMailbox.callLater = c.callLater 4818 SlowMailbox.fetchDeferred = defer.Deferred() 4819 self.server.callLater = c.callLater 4820 SimpleServer.theAccount.mailboxFactory = SlowMailbox 4821 SimpleServer.theAccount.addMailbox('mailbox-test') 4822 4823 self.server.setTimeout(1) 4824 4825 def login(): 4826 return self.client.login('testuser', 'password-test') 4827 def select(): 4828 self.server.setTimeout(1) 4829 return self.client.select('mailbox-test') 4830 def fetch(): 4831 return self.client.fetchUID('1:*') 4832 def stillConnected(): 4833 self.assertNotEquals(self.server.state, 'timeout') 4834 4835 def cbAdvance(ignored): 4836 for i in xrange(4): 4837 c.advance(.5) 4838 4839 SlowMailbox.fetchDeferred.addCallback(cbAdvance) 4840 4841 d1 = self.connected.addCallback(strip(login)) 4842 d1.addCallback(strip(select)) 4843 d1.addCallback(strip(fetch)) 4844 d1.addCallback(strip(stillConnected)) 4845 d1.addCallback(self._cbStopClient) 4846 d1.addErrback(self._ebGeneral) 4847 d = defer.gatherResults([d1, self.loopback()]) 4848 return d 4849 4850 4851 def test_idleClientDoesDisconnect(self): 4852 """ 4853 The *server* has a timeout mechanism which will close connections that 4854 are inactive for a period. 4855 """ 4856 c = Clock() 4857 # Hook up our server protocol 4858 transport = StringTransportWithDisconnection() 4859 transport.protocol = self.server 4860 self.server.callLater = c.callLater 4861 self.server.makeConnection(transport) 4862 4863 # Make sure we can notice when the connection goes away 4864 lost = [] 4865 connLost = self.server.connectionLost 4866 self.server.connectionLost = lambda reason: (lost.append(None), connLost(reason))[1] 4867 4868 # 2/3rds of the idle timeout elapses... 4869 c.pump([0.0] + [self.server.timeOut / 3.0] * 2) 4870 self.failIf(lost, lost) 4871 4872 # Now some more 4873 c.pump([0.0, self.server.timeOut / 2.0]) 4874 self.failUnless(lost) 4875 4876 4877 4878class Disconnection(unittest.TestCase): 4879 def testClientDisconnectFailsDeferreds(self): 4880 c = imap4.IMAP4Client() 4881 t = StringTransportWithDisconnection() 4882 c.makeConnection(t) 4883 d = self.assertFailure(c.login('testuser', 'example.com'), error.ConnectionDone) 4884 c.connectionLost(error.ConnectionDone("Connection closed")) 4885 return d 4886 4887 4888 4889class SynchronousMailbox(object): 4890 """ 4891 Trivial, in-memory mailbox implementation which can produce a message 4892 synchronously. 4893 """ 4894 def __init__(self, messages): 4895 self.messages = messages 4896 4897 4898 def fetch(self, msgset, uid): 4899 assert not uid, "Cannot handle uid requests." 4900 for msg in msgset: 4901 yield msg, self.messages[msg - 1] 4902 4903 4904 4905class StringTransportConsumer(StringTransport): 4906 producer = None 4907 streaming = None 4908 4909 def registerProducer(self, producer, streaming): 4910 self.producer = producer 4911 self.streaming = streaming 4912 4913 4914 4915class Pipelining(unittest.TestCase): 4916 """ 4917 Tests for various aspects of the IMAP4 server's pipelining support. 4918 """ 4919 messages = [ 4920 FakeyMessage({}, [], '', '0', None, None), 4921 FakeyMessage({}, [], '', '1', None, None), 4922 FakeyMessage({}, [], '', '2', None, None), 4923 ] 4924 4925 def setUp(self): 4926 self.iterators = [] 4927 4928 self.transport = StringTransportConsumer() 4929 self.server = imap4.IMAP4Server(None, None, self.iterateInReactor) 4930 self.server.makeConnection(self.transport) 4931 4932 4933 def iterateInReactor(self, iterator): 4934 d = defer.Deferred() 4935 self.iterators.append((iterator, d)) 4936 return d 4937 4938 4939 def tearDown(self): 4940 self.server.connectionLost(failure.Failure(error.ConnectionDone())) 4941 4942 4943 def test_synchronousFetch(self): 4944 """ 4945 Test that pipelined FETCH commands which can be responded to 4946 synchronously are responded to correctly. 4947 """ 4948 mailbox = SynchronousMailbox(self.messages) 4949 4950 # Skip over authentication and folder selection 4951 self.server.state = 'select' 4952 self.server.mbox = mailbox 4953 4954 # Get rid of any greeting junk 4955 self.transport.clear() 4956 4957 # Here's some pipelined stuff 4958 self.server.dataReceived( 4959 '01 FETCH 1 BODY[]\r\n' 4960 '02 FETCH 2 BODY[]\r\n' 4961 '03 FETCH 3 BODY[]\r\n') 4962 4963 # Flush anything the server has scheduled to run 4964 while self.iterators: 4965 for e in self.iterators[0][0]: 4966 break 4967 else: 4968 self.iterators.pop(0)[1].callback(None) 4969 4970 # The bodies are empty because we aren't simulating a transport 4971 # exactly correctly (we have StringTransportConsumer but we never 4972 # call resumeProducing on its producer). It doesn't matter: just 4973 # make sure the surrounding structure is okay, and that no 4974 # exceptions occurred. 4975 self.assertEqual( 4976 self.transport.value(), 4977 '* 1 FETCH (BODY[] )\r\n' 4978 '01 OK FETCH completed\r\n' 4979 '* 2 FETCH (BODY[] )\r\n' 4980 '02 OK FETCH completed\r\n' 4981 '* 3 FETCH (BODY[] )\r\n' 4982 '03 OK FETCH completed\r\n') 4983 4984 4985 4986if ClientTLSContext is None: 4987 for case in (TLSTestCase,): 4988 case.skip = "OpenSSL not present" 4989elif interfaces.IReactorSSL(reactor, None) is None: 4990 for case in (TLSTestCase,): 4991 case.skip = "Reactor doesn't support SSL" 4992 4993 4994 4995class IMAP4ServerFetchTestCase(unittest.TestCase): 4996 """ 4997 This test case is for the FETCH tests that require 4998 a C{StringTransport}. 4999 """ 5000 5001 def setUp(self): 5002 self.transport = StringTransport() 5003 self.server = imap4.IMAP4Server() 5004 self.server.state = 'select' 5005 self.server.makeConnection(self.transport) 5006 5007 5008 def test_fetchWithPartialValidArgument(self): 5009 """ 5010 If by any chance, extra bytes got appended at the end of of an valid 5011 FETCH arguments, the client should get a BAD - arguments invalid 5012 response. 5013 5014 See U{RFC 3501<http://tools.ietf.org/html/rfc3501#section-6.4.5>}, 5015 section 6.4.5, 5016 """ 5017 # We need to clear out the welcome message. 5018 self.transport.clear() 5019 # Let's send out the faulty command. 5020 self.server.dataReceived("0001 FETCH 1 FULLL\r\n") 5021 expected = "0001 BAD Illegal syntax: Invalid Argument\r\n" 5022 self.assertEqual(self.transport.value(), expected) 5023 self.transport.clear() 5024 self.server.connectionLost(error.ConnectionDone("Connection closed")) 5025