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