1import datetime
2import textwrap
3import unittest
4from email import errors
5from email import policy
6from email.message import Message
7from test.test_email import TestEmailBase, parameterize
8from email import headerregistry
9from email.headerregistry import Address, Group
10
11
12DITTO = object()
13
14
15class TestHeaderRegistry(TestEmailBase):
16
17    def test_arbitrary_name_unstructured(self):
18        factory = headerregistry.HeaderRegistry()
19        h = factory('foobar', 'test')
20        self.assertIsInstance(h, headerregistry.BaseHeader)
21        self.assertIsInstance(h, headerregistry.UnstructuredHeader)
22
23    def test_name_case_ignored(self):
24        factory = headerregistry.HeaderRegistry()
25        # Whitebox check that test is valid
26        self.assertNotIn('Subject', factory.registry)
27        h = factory('Subject', 'test')
28        self.assertIsInstance(h, headerregistry.BaseHeader)
29        self.assertIsInstance(h, headerregistry.UniqueUnstructuredHeader)
30
31    class FooBase:
32        def __init__(self, *args, **kw):
33            pass
34
35    def test_override_default_base_class(self):
36        factory = headerregistry.HeaderRegistry(base_class=self.FooBase)
37        h = factory('foobar', 'test')
38        self.assertIsInstance(h, self.FooBase)
39        self.assertIsInstance(h, headerregistry.UnstructuredHeader)
40
41    class FooDefault:
42        parse = headerregistry.UnstructuredHeader.parse
43
44    def test_override_default_class(self):
45        factory = headerregistry.HeaderRegistry(default_class=self.FooDefault)
46        h = factory('foobar', 'test')
47        self.assertIsInstance(h, headerregistry.BaseHeader)
48        self.assertIsInstance(h, self.FooDefault)
49
50    def test_override_default_class_only_overrides_default(self):
51        factory = headerregistry.HeaderRegistry(default_class=self.FooDefault)
52        h = factory('subject', 'test')
53        self.assertIsInstance(h, headerregistry.BaseHeader)
54        self.assertIsInstance(h, headerregistry.UniqueUnstructuredHeader)
55
56    def test_dont_use_default_map(self):
57        factory = headerregistry.HeaderRegistry(use_default_map=False)
58        h = factory('subject', 'test')
59        self.assertIsInstance(h, headerregistry.BaseHeader)
60        self.assertIsInstance(h, headerregistry.UnstructuredHeader)
61
62    def test_map_to_type(self):
63        factory = headerregistry.HeaderRegistry()
64        h1 = factory('foobar', 'test')
65        factory.map_to_type('foobar', headerregistry.UniqueUnstructuredHeader)
66        h2 = factory('foobar', 'test')
67        self.assertIsInstance(h1, headerregistry.BaseHeader)
68        self.assertIsInstance(h1, headerregistry.UnstructuredHeader)
69        self.assertIsInstance(h2, headerregistry.BaseHeader)
70        self.assertIsInstance(h2, headerregistry.UniqueUnstructuredHeader)
71
72
73class TestHeaderBase(TestEmailBase):
74
75    factory = headerregistry.HeaderRegistry()
76
77    def make_header(self, name, value):
78        return self.factory(name, value)
79
80
81class TestBaseHeaderFeatures(TestHeaderBase):
82
83    def test_str(self):
84        h = self.make_header('subject', 'this is a test')
85        self.assertIsInstance(h, str)
86        self.assertEqual(h, 'this is a test')
87        self.assertEqual(str(h), 'this is a test')
88
89    def test_substr(self):
90        h = self.make_header('subject', 'this is a test')
91        self.assertEqual(h[5:7], 'is')
92
93    def test_has_name(self):
94        h = self.make_header('subject', 'this is a test')
95        self.assertEqual(h.name, 'subject')
96
97    def _test_attr_ro(self, attr):
98        h = self.make_header('subject', 'this is a test')
99        with self.assertRaises(AttributeError):
100            setattr(h, attr, 'foo')
101
102    def test_name_read_only(self):
103        self._test_attr_ro('name')
104
105    def test_defects_read_only(self):
106        self._test_attr_ro('defects')
107
108    def test_defects_is_tuple(self):
109        h = self.make_header('subject', 'this is a test')
110        self.assertEqual(len(h.defects), 0)
111        self.assertIsInstance(h.defects, tuple)
112        # Make sure it is still true when there are defects.
113        h = self.make_header('date', '')
114        self.assertEqual(len(h.defects), 1)
115        self.assertIsInstance(h.defects, tuple)
116
117    # XXX: FIXME
118    #def test_CR_in_value(self):
119    #    # XXX: this also re-raises the issue of embedded headers,
120    #    # need test and solution for that.
121    #    value = '\r'.join(['this is', ' a test'])
122    #    h = self.make_header('subject', value)
123    #    self.assertEqual(h, value)
124    #    self.assertDefectsEqual(h.defects, [errors.ObsoleteHeaderDefect])
125
126
127@parameterize
128class TestUnstructuredHeader(TestHeaderBase):
129
130    def string_as_value(self,
131                        source,
132                        decoded,
133                        *args):
134        l = len(args)
135        defects = args[0] if l>0 else []
136        header = 'Subject:' + (' ' if source else '')
137        folded = header + (args[1] if l>1 else source) + '\n'
138        h = self.make_header('Subject', source)
139        self.assertEqual(h, decoded)
140        self.assertDefectsEqual(h.defects, defects)
141        self.assertEqual(h.fold(policy=policy.default), folded)
142
143    string_params = {
144
145        'rfc2047_simple_quopri': (
146            '=?utf-8?q?this_is_a_test?=',
147            'this is a test',
148            [],
149            'this is a test'),
150
151        'rfc2047_gb2312_base64': (
152            '=?gb2312?b?1eLKx9bQzsSy4srUo6E=?=',
153            '\u8fd9\u662f\u4e2d\u6587\u6d4b\u8bd5\uff01',
154            [],
155            '=?utf-8?b?6L+Z5piv5Lit5paH5rWL6K+V77yB?='),
156
157        'rfc2047_simple_nonascii_quopri': (
158            '=?utf-8?q?=C3=89ric?=',
159            'Éric'),
160
161        'rfc2047_quopri_with_regular_text': (
162            'The =?utf-8?q?=C3=89ric=2C?= Himself',
163            'The Éric, Himself'),
164
165    }
166
167
168@parameterize
169class TestDateHeader(TestHeaderBase):
170
171    datestring = 'Sun, 23 Sep 2001 20:10:55 -0700'
172    utcoffset = datetime.timedelta(hours=-7)
173    tz = datetime.timezone(utcoffset)
174    dt = datetime.datetime(2001, 9, 23, 20, 10, 55, tzinfo=tz)
175
176    def test_parse_date(self):
177        h = self.make_header('date', self.datestring)
178        self.assertEqual(h, self.datestring)
179        self.assertEqual(h.datetime, self.dt)
180        self.assertEqual(h.datetime.utcoffset(), self.utcoffset)
181        self.assertEqual(h.defects, ())
182
183    def test_set_from_datetime(self):
184        h = self.make_header('date', self.dt)
185        self.assertEqual(h, self.datestring)
186        self.assertEqual(h.datetime, self.dt)
187        self.assertEqual(h.defects, ())
188
189    def test_date_header_properties(self):
190        h = self.make_header('date', self.datestring)
191        self.assertIsInstance(h, headerregistry.UniqueDateHeader)
192        self.assertEqual(h.max_count, 1)
193        self.assertEqual(h.defects, ())
194
195    def test_resent_date_header_properties(self):
196        h = self.make_header('resent-date', self.datestring)
197        self.assertIsInstance(h, headerregistry.DateHeader)
198        self.assertEqual(h.max_count, None)
199        self.assertEqual(h.defects, ())
200
201    def test_no_value_is_defect(self):
202        h = self.make_header('date', '')
203        self.assertEqual(len(h.defects), 1)
204        self.assertIsInstance(h.defects[0], errors.HeaderMissingRequiredValue)
205
206    def test_datetime_read_only(self):
207        h = self.make_header('date', self.datestring)
208        with self.assertRaises(AttributeError):
209            h.datetime = 'foo'
210
211    def test_set_date_header_from_datetime(self):
212        m = Message(policy=policy.default)
213        m['Date'] = self.dt
214        self.assertEqual(m['Date'], self.datestring)
215        self.assertEqual(m['Date'].datetime, self.dt)
216
217
218@parameterize
219class TestContentTypeHeader(TestHeaderBase):
220
221    def content_type_as_value(self,
222                              source,
223                              content_type,
224                              maintype,
225                              subtype,
226                              *args):
227        l = len(args)
228        parmdict = args[0] if l>0 else {}
229        defects =  args[1] if l>1 else []
230        decoded =  args[2] if l>2 and args[2] is not DITTO else source
231        header = 'Content-Type:' + ' ' if source else ''
232        folded = args[3] if l>3 else header + source + '\n'
233        h = self.make_header('Content-Type', source)
234        self.assertEqual(h.content_type, content_type)
235        self.assertEqual(h.maintype, maintype)
236        self.assertEqual(h.subtype, subtype)
237        self.assertEqual(h.params, parmdict)
238        with self.assertRaises(TypeError):
239            h.params['abc'] = 'xyz'   # params is read-only.
240        self.assertDefectsEqual(h.defects, defects)
241        self.assertEqual(h, decoded)
242        self.assertEqual(h.fold(policy=policy.default), folded)
243
244    content_type_params = {
245
246        # Examples from RFC 2045.
247
248        'RFC_2045_1': (
249            'text/plain; charset=us-ascii (Plain text)',
250            'text/plain',
251            'text',
252            'plain',
253            {'charset': 'us-ascii'},
254            [],
255            'text/plain; charset="us-ascii"'),
256
257        'RFC_2045_2': (
258            'text/plain; charset=us-ascii',
259            'text/plain',
260            'text',
261            'plain',
262            {'charset': 'us-ascii'},
263            [],
264            'text/plain; charset="us-ascii"'),
265
266        'RFC_2045_3': (
267            'text/plain; charset="us-ascii"',
268            'text/plain',
269            'text',
270            'plain',
271            {'charset': 'us-ascii'}),
272
273        # RFC 2045 5.2 says syntactically invalid values are to be treated as
274        # text/plain.
275
276        'no_subtype_in_content_type': (
277            'text/',
278            'text/plain',
279            'text',
280            'plain',
281            {},
282            [errors.InvalidHeaderDefect]),
283
284        'no_slash_in_content_type': (
285            'foo',
286            'text/plain',
287            'text',
288            'plain',
289            {},
290            [errors.InvalidHeaderDefect]),
291
292        'junk_text_in_content_type': (
293            '<crazy "stuff">',
294            'text/plain',
295            'text',
296            'plain',
297            {},
298            [errors.InvalidHeaderDefect]),
299
300        'too_many_slashes_in_content_type': (
301            'image/jpeg/foo',
302            'text/plain',
303            'text',
304            'plain',
305            {},
306            [errors.InvalidHeaderDefect]),
307
308        # But unknown names are OK.  We could make non-IANA names a defect, but
309        # by not doing so we make ourselves future proof.  The fact that they
310        # are unknown will be detectable by the fact that they don't appear in
311        # the mime_registry...and the application is free to extend that list
312        # to handle them even if the core library doesn't.
313
314        'unknown_content_type': (
315            'bad/names',
316            'bad/names',
317            'bad',
318            'names'),
319
320        # The content type is case insensitive, and CFWS is ignored.
321
322        'mixed_case_content_type': (
323            'ImAge/JPeg',
324            'image/jpeg',
325            'image',
326            'jpeg'),
327
328        'spaces_in_content_type': (
329            '  text  /  plain  ',
330            'text/plain',
331            'text',
332            'plain'),
333
334        'cfws_in_content_type': (
335            '(foo) text (bar)/(baz)plain(stuff)',
336            'text/plain',
337            'text',
338            'plain'),
339
340        # test some parameters (more tests could be added for parameters
341        # associated with other content types, but since parameter parsing is
342        # generic they would be redundant for the current implementation).
343
344        'charset_param': (
345            'text/plain; charset="utf-8"',
346            'text/plain',
347            'text',
348            'plain',
349            {'charset': 'utf-8'}),
350
351        'capitalized_charset': (
352            'text/plain; charset="US-ASCII"',
353            'text/plain',
354            'text',
355            'plain',
356            {'charset': 'US-ASCII'}),
357
358        'unknown_charset': (
359            'text/plain; charset="fOo"',
360            'text/plain',
361            'text',
362            'plain',
363            {'charset': 'fOo'}),
364
365        'capitalized_charset_param_name_and_comment': (
366            'text/plain; (interjection) Charset="utf-8"',
367            'text/plain',
368            'text',
369            'plain',
370            {'charset': 'utf-8'},
371            [],
372            # Should the parameter name be lowercased here?
373            'text/plain; Charset="utf-8"'),
374
375        # Since this is pretty much the ur-mimeheader, we'll put all the tests
376        # that exercise the parameter parsing and formatting here.
377        #
378        # XXX: question: is minimal quoting preferred?
379
380        'unquoted_param_value': (
381            'text/plain; title=foo',
382            'text/plain',
383            'text',
384            'plain',
385            {'title': 'foo'},
386            [],
387            'text/plain; title="foo"'),
388
389        'param_value_with_tspecials': (
390            'text/plain; title="(bar)foo blue"',
391            'text/plain',
392            'text',
393            'plain',
394            {'title': '(bar)foo blue'}),
395
396        'param_with_extra_quoted_whitespace': (
397            'text/plain; title="  a     loong  way \t home   "',
398            'text/plain',
399            'text',
400            'plain',
401            {'title': '  a     loong  way \t home   '}),
402
403        'bad_params': (
404            'blarg; baz; boo',
405            'text/plain',
406            'text',
407            'plain',
408            {'baz': '', 'boo': ''},
409            [errors.InvalidHeaderDefect]*3),
410
411        'spaces_around_param_equals': (
412            'Multipart/mixed; boundary = "CPIMSSMTPC06p5f3tG"',
413            'multipart/mixed',
414            'multipart',
415            'mixed',
416            {'boundary': 'CPIMSSMTPC06p5f3tG'},
417            [],
418            'Multipart/mixed; boundary="CPIMSSMTPC06p5f3tG"'),
419
420        'spaces_around_semis': (
421            ('image/jpeg; name="wibble.JPG" ; x-mac-type="4A504547" ; '
422                'x-mac-creator="474B4F4E"'),
423            'image/jpeg',
424            'image',
425            'jpeg',
426            {'name': 'wibble.JPG',
427             'x-mac-type': '4A504547',
428             'x-mac-creator': '474B4F4E'},
429            [],
430            ('image/jpeg; name="wibble.JPG"; x-mac-type="4A504547"; '
431                'x-mac-creator="474B4F4E"'),
432            # XXX: it could be that we will eventually prefer to fold starting
433            # from the decoded value, in which case these spaces and similar
434            # spaces in other tests will be wrong.
435            ('Content-Type: image/jpeg; name="wibble.JPG" ; '
436                'x-mac-type="4A504547" ;\n'
437             ' x-mac-creator="474B4F4E"\n'),
438            ),
439
440        'semis_inside_quotes': (
441            'image/jpeg; name="Jim&amp;&amp;Jill"',
442            'image/jpeg',
443            'image',
444            'jpeg',
445            {'name': 'Jim&amp;&amp;Jill'}),
446
447        'single_quotes_inside_quotes': (
448            'image/jpeg; name="Jim \'Bob\' Jill"',
449            'image/jpeg',
450            'image',
451            'jpeg',
452            {'name': "Jim 'Bob' Jill"}),
453
454        'double_quotes_inside_quotes': (
455            r'image/jpeg; name="Jim \"Bob\" Jill"',
456            'image/jpeg',
457            'image',
458            'jpeg',
459            {'name': 'Jim "Bob" Jill'},
460            [],
461            r'image/jpeg; name="Jim \"Bob\" Jill"'),
462
463        # XXX: This test works except for the refolding of the header.  I'll
464        # deal with that bug when I deal with the other folding bugs.
465        #'non_ascii_in_params': (
466        #    ('foo\xa7/bar; b\xa7r=two; '
467        #        'baz=thr\xa7e'.encode('latin-1').decode('us-ascii',
468        #                                                'surrogateescape')),
469        #    'foo\uFFFD/bar',
470        #    'foo\uFFFD',
471        #    'bar',
472        #    {'b\uFFFDr': 'two', 'baz': 'thr\uFFFDe'},
473        #    [errors.UndecodableBytesDefect]*3,
474        #    'foo�/bar; b�r="two"; baz="thr�e"',
475        #    ),
476
477        # RFC 2231 parameter tests.
478
479        'rfc2231_segmented_normal_values': (
480            'image/jpeg; name*0="abc"; name*1=".html"',
481            'image/jpeg',
482            'image',
483            'jpeg',
484            {'name': "abc.html"},
485            [],
486            'image/jpeg; name="abc.html"'),
487
488        'quotes_inside_rfc2231_value': (
489            r'image/jpeg; bar*0="baz\"foobar"; bar*1="\"baz"',
490            'image/jpeg',
491            'image',
492            'jpeg',
493            {'bar': 'baz"foobar"baz'},
494            [],
495            r'image/jpeg; bar="baz\"foobar\"baz"'),
496
497        # XXX: This test works except for the refolding of the header.  I'll
498        # deal with that bug when I deal with the other folding bugs.
499        #'non_ascii_rfc2231_value': (
500        #    ('text/plain; charset=us-ascii; '
501        #     "title*=us-ascii'en'This%20is%20"
502        #     'not%20f\xa7n').encode('latin-1').decode('us-ascii',
503        #                                             'surrogateescape'),
504        #    'text/plain',
505        #    'text',
506        #    'plain',
507        #    {'charset': 'us-ascii', 'title': 'This is not f\uFFFDn'},
508        #     [errors.UndecodableBytesDefect],
509        #     'text/plain; charset="us-ascii"; title="This is not f�n"'),
510
511        'rfc2231_encoded_charset': (
512            'text/plain; charset*=ansi-x3.4-1968\'\'us-ascii',
513            'text/plain',
514            'text',
515            'plain',
516            {'charset': 'us-ascii'},
517            [],
518            'text/plain; charset="us-ascii"'),
519
520        # This follows the RFC: no double quotes around encoded values.
521        'rfc2231_encoded_no_double_quotes': (
522            ("text/plain;"
523                "\tname*0*=''This%20is%20;"
524                "\tname*1*=%2A%2A%2Afun%2A%2A%2A%20;"
525                '\tname*2="is it not.pdf"'),
526            'text/plain',
527            'text',
528            'plain',
529            {'name': 'This is ***fun*** is it not.pdf'},
530            [],
531            'text/plain; name="This is ***fun*** is it not.pdf"',
532            ('Content-Type: text/plain;\tname*0*=\'\'This%20is%20;\n'
533             '\tname*1*=%2A%2A%2Afun%2A%2A%2A%20;\tname*2="is it not.pdf"\n'),
534            ),
535
536        # Make sure we also handle it if there are spurious double quotes.
537        'rfc2231_encoded_with_double_quotes': (
538            ("text/plain;"
539                '\tname*0*="us-ascii\'\'This%20is%20even%20more%20";'
540                '\tname*1*="%2A%2A%2Afun%2A%2A%2A%20";'
541                '\tname*2="is it not.pdf"'),
542            'text/plain',
543            'text',
544            'plain',
545            {'name': 'This is even more ***fun*** is it not.pdf'},
546            [errors.InvalidHeaderDefect]*2,
547            'text/plain; name="This is even more ***fun*** is it not.pdf"',
548            ('Content-Type: text/plain;\t'
549                'name*0*="us-ascii\'\'This%20is%20even%20more%20";\n'
550             '\tname*1*="%2A%2A%2Afun%2A%2A%2A%20";\tname*2="is it not.pdf"\n'),
551            ),
552
553        'rfc2231_single_quote_inside_double_quotes': (
554            ('text/plain; charset=us-ascii;'
555               '\ttitle*0*="us-ascii\'en\'This%20is%20really%20";'
556               '\ttitle*1*="%2A%2A%2Afun%2A%2A%2A%20";'
557               '\ttitle*2="isn\'t it!"'),
558            'text/plain',
559            'text',
560            'plain',
561            {'charset': 'us-ascii', 'title': "This is really ***fun*** isn't it!"},
562            [errors.InvalidHeaderDefect]*2,
563            ('text/plain; charset="us-ascii"; '
564               'title="This is really ***fun*** isn\'t it!"'),
565            ('Content-Type: text/plain; charset=us-ascii;\n'
566             '\ttitle*0*="us-ascii\'en\'This%20is%20really%20";\n'
567             '\ttitle*1*="%2A%2A%2Afun%2A%2A%2A%20";\ttitle*2="isn\'t it!"\n'),
568            ),
569
570        'rfc2231_single_quote_in_value_with_charset_and_lang': (
571            ('application/x-foo;'
572                "\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\""),
573            'application/x-foo',
574            'application',
575            'x-foo',
576            {'name': "Frank's Document"},
577            [errors.InvalidHeaderDefect]*2,
578            'application/x-foo; name="Frank\'s Document"',
579            ('Content-Type: application/x-foo;\t'
580                'name*0*="us-ascii\'en-us\'Frank\'s";\n'
581             ' name*1*=" Document"\n'),
582            ),
583
584        'rfc2231_single_quote_in_non_encoded_value': (
585            ('application/x-foo;'
586                "\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\""),
587            'application/x-foo',
588            'application',
589            'x-foo',
590            {'name': "us-ascii'en-us'Frank's Document"},
591            [],
592            'application/x-foo; name="us-ascii\'en-us\'Frank\'s Document"',
593            ('Content-Type: application/x-foo;\t'
594                'name*0="us-ascii\'en-us\'Frank\'s";\n'
595             ' name*1=" Document"\n'),
596             ),
597
598        'rfc2231_no_language_or_charset': (
599            'text/plain; NAME*0*=english_is_the_default.html',
600            'text/plain',
601            'text',
602            'plain',
603            {'name': 'english_is_the_default.html'},
604            [errors.InvalidHeaderDefect],
605            'text/plain; NAME="english_is_the_default.html"'),
606
607        'rfc2231_encoded_no_charset': (
608            ("text/plain;"
609                '\tname*0*="\'\'This%20is%20even%20more%20";'
610                '\tname*1*="%2A%2A%2Afun%2A%2A%2A%20";'
611                '\tname*2="is it.pdf"'),
612            'text/plain',
613            'text',
614            'plain',
615            {'name': 'This is even more ***fun*** is it.pdf'},
616            [errors.InvalidHeaderDefect]*2,
617            'text/plain; name="This is even more ***fun*** is it.pdf"',
618            ('Content-Type: text/plain;\t'
619                'name*0*="\'\'This%20is%20even%20more%20";\n'
620             '\tname*1*="%2A%2A%2Afun%2A%2A%2A%20";\tname*2="is it.pdf"\n'),
621            ),
622
623        # XXX: see below...the first name line here should be *0 not *0*.
624        'rfc2231_partly_encoded': (
625            ("text/plain;"
626                '\tname*0*="\'\'This%20is%20even%20more%20";'
627                '\tname*1*="%2A%2A%2Afun%2A%2A%2A%20";'
628                '\tname*2="is it.pdf"'),
629            'text/plain',
630            'text',
631            'plain',
632            {'name': 'This is even more ***fun*** is it.pdf'},
633            [errors.InvalidHeaderDefect]*2,
634            'text/plain; name="This is even more ***fun*** is it.pdf"',
635            ('Content-Type: text/plain;\t'
636                'name*0*="\'\'This%20is%20even%20more%20";\n'
637             '\tname*1*="%2A%2A%2Afun%2A%2A%2A%20";\tname*2="is it.pdf"\n'),
638            ),
639
640        'rfc2231_partly_encoded_2': (
641            ("text/plain;"
642                '\tname*0*="\'\'This%20is%20even%20more%20";'
643                '\tname*1="%2A%2A%2Afun%2A%2A%2A%20";'
644                '\tname*2="is it.pdf"'),
645            'text/plain',
646            'text',
647            'plain',
648            {'name': 'This is even more %2A%2A%2Afun%2A%2A%2A%20is it.pdf'},
649            [errors.InvalidHeaderDefect],
650            'text/plain; name="This is even more %2A%2A%2Afun%2A%2A%2A%20is it.pdf"',
651            ('Content-Type: text/plain;\t'
652                'name*0*="\'\'This%20is%20even%20more%20";\n'
653             '\tname*1="%2A%2A%2Afun%2A%2A%2A%20";\tname*2="is it.pdf"\n'),
654            ),
655
656        'rfc2231_unknown_charset_treated_as_ascii': (
657            "text/plain; name*0*=bogus'xx'ascii_is_the_default",
658            'text/plain',
659            'text',
660            'plain',
661            {'name': 'ascii_is_the_default'},
662            [],
663            'text/plain; name="ascii_is_the_default"'),
664
665        'rfc2231_bad_character_in_charset_parameter_value': (
666            "text/plain; charset*=ascii''utf-8%F1%F2%F3",
667            'text/plain',
668            'text',
669            'plain',
670            {'charset': 'utf-8\uFFFD\uFFFD\uFFFD'},
671            [errors.UndecodableBytesDefect],
672            'text/plain; charset="utf-8\uFFFD\uFFFD\uFFFD"'),
673
674        'rfc2231_utf_8_in_supposedly_ascii_charset_parameter_value': (
675            "text/plain; charset*=ascii''utf-8%E2%80%9D",
676            'text/plain',
677            'text',
678            'plain',
679            {'charset': 'utf-8”'},
680            [errors.UndecodableBytesDefect],
681            'text/plain; charset="utf-8”"',
682            ),
683            # XXX: if the above were *re*folded, it would get tagged as utf-8
684            # instead of ascii in the param, since it now contains non-ASCII.
685
686        'rfc2231_encoded_then_unencoded_segments': (
687            ('application/x-foo;'
688                '\tname*0*="us-ascii\'en-us\'My";'
689                '\tname*1=" Document";'
690                '\tname*2=" For You"'),
691            'application/x-foo',
692            'application',
693            'x-foo',
694            {'name': 'My Document For You'},
695            [errors.InvalidHeaderDefect],
696            'application/x-foo; name="My Document For You"',
697            ('Content-Type: application/x-foo;\t'
698                'name*0*="us-ascii\'en-us\'My";\n'
699             '\tname*1=" Document";\tname*2=" For You"\n'),
700            ),
701
702        # My reading of the RFC is that this is an invalid header.  The RFC
703        # says that if charset and language information is given, the first
704        # segment *must* be encoded.
705        'rfc2231_unencoded_then_encoded_segments': (
706            ('application/x-foo;'
707                '\tname*0=us-ascii\'en-us\'My;'
708                '\tname*1*=" Document";'
709                '\tname*2*=" For You"'),
710            'application/x-foo',
711            'application',
712            'x-foo',
713            {'name': 'My Document For You'},
714            [errors.InvalidHeaderDefect]*3,
715            'application/x-foo; name="My Document For You"',
716            ("Content-Type: application/x-foo;\tname*0=us-ascii'en-us'My;\t"
717                # XXX: the newline is in the wrong place, come back and fix
718                # this when the rest of tests pass.
719                'name*1*=" Document"\n;'
720             '\tname*2*=" For You"\n'),
721            ),
722
723        # XXX: I would say this one should default to ascii/en for the
724        # "encoded" segment, since the first segment is not encoded and is
725        # in double quotes, making the value a valid non-encoded string.  The
726        # old parser decodes this just like the previous case, which may be the
727        # better Postel rule, but could equally result in borking headers that
728        # intentionally have quoted quotes in them.  We could get this 98%
729        # right if we treat it as a quoted string *unless* it matches the
730        # charset'lang'value pattern exactly *and* there is at least one
731        # encoded segment.  Implementing that algorithm will require some
732        # refactoring, so I haven't done it (yet).
733
734        'rfc2231_qouted_unencoded_then_encoded_segments': (
735            ('application/x-foo;'
736                '\tname*0="us-ascii\'en-us\'My";'
737                '\tname*1*=" Document";'
738                '\tname*2*=" For You"'),
739            'application/x-foo',
740            'application',
741            'x-foo',
742            {'name': "us-ascii'en-us'My Document For You"},
743            [errors.InvalidHeaderDefect]*2,
744            'application/x-foo; name="us-ascii\'en-us\'My Document For You"',
745            ('Content-Type: application/x-foo;\t'
746                'name*0="us-ascii\'en-us\'My";\n'
747             '\tname*1*=" Document";\tname*2*=" For You"\n'),
748            ),
749
750    }
751
752
753@parameterize
754class TestContentTransferEncoding(TestHeaderBase):
755
756    def cte_as_value(self,
757                     source,
758                     cte,
759                     *args):
760        l = len(args)
761        defects =  args[0] if l>0 else []
762        decoded =  args[1] if l>1 and args[1] is not DITTO else source
763        header = 'Content-Transfer-Encoding:' + ' ' if source else ''
764        folded = args[2] if l>2 else header + source + '\n'
765        h = self.make_header('Content-Transfer-Encoding', source)
766        self.assertEqual(h.cte, cte)
767        self.assertDefectsEqual(h.defects, defects)
768        self.assertEqual(h, decoded)
769        self.assertEqual(h.fold(policy=policy.default), folded)
770
771    cte_params = {
772
773        'RFC_2183_1': (
774            'base64',
775            'base64',),
776
777        'no_value': (
778            '',
779            '7bit',
780            [errors.HeaderMissingRequiredValue],
781            '',
782            'Content-Transfer-Encoding:\n',
783            ),
784
785        'junk_after_cte': (
786            '7bit and a bunch more',
787            '7bit',
788            [errors.InvalidHeaderDefect]),
789
790    }
791
792
793@parameterize
794class TestContentDisposition(TestHeaderBase):
795
796    def content_disp_as_value(self,
797                              source,
798                              content_disposition,
799                              *args):
800        l = len(args)
801        parmdict = args[0] if l>0 else {}
802        defects =  args[1] if l>1 else []
803        decoded =  args[2] if l>2 and args[2] is not DITTO else source
804        header = 'Content-Disposition:' + ' ' if source else ''
805        folded = args[3] if l>3 else header + source + '\n'
806        h = self.make_header('Content-Disposition', source)
807        self.assertEqual(h.content_disposition, content_disposition)
808        self.assertEqual(h.params, parmdict)
809        self.assertDefectsEqual(h.defects, defects)
810        self.assertEqual(h, decoded)
811        self.assertEqual(h.fold(policy=policy.default), folded)
812
813    content_disp_params = {
814
815        # Examples from RFC 2183.
816
817        'RFC_2183_1': (
818            'inline',
819            'inline',),
820
821        'RFC_2183_2': (
822            ('attachment; filename=genome.jpeg;'
823             '  modification-date="Wed, 12 Feb 1997 16:29:51 -0500";'),
824            'attachment',
825            {'filename': 'genome.jpeg',
826             'modification-date': 'Wed, 12 Feb 1997 16:29:51 -0500'},
827            [],
828            ('attachment; filename="genome.jpeg"; '
829                 'modification-date="Wed, 12 Feb 1997 16:29:51 -0500"'),
830            ('Content-Disposition: attachment; filename=genome.jpeg;\n'
831             '  modification-date="Wed, 12 Feb 1997 16:29:51 -0500";\n'),
832            ),
833
834        'no_value': (
835            '',
836            None,
837            {},
838            [errors.HeaderMissingRequiredValue],
839            '',
840            'Content-Disposition:\n'),
841
842        'invalid_value': (
843            'ab./k',
844            'ab.',
845            {},
846            [errors.InvalidHeaderDefect]),
847
848        'invalid_value_with_params': (
849            'ab./k; filename="foo"',
850            'ab.',
851            {'filename': 'foo'},
852            [errors.InvalidHeaderDefect]),
853
854    }
855
856
857@parameterize
858class TestMIMEVersionHeader(TestHeaderBase):
859
860    def version_string_as_MIME_Version(self,
861                                       source,
862                                       decoded,
863                                       version,
864                                       major,
865                                       minor,
866                                       defects):
867        h = self.make_header('MIME-Version', source)
868        self.assertEqual(h, decoded)
869        self.assertEqual(h.version, version)
870        self.assertEqual(h.major, major)
871        self.assertEqual(h.minor, minor)
872        self.assertDefectsEqual(h.defects, defects)
873        if source:
874            source = ' ' + source
875        self.assertEqual(h.fold(policy=policy.default),
876                        'MIME-Version:' + source + '\n')
877
878    version_string_params = {
879
880        # Examples from the RFC.
881
882        'RFC_2045_1': (
883            '1.0',
884            '1.0',
885            '1.0',
886            1,
887            0,
888            []),
889
890        'RFC_2045_2': (
891            '1.0 (produced by MetaSend Vx.x)',
892            '1.0 (produced by MetaSend Vx.x)',
893            '1.0',
894            1,
895            0,
896            []),
897
898        'RFC_2045_3': (
899            '(produced by MetaSend Vx.x) 1.0',
900            '(produced by MetaSend Vx.x) 1.0',
901            '1.0',
902            1,
903            0,
904            []),
905
906        'RFC_2045_4': (
907            '1.(produced by MetaSend Vx.x)0',
908            '1.(produced by MetaSend Vx.x)0',
909            '1.0',
910            1,
911            0,
912            []),
913
914        # Other valid values.
915
916        '1_1': (
917            '1.1',
918            '1.1',
919            '1.1',
920            1,
921            1,
922            []),
923
924        '2_1': (
925            '2.1',
926            '2.1',
927            '2.1',
928            2,
929            1,
930            []),
931
932        'whitespace': (
933            '1 .0',
934            '1 .0',
935            '1.0',
936            1,
937            0,
938            []),
939
940        'leading_trailing_whitespace_ignored': (
941            '  1.0  ',
942            '  1.0  ',
943            '1.0',
944            1,
945            0,
946            []),
947
948        # Recoverable invalid values.  We can recover here only because we
949        # already have a valid value by the time we encounter the garbage.
950        # Anywhere else, and we don't know where the garbage ends.
951
952        'non_comment_garbage_after': (
953            '1.0 <abc>',
954            '1.0 <abc>',
955            '1.0',
956            1,
957            0,
958            [errors.InvalidHeaderDefect]),
959
960        # Unrecoverable invalid values.  We *could* apply more heuristics to
961        # get something out of the first two, but doing so is not worth the
962        # effort.
963
964        'non_comment_garbage_before': (
965            '<abc> 1.0',
966            '<abc> 1.0',
967            None,
968            None,
969            None,
970            [errors.InvalidHeaderDefect]),
971
972        'non_comment_garbage_inside': (
973            '1.<abc>0',
974            '1.<abc>0',
975            None,
976            None,
977            None,
978            [errors.InvalidHeaderDefect]),
979
980        'two_periods': (
981            '1..0',
982            '1..0',
983            None,
984            None,
985            None,
986            [errors.InvalidHeaderDefect]),
987
988        '2_x': (
989            '2.x',
990            '2.x',
991            None,  # This could be 2, but it seems safer to make it None.
992            None,
993            None,
994            [errors.InvalidHeaderDefect]),
995
996        'foo': (
997            'foo',
998            'foo',
999            None,
1000            None,
1001            None,
1002            [errors.InvalidHeaderDefect]),
1003
1004        'missing': (
1005            '',
1006            '',
1007            None,
1008            None,
1009            None,
1010            [errors.HeaderMissingRequiredValue]),
1011
1012        }
1013
1014
1015@parameterize
1016class TestAddressHeader(TestHeaderBase):
1017
1018    example_params = {
1019
1020        'empty':
1021            ('<>',
1022             [errors.InvalidHeaderDefect],
1023             '<>',
1024             '',
1025             '<>',
1026             '',
1027             '',
1028             None),
1029
1030        'address_only':
1031            ('zippy@pinhead.com',
1032             [],
1033             'zippy@pinhead.com',
1034             '',
1035             'zippy@pinhead.com',
1036             'zippy',
1037             'pinhead.com',
1038             None),
1039
1040        'name_and_address':
1041            ('Zaphrod Beblebrux <zippy@pinhead.com>',
1042             [],
1043             'Zaphrod Beblebrux <zippy@pinhead.com>',
1044             'Zaphrod Beblebrux',
1045             'zippy@pinhead.com',
1046             'zippy',
1047             'pinhead.com',
1048             None),
1049
1050        'quoted_local_part':
1051            ('Zaphrod Beblebrux <"foo bar"@pinhead.com>',
1052             [],
1053             'Zaphrod Beblebrux <"foo bar"@pinhead.com>',
1054             'Zaphrod Beblebrux',
1055             '"foo bar"@pinhead.com',
1056             'foo bar',
1057             'pinhead.com',
1058             None),
1059
1060        'quoted_parens_in_name':
1061            (r'"A \(Special\) Person" <person@dom.ain>',
1062             [],
1063             '"A (Special) Person" <person@dom.ain>',
1064             'A (Special) Person',
1065             'person@dom.ain',
1066             'person',
1067             'dom.ain',
1068             None),
1069
1070        'quoted_backslashes_in_name':
1071            (r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>',
1072             [],
1073             r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>',
1074             r'Arthur \Backslash\ Foobar',
1075             'person@dom.ain',
1076             'person',
1077             'dom.ain',
1078             None),
1079
1080        'name_with_dot':
1081            ('John X. Doe <jxd@example.com>',
1082             [errors.ObsoleteHeaderDefect],
1083             '"John X. Doe" <jxd@example.com>',
1084             'John X. Doe',
1085             'jxd@example.com',
1086             'jxd',
1087             'example.com',
1088             None),
1089
1090        'quoted_strings_in_local_part':
1091            ('""example" example"@example.com',
1092             [errors.InvalidHeaderDefect]*3,
1093             '"example example"@example.com',
1094             '',
1095             '"example example"@example.com',
1096             'example example',
1097             'example.com',
1098             None),
1099
1100        'escaped_quoted_strings_in_local_part':
1101            (r'"\"example\" example"@example.com',
1102             [],
1103             r'"\"example\" example"@example.com',
1104             '',
1105             r'"\"example\" example"@example.com',
1106             r'"example" example',
1107             'example.com',
1108            None),
1109
1110        'escaped_escapes_in_local_part':
1111            (r'"\\"example\\" example"@example.com',
1112             [errors.InvalidHeaderDefect]*5,
1113             r'"\\example\\\\ example"@example.com',
1114             '',
1115             r'"\\example\\\\ example"@example.com',
1116             r'\example\\ example',
1117             'example.com',
1118            None),
1119
1120        'spaces_in_unquoted_local_part_collapsed':
1121            ('merwok  wok  @example.com',
1122             [errors.InvalidHeaderDefect]*2,
1123             '"merwok wok"@example.com',
1124             '',
1125             '"merwok wok"@example.com',
1126             'merwok wok',
1127             'example.com',
1128             None),
1129
1130        'spaces_around_dots_in_local_part_removed':
1131            ('merwok. wok .  wok@example.com',
1132             [errors.ObsoleteHeaderDefect],
1133             'merwok.wok.wok@example.com',
1134             '',
1135             'merwok.wok.wok@example.com',
1136             'merwok.wok.wok',
1137             'example.com',
1138             None),
1139
1140        'rfc2047_atom_is_decoded':
1141            ('=?utf-8?q?=C3=89ric?= <foo@example.com>',
1142            [],
1143            'Éric <foo@example.com>',
1144            'Éric',
1145            'foo@example.com',
1146            'foo',
1147            'example.com',
1148            None),
1149
1150        'rfc2047_atom_in_phrase_is_decoded':
1151            ('The =?utf-8?q?=C3=89ric=2C?= Himself <foo@example.com>',
1152            [],
1153            '"The Éric, Himself" <foo@example.com>',
1154            'The Éric, Himself',
1155            'foo@example.com',
1156            'foo',
1157            'example.com',
1158            None),
1159
1160        'rfc2047_atom_in_quoted_string_is_decoded':
1161            ('"=?utf-8?q?=C3=89ric?=" <foo@example.com>',
1162            [errors.InvalidHeaderDefect],
1163            'Éric <foo@example.com>',
1164            'Éric',
1165            'foo@example.com',
1166            'foo',
1167            'example.com',
1168            None),
1169
1170        }
1171
1172        # XXX: Need many more examples, and in particular some with names in
1173        # trailing comments, which aren't currently handled.  comments in
1174        # general are not handled yet.
1175
1176    def example_as_address(self, source, defects, decoded, display_name,
1177                           addr_spec, username, domain, comment):
1178        h = self.make_header('sender', source)
1179        self.assertEqual(h, decoded)
1180        self.assertDefectsEqual(h.defects, defects)
1181        a = h.address
1182        self.assertEqual(str(a), decoded)
1183        self.assertEqual(len(h.groups), 1)
1184        self.assertEqual([a], list(h.groups[0].addresses))
1185        self.assertEqual([a], list(h.addresses))
1186        self.assertEqual(a.display_name, display_name)
1187        self.assertEqual(a.addr_spec, addr_spec)
1188        self.assertEqual(a.username, username)
1189        self.assertEqual(a.domain, domain)
1190        # XXX: we have no comment support yet.
1191        #self.assertEqual(a.comment, comment)
1192
1193    def example_as_group(self, source, defects, decoded, display_name,
1194                         addr_spec, username, domain, comment):
1195        source = 'foo: {};'.format(source)
1196        gdecoded = 'foo: {};'.format(decoded) if decoded else 'foo:;'
1197        h = self.make_header('to', source)
1198        self.assertEqual(h, gdecoded)
1199        self.assertDefectsEqual(h.defects, defects)
1200        self.assertEqual(h.groups[0].addresses, h.addresses)
1201        self.assertEqual(len(h.groups), 1)
1202        self.assertEqual(len(h.addresses), 1)
1203        a = h.addresses[0]
1204        self.assertEqual(str(a), decoded)
1205        self.assertEqual(a.display_name, display_name)
1206        self.assertEqual(a.addr_spec, addr_spec)
1207        self.assertEqual(a.username, username)
1208        self.assertEqual(a.domain, domain)
1209
1210    def test_simple_address_list(self):
1211        value = ('Fred <dinsdale@python.org>, foo@example.com, '
1212                    '"Harry W. Hastings" <hasty@example.com>')
1213        h = self.make_header('to', value)
1214        self.assertEqual(h, value)
1215        self.assertEqual(len(h.groups), 3)
1216        self.assertEqual(len(h.addresses), 3)
1217        for i in range(3):
1218            self.assertEqual(h.groups[i].addresses[0], h.addresses[i])
1219        self.assertEqual(str(h.addresses[0]), 'Fred <dinsdale@python.org>')
1220        self.assertEqual(str(h.addresses[1]), 'foo@example.com')
1221        self.assertEqual(str(h.addresses[2]),
1222            '"Harry W. Hastings" <hasty@example.com>')
1223        self.assertEqual(h.addresses[2].display_name,
1224            'Harry W. Hastings')
1225
1226    def test_complex_address_list(self):
1227        examples = list(self.example_params.values())
1228        source = ('dummy list:;, another: (empty);,' +
1229                 ', '.join([x[0] for x in examples[:4]]) + ', ' +
1230                 r'"A \"list\"": ' +
1231                    ', '.join([x[0] for x in examples[4:6]]) + ';,' +
1232                 ', '.join([x[0] for x in examples[6:]])
1233            )
1234        # XXX: the fact that (empty) disappears here is a potential API design
1235        # bug.  We don't currently have a way to preserve comments.
1236        expected = ('dummy list:;, another:;, ' +
1237                 ', '.join([x[2] for x in examples[:4]]) + ', ' +
1238                 r'"A \"list\"": ' +
1239                    ', '.join([x[2] for x in examples[4:6]]) + ';, ' +
1240                 ', '.join([x[2] for x in examples[6:]])
1241            )
1242
1243        h = self.make_header('to', source)
1244        self.assertEqual(h.split(','), expected.split(','))
1245        self.assertEqual(h, expected)
1246        self.assertEqual(len(h.groups), 7 + len(examples) - 6)
1247        self.assertEqual(h.groups[0].display_name, 'dummy list')
1248        self.assertEqual(h.groups[1].display_name, 'another')
1249        self.assertEqual(h.groups[6].display_name, 'A "list"')
1250        self.assertEqual(len(h.addresses), len(examples))
1251        for i in range(4):
1252            self.assertIsNone(h.groups[i+2].display_name)
1253            self.assertEqual(str(h.groups[i+2].addresses[0]), examples[i][2])
1254        for i in range(7, 7 + len(examples) - 6):
1255            self.assertIsNone(h.groups[i].display_name)
1256            self.assertEqual(str(h.groups[i].addresses[0]), examples[i-1][2])
1257        for i in range(len(examples)):
1258            self.assertEqual(str(h.addresses[i]), examples[i][2])
1259            self.assertEqual(h.addresses[i].addr_spec, examples[i][4])
1260
1261    def test_address_read_only(self):
1262        h = self.make_header('sender', 'abc@xyz.com')
1263        with self.assertRaises(AttributeError):
1264            h.address = 'foo'
1265
1266    def test_addresses_read_only(self):
1267        h = self.make_header('sender', 'abc@xyz.com')
1268        with self.assertRaises(AttributeError):
1269            h.addresses = 'foo'
1270
1271    def test_groups_read_only(self):
1272        h = self.make_header('sender', 'abc@xyz.com')
1273        with self.assertRaises(AttributeError):
1274            h.groups = 'foo'
1275
1276    def test_addresses_types(self):
1277        source = 'me <who@example.com>'
1278        h = self.make_header('to', source)
1279        self.assertIsInstance(h.addresses, tuple)
1280        self.assertIsInstance(h.addresses[0], Address)
1281
1282    def test_groups_types(self):
1283        source = 'me <who@example.com>'
1284        h = self.make_header('to', source)
1285        self.assertIsInstance(h.groups, tuple)
1286        self.assertIsInstance(h.groups[0], Group)
1287
1288    def test_set_from_Address(self):
1289        h = self.make_header('to', Address('me', 'foo', 'example.com'))
1290        self.assertEqual(h, 'me <foo@example.com>')
1291
1292    def test_set_from_Address_list(self):
1293        h = self.make_header('to', [Address('me', 'foo', 'example.com'),
1294                                    Address('you', 'bar', 'example.com')])
1295        self.assertEqual(h, 'me <foo@example.com>, you <bar@example.com>')
1296
1297    def test_set_from_Address_and_Group_list(self):
1298        h = self.make_header('to', [Address('me', 'foo', 'example.com'),
1299                                    Group('bing', [Address('fiz', 'z', 'b.com'),
1300                                                   Address('zif', 'f', 'c.com')]),
1301                                    Address('you', 'bar', 'example.com')])
1302        self.assertEqual(h, 'me <foo@example.com>, bing: fiz <z@b.com>, '
1303                            'zif <f@c.com>;, you <bar@example.com>')
1304        self.assertEqual(h.fold(policy=policy.default.clone(max_line_length=40)),
1305                        'to: me <foo@example.com>,\n'
1306                        ' bing: fiz <z@b.com>, zif <f@c.com>;,\n'
1307                        ' you <bar@example.com>\n')
1308
1309    def test_set_from_Group_list(self):
1310        h = self.make_header('to', [Group('bing', [Address('fiz', 'z', 'b.com'),
1311                                                   Address('zif', 'f', 'c.com')])])
1312        self.assertEqual(h, 'bing: fiz <z@b.com>, zif <f@c.com>;')
1313
1314
1315class TestAddressAndGroup(TestEmailBase):
1316
1317    def _test_attr_ro(self, obj, attr):
1318        with self.assertRaises(AttributeError):
1319            setattr(obj, attr, 'foo')
1320
1321    def test_address_display_name_ro(self):
1322        self._test_attr_ro(Address('foo', 'bar', 'baz'), 'display_name')
1323
1324    def test_address_username_ro(self):
1325        self._test_attr_ro(Address('foo', 'bar', 'baz'), 'username')
1326
1327    def test_address_domain_ro(self):
1328        self._test_attr_ro(Address('foo', 'bar', 'baz'), 'domain')
1329
1330    def test_group_display_name_ro(self):
1331        self._test_attr_ro(Group('foo'), 'display_name')
1332
1333    def test_group_addresses_ro(self):
1334        self._test_attr_ro(Group('foo'), 'addresses')
1335
1336    def test_address_from_username_domain(self):
1337        a = Address('foo', 'bar', 'baz')
1338        self.assertEqual(a.display_name, 'foo')
1339        self.assertEqual(a.username, 'bar')
1340        self.assertEqual(a.domain, 'baz')
1341        self.assertEqual(a.addr_spec, 'bar@baz')
1342        self.assertEqual(str(a), 'foo <bar@baz>')
1343
1344    def test_address_from_addr_spec(self):
1345        a = Address('foo', addr_spec='bar@baz')
1346        self.assertEqual(a.display_name, 'foo')
1347        self.assertEqual(a.username, 'bar')
1348        self.assertEqual(a.domain, 'baz')
1349        self.assertEqual(a.addr_spec, 'bar@baz')
1350        self.assertEqual(str(a), 'foo <bar@baz>')
1351
1352    def test_address_with_no_display_name(self):
1353        a = Address(addr_spec='bar@baz')
1354        self.assertEqual(a.display_name, '')
1355        self.assertEqual(a.username, 'bar')
1356        self.assertEqual(a.domain, 'baz')
1357        self.assertEqual(a.addr_spec, 'bar@baz')
1358        self.assertEqual(str(a), 'bar@baz')
1359
1360    def test_null_address(self):
1361        a = Address()
1362        self.assertEqual(a.display_name, '')
1363        self.assertEqual(a.username, '')
1364        self.assertEqual(a.domain, '')
1365        self.assertEqual(a.addr_spec, '<>')
1366        self.assertEqual(str(a), '<>')
1367
1368    def test_domain_only(self):
1369        # This isn't really a valid address.
1370        a = Address(domain='buzz')
1371        self.assertEqual(a.display_name, '')
1372        self.assertEqual(a.username, '')
1373        self.assertEqual(a.domain, 'buzz')
1374        self.assertEqual(a.addr_spec, '@buzz')
1375        self.assertEqual(str(a), '@buzz')
1376
1377    def test_username_only(self):
1378        # This isn't really a valid address.
1379        a = Address(username='buzz')
1380        self.assertEqual(a.display_name, '')
1381        self.assertEqual(a.username, 'buzz')
1382        self.assertEqual(a.domain, '')
1383        self.assertEqual(a.addr_spec, 'buzz')
1384        self.assertEqual(str(a), 'buzz')
1385
1386    def test_display_name_only(self):
1387        a = Address('buzz')
1388        self.assertEqual(a.display_name, 'buzz')
1389        self.assertEqual(a.username, '')
1390        self.assertEqual(a.domain, '')
1391        self.assertEqual(a.addr_spec, '<>')
1392        self.assertEqual(str(a), 'buzz <>')
1393
1394    def test_quoting(self):
1395        # Ideally we'd check every special individually, but I'm not up for
1396        # writing that many tests.
1397        a = Address('Sara J.', 'bad name', 'example.com')
1398        self.assertEqual(a.display_name, 'Sara J.')
1399        self.assertEqual(a.username, 'bad name')
1400        self.assertEqual(a.domain, 'example.com')
1401        self.assertEqual(a.addr_spec, '"bad name"@example.com')
1402        self.assertEqual(str(a), '"Sara J." <"bad name"@example.com>')
1403
1404    def test_il8n(self):
1405        a = Address('Éric', 'wok', 'exàmple.com')
1406        self.assertEqual(a.display_name, 'Éric')
1407        self.assertEqual(a.username, 'wok')
1408        self.assertEqual(a.domain, 'exàmple.com')
1409        self.assertEqual(a.addr_spec, 'wok@exàmple.com')
1410        self.assertEqual(str(a), 'Éric <wok@exàmple.com>')
1411
1412    # XXX: there is an API design issue that needs to be solved here.
1413    #def test_non_ascii_username_raises(self):
1414    #    with self.assertRaises(ValueError):
1415    #        Address('foo', 'wők', 'example.com')
1416
1417    def test_non_ascii_username_in_addr_spec_raises(self):
1418        with self.assertRaises(ValueError):
1419            Address('foo', addr_spec='wők@example.com')
1420
1421    def test_address_addr_spec_and_username_raises(self):
1422        with self.assertRaises(TypeError):
1423            Address('foo', username='bing', addr_spec='bar@baz')
1424
1425    def test_address_addr_spec_and_domain_raises(self):
1426        with self.assertRaises(TypeError):
1427            Address('foo', domain='bing', addr_spec='bar@baz')
1428
1429    def test_address_addr_spec_and_username_and_domain_raises(self):
1430        with self.assertRaises(TypeError):
1431            Address('foo', username='bong', domain='bing', addr_spec='bar@baz')
1432
1433    def test_space_in_addr_spec_username_raises(self):
1434        with self.assertRaises(ValueError):
1435            Address('foo', addr_spec="bad name@example.com")
1436
1437    def test_bad_addr_sepc_raises(self):
1438        with self.assertRaises(ValueError):
1439            Address('foo', addr_spec="name@ex[]ample.com")
1440
1441    def test_empty_group(self):
1442        g = Group('foo')
1443        self.assertEqual(g.display_name, 'foo')
1444        self.assertEqual(g.addresses, tuple())
1445        self.assertEqual(str(g), 'foo:;')
1446
1447    def test_empty_group_list(self):
1448        g = Group('foo', addresses=[])
1449        self.assertEqual(g.display_name, 'foo')
1450        self.assertEqual(g.addresses, tuple())
1451        self.assertEqual(str(g), 'foo:;')
1452
1453    def test_null_group(self):
1454        g = Group()
1455        self.assertIsNone(g.display_name)
1456        self.assertEqual(g.addresses, tuple())
1457        self.assertEqual(str(g), 'None:;')
1458
1459    def test_group_with_addresses(self):
1460        addrs = [Address('b', 'b', 'c'), Address('a', 'b','c')]
1461        g = Group('foo', addrs)
1462        self.assertEqual(g.display_name, 'foo')
1463        self.assertEqual(g.addresses, tuple(addrs))
1464        self.assertEqual(str(g), 'foo: b <b@c>, a <b@c>;')
1465
1466    def test_group_with_addresses_no_display_name(self):
1467        addrs = [Address('b', 'b', 'c'), Address('a', 'b','c')]
1468        g = Group(addresses=addrs)
1469        self.assertIsNone(g.display_name)
1470        self.assertEqual(g.addresses, tuple(addrs))
1471        self.assertEqual(str(g), 'None: b <b@c>, a <b@c>;')
1472
1473    def test_group_with_one_address_no_display_name(self):
1474        addrs = [Address('b', 'b', 'c')]
1475        g = Group(addresses=addrs)
1476        self.assertIsNone(g.display_name)
1477        self.assertEqual(g.addresses, tuple(addrs))
1478        self.assertEqual(str(g), 'b <b@c>')
1479
1480    def test_display_name_quoting(self):
1481        g = Group('foo.bar')
1482        self.assertEqual(g.display_name, 'foo.bar')
1483        self.assertEqual(g.addresses, tuple())
1484        self.assertEqual(str(g), '"foo.bar":;')
1485
1486    def test_display_name_blanks_not_quoted(self):
1487        g = Group('foo bar')
1488        self.assertEqual(g.display_name, 'foo bar')
1489        self.assertEqual(g.addresses, tuple())
1490        self.assertEqual(str(g), 'foo bar:;')
1491
1492    def test_set_message_header_from_address(self):
1493        a = Address('foo', 'bar', 'example.com')
1494        m = Message(policy=policy.default)
1495        m['To'] = a
1496        self.assertEqual(m['to'], 'foo <bar@example.com>')
1497        self.assertEqual(m['to'].addresses, (a,))
1498
1499    def test_set_message_header_from_group(self):
1500        g = Group('foo bar')
1501        m = Message(policy=policy.default)
1502        m['To'] = g
1503        self.assertEqual(m['to'], 'foo bar:;')
1504        self.assertEqual(m['to'].addresses, g.addresses)
1505
1506
1507class TestFolding(TestHeaderBase):
1508
1509    def test_short_unstructured(self):
1510        h = self.make_header('subject', 'this is a test')
1511        self.assertEqual(h.fold(policy=policy.default),
1512                         'subject: this is a test\n')
1513
1514    def test_long_unstructured(self):
1515        h = self.make_header('Subject', 'This is a long header '
1516            'line that will need to be folded into two lines '
1517            'and will demonstrate basic folding')
1518        self.assertEqual(h.fold(policy=policy.default),
1519                        'Subject: This is a long header line that will '
1520                            'need to be folded into two lines\n'
1521                        ' and will demonstrate basic folding\n')
1522
1523    def test_unstructured_short_max_line_length(self):
1524        h = self.make_header('Subject', 'this is a short header '
1525            'that will be folded anyway')
1526        self.assertEqual(
1527            h.fold(policy=policy.default.clone(max_line_length=20)),
1528            textwrap.dedent("""\
1529                Subject: this is a
1530                 short header that
1531                 will be folded
1532                 anyway
1533                """))
1534
1535    def test_fold_unstructured_single_word(self):
1536        h = self.make_header('Subject', 'test')
1537        self.assertEqual(h.fold(policy=policy.default), 'Subject: test\n')
1538
1539    def test_fold_unstructured_short(self):
1540        h = self.make_header('Subject', 'test test test')
1541        self.assertEqual(h.fold(policy=policy.default),
1542                        'Subject: test test test\n')
1543
1544    def test_fold_unstructured_with_overlong_word(self):
1545        h = self.make_header('Subject', 'thisisaverylonglineconsistingofa'
1546            'singlewordthatwontfit')
1547        self.assertEqual(
1548            h.fold(policy=policy.default.clone(max_line_length=20)),
1549            'Subject: thisisaverylonglineconsistingofasinglewordthatwontfit\n')
1550
1551    def test_fold_unstructured_with_two_overlong_words(self):
1552        h = self.make_header('Subject', 'thisisaverylonglineconsistingofa'
1553            'singlewordthatwontfit plusanotherverylongwordthatwontfit')
1554        self.assertEqual(
1555            h.fold(policy=policy.default.clone(max_line_length=20)),
1556            'Subject: thisisaverylonglineconsistingofasinglewordthatwontfit\n'
1557                ' plusanotherverylongwordthatwontfit\n')
1558
1559    def test_fold_unstructured_with_slightly_long_word(self):
1560        h = self.make_header('Subject', 'thislongwordislessthanmaxlinelen')
1561        self.assertEqual(
1562            h.fold(policy=policy.default.clone(max_line_length=35)),
1563            'Subject:\n thislongwordislessthanmaxlinelen\n')
1564
1565    def test_fold_unstructured_with_commas(self):
1566        # The old wrapper would fold this at the commas.
1567        h = self.make_header('Subject', "This header is intended to "
1568            "demonstrate, in a fairly succinct way, that we now do "
1569            "not give a , special treatment in unstructured headers.")
1570        self.assertEqual(
1571            h.fold(policy=policy.default.clone(max_line_length=60)),
1572            textwrap.dedent("""\
1573                Subject: This header is intended to demonstrate, in a fairly
1574                 succinct way, that we now do not give a , special treatment
1575                 in unstructured headers.
1576                 """))
1577
1578    def test_fold_address_list(self):
1579        h = self.make_header('To', '"Theodore H. Perfect" <yes@man.com>, '
1580            '"My address is very long because my name is long" <foo@bar.com>, '
1581            '"Only A. Friend" <no@yes.com>')
1582        self.assertEqual(h.fold(policy=policy.default), textwrap.dedent("""\
1583            To: "Theodore H. Perfect" <yes@man.com>,
1584             "My address is very long because my name is long" <foo@bar.com>,
1585             "Only A. Friend" <no@yes.com>
1586             """))
1587
1588    def test_fold_date_header(self):
1589        h = self.make_header('Date', 'Sat, 2 Feb 2002 17:00:06 -0800')
1590        self.assertEqual(h.fold(policy=policy.default),
1591                        'Date: Sat, 02 Feb 2002 17:00:06 -0800\n')
1592
1593
1594
1595if __name__ == '__main__':
1596    unittest.main()
1597