root/imapclient/test/test_response_parser.py

Revision 304:239f24fbd17c, 12.8 KB (checked in by Menno Smits <menno@…>, 2 weeks ago)

updated copyright date to 2012

Line 
1# Copyright (c) 2012, Menno Smits
2# Released subject to the New BSD License
3# Please see http://en.wikipedia.org/wiki/BSD_licenses
4
5'''
6Unit tests for the FetchTokeniser and FetchParser classes
7'''
8
9from datetime import datetime
10from textwrap import dedent
11from imapclient.fixed_offset import FixedOffset
12from imapclient.response_parser import parse_response, parse_fetch_response, ParseError
13from imapclient.test.util import unittest
14
15#TODO: tokenising tests
16#TODO: test invalid dates and times
17
18
19CRLF = '\r\n'
20
21class TestParseResponse(unittest.TestCase):
22
23    def test_unquoted(self):
24        self._test('FOO', 'FOO')
25        self._test('F.O:-O_0;', 'F.O:-O_0;')
26        self._test(r'\Seen', r'\Seen')
27
28    def test_string(self):
29        self._test('"TEST"', 'TEST')
30
31    def test_int(self):
32        self._test('45', 45)
33
34    def test_nil(self):
35        self._test('NIL', None)
36
37    def test_empty_tuple(self):
38        self._test('()', ())
39
40    def test_tuple(self):
41        self._test('(123 "foo" GeE)', (123, 'foo', 'GeE'))
42
43    def test_int_and_tuple(self):
44        self._test('1 (123 "foo")', (1, (123, 'foo')), wrap=False)
45
46    def test_nested_tuple(self):
47        self._test('(123 "foo" ("more" NIL) 66)',
48                   (123, "foo", ("more", None), 66))
49
50    def test_deeper_nest_tuple(self):
51        self._test('(123 "foo" ((0 1 2) "more" NIL) 66)',
52                   (123, "foo", ((0, 1, 2), "more", None), 66))
53
54    def test_complex_mixed(self):
55        self._test('((FOO "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 1152 23) '
56                   '("TEXT" "PLAIN" ("CHARSET" "US-ASCII" "NAME" "cc.diff") '
57                   '"<hi.there>" "foo" "BASE64" 4554 73) "MIXED")',
58                   (('FOO', 'PLAIN', ('CHARSET', 'US-ASCII'), None, None, '7BIT', 1152, 23),
59                    ('TEXT', 'PLAIN', ('CHARSET', 'US-ASCII', 'NAME', 'cc.diff'),
60                    '<hi.there>', 'foo', 'BASE64', 4554, 73), 'MIXED'))
61
62    def test_envelopey(self):
63        self._test('(UID 5 ENVELOPE ("internal_date" "subject" '
64                   '(("name" NIL "address1" "domain1.com")) '
65                   '((NIL NIL "address2" "domain2.com")) '
66                   '(("name" NIL "address3" "domain3.com")) '
67                   '((NIL NIL "address4" "domain4.com")) '
68                   'NIL NIL "<reply-to-id>" "<msg_id>"))',
69                   ('UID',
70                    5,
71                    'ENVELOPE',
72                    ('internal_date',
73                     'subject',
74                     (('name', None, 'address1', 'domain1.com'),),
75                     ((None, None, 'address2', 'domain2.com'),),
76                     (('name', None, 'address3', 'domain3.com'),),
77                     ((None, None, 'address4', 'domain4.com'),),
78                     None,
79                     None,
80                     '<reply-to-id>',
81                     '<msg_id>')))
82
83    def test_envelopey_quoted(self):
84        self._test('(UID 5 ENVELOPE ("internal_date" "subject with \\"quotes\\"" '
85                   '(("name" NIL "address1" "domain1.com")) '
86                   '((NIL NIL "address2" "domain2.com")) '
87                   '(("name" NIL "address3" "domain3.com")) '
88                   '((NIL NIL "address4" "domain4.com")) '
89                   'NIL NIL "<reply-to-id>" "<msg_id>"))',
90                   ('UID',
91                    5,
92                    'ENVELOPE',
93                    ('internal_date',
94                     'subject with "quotes"',
95                     (('name', None, 'address1', 'domain1.com'),),
96                     ((None, None, 'address2', 'domain2.com'),),
97                     (('name', None, 'address3', 'domain3.com'),),
98                     ((None, None, 'address4', 'domain4.com'),),
99                     None,
100                     None,
101                     '<reply-to-id>',
102                     '<msg_id>')))
103
104    def test_literal(self):
105        literal_text = add_crlf(dedent("""\
106            012
107            abc def XYZ
108            """))
109        self._test([('{18}', literal_text)], literal_text)
110
111
112    def test_literal_with_more(self):
113        literal_text = add_crlf(dedent("""\
114            012
115            abc def XYZ
116            """))
117        response = [('(12 "foo" {18}', literal_text), ")"]
118        self._test(response, (12, 'foo', literal_text))
119
120
121    def test_quoted_specials(self):
122        self._test(r'"\"foo bar\""', '"foo bar"')
123        self._test(r'"foo \"bar\""', 'foo "bar"')
124        self._test(r'"foo\\bar"', r'foo\bar')
125
126    def test_square_brackets(self):
127        self._test('foo[bar rrr]', 'foo[bar rrr]')
128        self._test('"foo[bar rrr]"', 'foo[bar rrr]')
129        self._test('[foo bar]def', '[foo bar]def')
130        self._test('(foo [bar rrr])', ('foo', '[bar rrr]'))
131        self._test('(foo foo[bar rrr])', ('foo', 'foo[bar rrr]'))
132
133    def test_incomplete_tuple(self):
134        self._test_parse_error('abc (1 2', 'Tuple incomplete before "\(1 2"')
135
136    def test_bad_literal(self):
137        self._test_parse_error([('{99}', 'abc')],
138                               'Expecting literal of size 99, got 3')
139
140    def test_bad_quoting(self):
141        self._test_parse_error('"abc next', """No closing '"'""")
142
143    def _test(self, to_parse, expected, wrap=True):
144        if wrap:
145            # convenience - expected value should be wrapped in another tuple
146            expected = (expected,)
147        if not isinstance(to_parse, list):
148            to_parse = [to_parse]
149        output = parse_response(to_parse)
150        self.assertSequenceEqual(output, expected)
151
152    def _test_parse_error(self, to_parse, expected_msg):
153        if not isinstance(to_parse, list):
154            to_parse = [to_parse]
155        self.assertRaisesRegexp(ParseError, expected_msg,
156                                parse_response, to_parse)
157
158
159class TestParseFetchResponse(unittest.TestCase):
160
161    def test_basic(self):
162        self.assertEquals(parse_fetch_response('4 ()'), {4: {'SEQ': 4}})
163
164
165    def test_none_special_case(self):
166        self.assertEquals(parse_fetch_response([None]), {})
167
168
169    def test_bad_msgid(self):
170        self.assertRaises(ParseError, parse_fetch_response, ['abc ()'])
171
172
173    def test_bad_data(self):
174        self.assertRaises(ParseError, parse_fetch_response, ['2 WHAT'])
175
176
177    def test_missing_data(self):
178        self.assertRaises(ParseError, parse_fetch_response, ['2'])
179
180
181    def test_simple_pairs(self):
182        self.assertEquals(parse_fetch_response(['23 (ABC 123 StUfF "hello")']),
183                          {23: {'ABC': 123,
184                                'STUFF': 'hello',
185                                'SEQ': 23}})
186
187
188    def test_odd_pairs(self):
189        self.assertRaises(ParseError, parse_fetch_response, ['(ONE)'])
190        self.assertRaises(ParseError, parse_fetch_response, ['(ONE TWO THREE)'])
191
192
193    def test_UID(self):
194        self.assertEquals(parse_fetch_response(['23 (UID 76)']),
195                          {76: {'SEQ': 23}})
196        self.assertEquals(parse_fetch_response(['23 (uiD 76)']),
197                          {76: {'SEQ': 23}})
198
199
200    def test_not_uid_is_key(self):
201        self.assertEquals(parse_fetch_response(['23 (UID 76)'], uid_is_key=False),
202                          {23: {'UID': 76,
203                                'SEQ': 23}})
204
205
206    def test_bad_UID(self):
207        self.assertRaises(ParseError, parse_fetch_response, ['(UID X)'])
208       
209
210    def test_FLAGS(self):
211        self.assertEquals(parse_fetch_response(['23 (FLAGS (\Seen Stuff))']),
212                          {23: {'SEQ': 23, 'FLAGS': (r'\Seen', 'Stuff')}})
213
214
215    def test_multiple_messages(self):
216        self.assertEquals(parse_fetch_response(
217                                    ["2 (FLAGS (Foo Bar)) ",
218                                     "7 (FLAGS (Baz Sneeve))"]),
219                         {
220                            2: {'FLAGS': ('Foo', 'Bar'), 'SEQ': 2},
221                            7: {'FLAGS': ('Baz', 'Sneeve'), 'SEQ': 7},
222                         })
223
224
225    def test_literals(self):
226        self.assertEquals(parse_fetch_response([('1 (RFC822.TEXT {4}', 'body'),
227                                                (' RFC822 {21}', 'Subject: test\r\n\r\nbody'),
228                                                ')']),
229                          {1: {'RFC822.TEXT': 'body',
230                               'RFC822': 'Subject: test\r\n\r\nbody',
231                               'SEQ': 1}})
232
233
234    def test_literals_and_keys_with_square_brackets(self):
235        self.assertEquals(parse_fetch_response([('1 (BODY[TEXT] {11}', 'Hi there.\r\n'), ')']),
236                          { 1: {'BODY[TEXT]': 'Hi there.\r\n',
237                                'SEQ': 1}})
238
239
240    def test_BODY_HEADER_FIELDS(self):
241        header_text = 'Subject: A subject\r\nFrom: Some one <someone@mail.com>\r\n\r\n'
242        self.assertEquals(parse_fetch_response(
243            [('123 (UID 31710 BODY[HEADER.FIELDS (from subject)] {57}', header_text), ')']),
244            { 31710: {'BODY[HEADER.FIELDS (FROM SUBJECT)]': header_text,
245                      'SEQ': 123}})
246
247    def test_BODY(self):
248         self.check_BODYish_single_part('BODY')
249         self.check_BODYish_multipart('BODY')
250
251    def test_BODYSTRUCTURE(self):
252         self.check_BODYish_single_part('BODYSTRUCTURE')
253         self.check_BODYish_multipart('BODYSTRUCTURE')
254   
255    def check_BODYish_single_part(self, respType):
256        text =  '123 (UID 317 %s ("TEXT" "PLAIN" ("CHARSET" "us-ascii") NIL NIL "7BIT" 16 1))' % respType
257        parsed = parse_fetch_response([text])
258        self.assertEquals(parsed, {317: {respType: ('TEXT', 'PLAIN', ('CHARSET', 'us-ascii'), None, None, '7BIT', 16, 1),
259                                         'SEQ': 123 }
260                                         })
261        self.assertFalse(parsed[317][respType].is_multipart)
262
263    def check_BODYish_multipart(self, respType):
264        text = '123 (UID 269 %s (("TEXT" "HTML" ("CHARSET" "us-ascii") NIL NIL "QUOTED-PRINTABLE" 55 3)' \
265                                '("TEXT" "PLAIN" ("CHARSET" "us-ascii") NIL NIL "7BIT" 26 1) "MIXED"))' \
266                                % respType
267        parsed = parse_fetch_response([text])
268        self.assertEquals(parsed, {269: {respType: ([('TEXT', 'HTML', ('CHARSET', 'us-ascii'), None, None, 'QUOTED-PRINTABLE', 55, 3),
269                                                     ('TEXT', 'PLAIN', ('CHARSET', 'us-ascii'), None, None, '7BIT', 26, 1)],
270                                                     'MIXED'),
271                                        'SEQ': 123}
272                                        })
273        self.assertTrue(parsed[269][respType].is_multipart)
274
275    def test_partial_fetch(self):
276        body = '01234567890123456789'
277        self.assertEquals(parse_fetch_response(
278            [('123 (UID 367 BODY[]<0> {20}', body), ')']),
279            { 367: {'BODY[]<0>': body,
280                    'SEQ': 123}})
281                   
282
283    def test_INTERNALDATE_normalised(self):
284        def check(date_str, expected_dt):
285            output = parse_fetch_response(['3 (INTERNALDATE "%s")' % date_str])
286            actual_dt = output[3]['INTERNALDATE']
287            self.assert_(actual_dt.tzinfo is None)   # Returned date should be in local timezone
288            expected_dt = datetime_to_native(expected_dt)
289            self.assertEquals(actual_dt, expected_dt)
290
291        check(' 9-Feb-2007 17:08:08 -0430',
292              datetime(2007, 2, 9, 17, 8, 8, 0, FixedOffset(-4*60 - 30)))
293 
294        check('12-Feb-2007 17:08:08 +0200',
295              datetime(2007, 2, 12, 17, 8, 8, 0, FixedOffset(2*60)))
296 
297        check(' 9-Dec-2007 17:08:08 +0000',
298              datetime(2007, 12, 9, 17, 8, 8, 0, FixedOffset(0)))
299
300    def test_INTERNALDATE(self):
301        def check(date_str, expected_dt):
302            output = parse_fetch_response(['3 (INTERNALDATE "%s")' % date_str], normalise_times=False)
303            actual_dt = output[3]['INTERNALDATE']
304            self.assertEquals(actual_dt, expected_dt)
305
306        check(' 9-Feb-2007 17:08:08 -0430',
307              datetime(2007, 2, 9, 17, 8, 8, 0, FixedOffset(-4*60 - 30)))
308 
309        check('12-Feb-2007 17:08:08 +0200',
310              datetime(2007, 2, 12, 17, 8, 8, 0, FixedOffset(2*60)))
311 
312        check(' 9-Dec-2007 17:08:08 +0000',
313              datetime(2007, 12, 9, 17, 8, 8, 0, FixedOffset(0)))
314
315    def test_mixed_types(self):
316        self.assertEquals(parse_fetch_response([('1 (INTERNALDATE " 9-Feb-2007 17:08:08 +0100" RFC822 {21}',
317                                                 'Subject: test\r\n\r\nbody'),
318                                                ')']),
319                          {1: {'INTERNALDATE': datetime_to_native(datetime(2007, 2, 9,
320                                                                           17, 8, 8, 0,
321                                                                           FixedOffset(60))),
322                               'RFC822': 'Subject: test\r\n\r\nbody',
323                               'SEQ': 1}})
324
325
326def add_crlf(text):
327    return CRLF.join(text.splitlines()) + CRLF
328
329
330system_offset = FixedOffset.for_system()
331def datetime_to_native(dt):
332    return dt.astimezone(system_offset).replace(tzinfo=None)
333
Note: See TracBrowser for help on using the browser.