• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 + decoded + '\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'   # make sure 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.  Note that
377        # when we refold we may canonicalize, so things like whitespace,
378        # quoting, and rfc2231 encoding may change from what was in the input
379        # header.
380
381        'unquoted_param_value': (
382            'text/plain; title=foo',
383            'text/plain',
384            'text',
385            'plain',
386            {'title': 'foo'},
387            [],
388            'text/plain; title="foo"',
389            ),
390
391        'param_value_with_tspecials': (
392            'text/plain; title="(bar)foo blue"',
393            'text/plain',
394            'text',
395            'plain',
396            {'title': '(bar)foo blue'}),
397
398        'param_with_extra_quoted_whitespace': (
399            'text/plain; title="  a     loong  way \t home   "',
400            'text/plain',
401            'text',
402            'plain',
403            {'title': '  a     loong  way \t home   '}),
404
405        'bad_params': (
406            'blarg; baz; boo',
407            'text/plain',
408            'text',
409            'plain',
410            {'baz': '', 'boo': ''},
411            [errors.InvalidHeaderDefect]*3),
412
413        'spaces_around_param_equals': (
414            'Multipart/mixed; boundary = "CPIMSSMTPC06p5f3tG"',
415            'multipart/mixed',
416            'multipart',
417            'mixed',
418            {'boundary': 'CPIMSSMTPC06p5f3tG'},
419            [],
420            'Multipart/mixed; boundary="CPIMSSMTPC06p5f3tG"',
421            ),
422
423        'spaces_around_semis': (
424            ('image/jpeg; name="wibble.JPG" ; x-mac-type="4A504547" ; '
425                'x-mac-creator="474B4F4E"'),
426            'image/jpeg',
427            'image',
428            'jpeg',
429            {'name': 'wibble.JPG',
430             'x-mac-type': '4A504547',
431             'x-mac-creator': '474B4F4E'},
432            [],
433            ('image/jpeg; name="wibble.JPG"; x-mac-type="4A504547"; '
434                'x-mac-creator="474B4F4E"'),
435            ('Content-Type: image/jpeg; name="wibble.JPG";'
436                ' x-mac-type="4A504547";\n'
437             ' x-mac-creator="474B4F4E"\n'),
438            ),
439
440        'lots_of_mime_params': (
441            ('image/jpeg; name="wibble.JPG"; x-mac-type="4A504547"; '
442                'x-mac-creator="474B4F4E"; x-extrastuff="make it longer"'),
443            'image/jpeg',
444            'image',
445            'jpeg',
446            {'name': 'wibble.JPG',
447             'x-mac-type': '4A504547',
448             'x-mac-creator': '474B4F4E',
449             'x-extrastuff': 'make it longer'},
450            [],
451            ('image/jpeg; name="wibble.JPG"; x-mac-type="4A504547"; '
452                'x-mac-creator="474B4F4E"; x-extrastuff="make it longer"'),
453            # In this case the whole of the MimeParameters does *not* fit
454            # one one line, so we break at a lower syntactic level.
455            ('Content-Type: image/jpeg; name="wibble.JPG";'
456                ' x-mac-type="4A504547";\n'
457             ' x-mac-creator="474B4F4E"; x-extrastuff="make it longer"\n'),
458            ),
459
460        'semis_inside_quotes': (
461            'image/jpeg; name="Jim&amp;&amp;Jill"',
462            'image/jpeg',
463            'image',
464            'jpeg',
465            {'name': 'Jim&amp;&amp;Jill'}),
466
467        'single_quotes_inside_quotes': (
468            'image/jpeg; name="Jim \'Bob\' Jill"',
469            'image/jpeg',
470            'image',
471            'jpeg',
472            {'name': "Jim 'Bob' Jill"}),
473
474        'double_quotes_inside_quotes': (
475            r'image/jpeg; name="Jim \"Bob\" Jill"',
476            'image/jpeg',
477            'image',
478            'jpeg',
479            {'name': 'Jim "Bob" Jill'},
480            [],
481            r'image/jpeg; name="Jim \"Bob\" Jill"'),
482
483        'non_ascii_in_params': (
484            ('foo\xa7/bar; b\xa7r=two; '
485                'baz=thr\xa7e'.encode('latin-1').decode('us-ascii',
486                                                        'surrogateescape')),
487            'foo\uFFFD/bar',
488            'foo\uFFFD',
489            'bar',
490            {'b\uFFFDr': 'two', 'baz': 'thr\uFFFDe'},
491            [errors.UndecodableBytesDefect]*3,
492            'foo�/bar; b�r="two"; baz="thr�e"',
493            # XXX Two bugs here: the mime type is not allowed to be an encoded
494            # word, and we shouldn't be emitting surrogates in the parameter
495            # names.  But I don't know what the behavior should be here, so I'm
496            # punting for now.  In practice this is unlikely to be encountered
497            # since headers with binary in them only come from a binary source
498            # and are almost certain to be re-emitted without refolding.
499            'Content-Type: =?unknown-8bit?q?foo=A7?=/bar; b\udca7r="two";\n'
500            " baz*=unknown-8bit''thr%A7e\n",
501            ),
502
503        # RFC 2231 parameter tests.
504
505        'rfc2231_segmented_normal_values': (
506            'image/jpeg; name*0="abc"; name*1=".html"',
507            'image/jpeg',
508            'image',
509            'jpeg',
510            {'name': "abc.html"},
511            [],
512            'image/jpeg; name="abc.html"'),
513
514        'quotes_inside_rfc2231_value': (
515            r'image/jpeg; bar*0="baz\"foobar"; bar*1="\"baz"',
516            'image/jpeg',
517            'image',
518            'jpeg',
519            {'bar': 'baz"foobar"baz'},
520            [],
521            r'image/jpeg; bar="baz\"foobar\"baz"'),
522
523        'non_ascii_rfc2231_value': (
524            ('text/plain; charset=us-ascii; '
525             "title*=us-ascii'en'This%20is%20"
526             'not%20f\xa7n').encode('latin-1').decode('us-ascii',
527                                                     'surrogateescape'),
528            'text/plain',
529            'text',
530            'plain',
531            {'charset': 'us-ascii', 'title': 'This is not f\uFFFDn'},
532             [errors.UndecodableBytesDefect],
533             'text/plain; charset="us-ascii"; title="This is not f�n"',
534            'Content-Type: text/plain; charset="us-ascii";\n'
535            " title*=unknown-8bit''This%20is%20not%20f%A7n\n",
536            ),
537
538        'rfc2231_encoded_charset': (
539            'text/plain; charset*=ansi-x3.4-1968\'\'us-ascii',
540            'text/plain',
541            'text',
542            'plain',
543            {'charset': 'us-ascii'},
544            [],
545            'text/plain; charset="us-ascii"'),
546
547        # This follows the RFC: no double quotes around encoded values.
548        'rfc2231_encoded_no_double_quotes': (
549            ("text/plain;"
550                "\tname*0*=''This%20is%20;"
551                "\tname*1*=%2A%2A%2Afun%2A%2A%2A%20;"
552                '\tname*2="is it not.pdf"'),
553            'text/plain',
554            'text',
555            'plain',
556            {'name': 'This is ***fun*** is it not.pdf'},
557            [],
558            'text/plain; name="This is ***fun*** is it not.pdf"',
559            ),
560
561        # Make sure we also handle it if there are spurious double quotes.
562        'rfc2231_encoded_with_double_quotes': (
563            ("text/plain;"
564                '\tname*0*="us-ascii\'\'This%20is%20even%20more%20";'
565                '\tname*1*="%2A%2A%2Afun%2A%2A%2A%20";'
566                '\tname*2="is it not.pdf"'),
567            'text/plain',
568            'text',
569            'plain',
570            {'name': 'This is even more ***fun*** is it not.pdf'},
571            [errors.InvalidHeaderDefect]*2,
572            'text/plain; name="This is even more ***fun*** is it not.pdf"',
573            ),
574
575        'rfc2231_single_quote_inside_double_quotes': (
576            ('text/plain; charset=us-ascii;'
577               '\ttitle*0*="us-ascii\'en\'This%20is%20really%20";'
578               '\ttitle*1*="%2A%2A%2Afun%2A%2A%2A%20";'
579               '\ttitle*2="isn\'t it!"'),
580            'text/plain',
581            'text',
582            'plain',
583            {'charset': 'us-ascii', 'title': "This is really ***fun*** isn't it!"},
584            [errors.InvalidHeaderDefect]*2,
585            ('text/plain; charset="us-ascii"; '
586               'title="This is really ***fun*** isn\'t it!"'),
587            ('Content-Type: text/plain; charset="us-ascii";\n'
588                ' title="This is really ***fun*** isn\'t it!"\n'),
589            ),
590
591        'rfc2231_single_quote_in_value_with_charset_and_lang': (
592            ('application/x-foo;'
593                "\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\""),
594            'application/x-foo',
595            'application',
596            'x-foo',
597            {'name': "Frank's Document"},
598            [errors.InvalidHeaderDefect]*2,
599            'application/x-foo; name="Frank\'s Document"',
600            ),
601
602        'rfc2231_single_quote_in_non_encoded_value': (
603            ('application/x-foo;'
604                "\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\""),
605            'application/x-foo',
606            'application',
607            'x-foo',
608            {'name': "us-ascii'en-us'Frank's Document"},
609            [],
610            'application/x-foo; name="us-ascii\'en-us\'Frank\'s Document"',
611             ),
612
613        'rfc2231_no_language_or_charset': (
614            'text/plain; NAME*0*=english_is_the_default.html',
615            'text/plain',
616            'text',
617            'plain',
618            {'name': 'english_is_the_default.html'},
619            [errors.InvalidHeaderDefect],
620            'text/plain; NAME="english_is_the_default.html"'),
621
622        'rfc2231_encoded_no_charset': (
623            ("text/plain;"
624                '\tname*0*="\'\'This%20is%20even%20more%20";'
625                '\tname*1*="%2A%2A%2Afun%2A%2A%2A%20";'
626                '\tname*2="is it.pdf"'),
627            'text/plain',
628            'text',
629            'plain',
630            {'name': 'This is even more ***fun*** is it.pdf'},
631            [errors.InvalidHeaderDefect]*2,
632            'text/plain; name="This is even more ***fun*** is it.pdf"',
633            ),
634
635        'rfc2231_partly_encoded': (
636            ("text/plain;"
637                '\tname*0*="\'\'This%20is%20even%20more%20";'
638                '\tname*1*="%2A%2A%2Afun%2A%2A%2A%20";'
639                '\tname*2="is it.pdf"'),
640            'text/plain',
641            'text',
642            'plain',
643            {'name': 'This is even more ***fun*** is it.pdf'},
644            [errors.InvalidHeaderDefect]*2,
645            'text/plain; name="This is even more ***fun*** is it.pdf"',
646            ),
647
648        'rfc2231_partly_encoded_2': (
649            ("text/plain;"
650                '\tname*0*="\'\'This%20is%20even%20more%20";'
651                '\tname*1="%2A%2A%2Afun%2A%2A%2A%20";'
652                '\tname*2="is it.pdf"'),
653            'text/plain',
654            'text',
655            'plain',
656            {'name': 'This is even more %2A%2A%2Afun%2A%2A%2A%20is it.pdf'},
657            [errors.InvalidHeaderDefect],
658            ('text/plain;'
659             ' name="This is even more %2A%2A%2Afun%2A%2A%2A%20is it.pdf"'),
660            ('Content-Type: text/plain;\n'
661             ' name="This is even more %2A%2A%2Afun%2A%2A%2A%20is'
662                ' it.pdf"\n'),
663            ),
664
665        'rfc2231_unknown_charset_treated_as_ascii': (
666            "text/plain; name*0*=bogus'xx'ascii_is_the_default",
667            'text/plain',
668            'text',
669            'plain',
670            {'name': 'ascii_is_the_default'},
671            [],
672            'text/plain; name="ascii_is_the_default"'),
673
674        'rfc2231_bad_character_in_charset_parameter_value': (
675            "text/plain; charset*=ascii''utf-8%F1%F2%F3",
676            'text/plain',
677            'text',
678            'plain',
679            {'charset': 'utf-8\uFFFD\uFFFD\uFFFD'},
680            [errors.UndecodableBytesDefect],
681            'text/plain; charset="utf-8\uFFFD\uFFFD\uFFFD"',
682            "Content-Type: text/plain;"
683            " charset*=unknown-8bit''utf-8%F1%F2%F3\n",
684            ),
685
686        'rfc2231_utf8_in_supposedly_ascii_charset_parameter_value': (
687            "text/plain; charset*=ascii''utf-8%E2%80%9D",
688            'text/plain',
689            'text',
690            'plain',
691            {'charset': 'utf-8”'},
692            [errors.UndecodableBytesDefect],
693            'text/plain; charset="utf-8”"',
694            # XXX Should folding change the charset to utf8?  Currently it just
695            # reproduces the original, which is arguably fine.
696            "Content-Type: text/plain;"
697            " charset*=unknown-8bit''utf-8%E2%80%9D\n",
698            ),
699
700        'rfc2231_encoded_then_unencoded_segments': (
701            ('application/x-foo;'
702                '\tname*0*="us-ascii\'en-us\'My";'
703                '\tname*1=" Document";'
704                '\tname*2=" For You"'),
705            'application/x-foo',
706            'application',
707            'x-foo',
708            {'name': 'My Document For You'},
709            [errors.InvalidHeaderDefect],
710            'application/x-foo; name="My Document For You"',
711            ),
712
713        # My reading of the RFC is that this is an invalid header.  The RFC
714        # says that if charset and language information is given, the first
715        # segment *must* be encoded.
716        'rfc2231_unencoded_then_encoded_segments': (
717            ('application/x-foo;'
718                '\tname*0=us-ascii\'en-us\'My;'
719                '\tname*1*=" Document";'
720                '\tname*2*=" For You"'),
721            'application/x-foo',
722            'application',
723            'x-foo',
724            {'name': 'My Document For You'},
725            [errors.InvalidHeaderDefect]*3,
726            'application/x-foo; name="My Document For You"',
727            ),
728
729        # XXX: I would say this one should default to ascii/en for the
730        # "encoded" segment, since the first segment is not encoded and is
731        # in double quotes, making the value a valid non-encoded string.  The
732        # old parser decodes this just like the previous case, which may be the
733        # better Postel rule, but could equally result in borking headers that
734        # intentionally have quoted quotes in them.  We could get this 98%
735        # right if we treat it as a quoted string *unless* it matches the
736        # charset'lang'value pattern exactly *and* there is at least one
737        # encoded segment.  Implementing that algorithm will require some
738        # refactoring, so I haven't done it (yet).
739        'rfc2231_quoted_unencoded_then_encoded_segments': (
740            ('application/x-foo;'
741                '\tname*0="us-ascii\'en-us\'My";'
742                '\tname*1*=" Document";'
743                '\tname*2*=" For You"'),
744            'application/x-foo',
745            'application',
746            'x-foo',
747            {'name': "us-ascii'en-us'My Document For You"},
748            [errors.InvalidHeaderDefect]*2,
749            'application/x-foo; name="us-ascii\'en-us\'My Document For You"',
750            ),
751
752        # Make sure our folding algorithm produces multiple sections correctly.
753        # We could mix encoded and non-encoded segments, but we don't, we just
754        # make them all encoded.  It might be worth fixing that, since the
755        # sections can get used for wrapping ascii text.
756        'rfc2231_folded_segments_correctly_formatted': (
757            ('application/x-foo;'
758                '\tname="' + "with spaces"*8 + '"'),
759            'application/x-foo',
760            'application',
761            'x-foo',
762            {'name': "with spaces"*8},
763            [],
764            'application/x-foo; name="' + "with spaces"*8 + '"',
765            "Content-Type: application/x-foo;\n"
766            " name*0*=us-ascii''with%20spaceswith%20spaceswith%20spaceswith"
767                "%20spaceswith;\n"
768            " name*1*=%20spaceswith%20spaceswith%20spaceswith%20spaces\n"
769            ),
770
771    }
772
773
774@parameterize
775class TestContentTransferEncoding(TestHeaderBase):
776
777    def cte_as_value(self,
778                     source,
779                     cte,
780                     *args):
781        l = len(args)
782        defects =  args[0] if l>0 else []
783        decoded =  args[1] if l>1 and args[1] is not DITTO else source
784        header = 'Content-Transfer-Encoding:' + ' ' if source else ''
785        folded = args[2] if l>2 else header + source + '\n'
786        h = self.make_header('Content-Transfer-Encoding', source)
787        self.assertEqual(h.cte, cte)
788        self.assertDefectsEqual(h.defects, defects)
789        self.assertEqual(h, decoded)
790        self.assertEqual(h.fold(policy=policy.default), folded)
791
792    cte_params = {
793
794        'RFC_2183_1': (
795            'base64',
796            'base64',),
797
798        'no_value': (
799            '',
800            '7bit',
801            [errors.HeaderMissingRequiredValue],
802            '',
803            'Content-Transfer-Encoding:\n',
804            ),
805
806        'junk_after_cte': (
807            '7bit and a bunch more',
808            '7bit',
809            [errors.InvalidHeaderDefect]),
810
811    }
812
813
814@parameterize
815class TestContentDisposition(TestHeaderBase):
816
817    def content_disp_as_value(self,
818                              source,
819                              content_disposition,
820                              *args):
821        l = len(args)
822        parmdict = args[0] if l>0 else {}
823        defects =  args[1] if l>1 else []
824        decoded =  args[2] if l>2 and args[2] is not DITTO else source
825        header = 'Content-Disposition:' + ' ' if source else ''
826        folded = args[3] if l>3 else header + source + '\n'
827        h = self.make_header('Content-Disposition', source)
828        self.assertEqual(h.content_disposition, content_disposition)
829        self.assertEqual(h.params, parmdict)
830        self.assertDefectsEqual(h.defects, defects)
831        self.assertEqual(h, decoded)
832        self.assertEqual(h.fold(policy=policy.default), folded)
833
834    content_disp_params = {
835
836        # Examples from RFC 2183.
837
838        'RFC_2183_1': (
839            'inline',
840            'inline',),
841
842        'RFC_2183_2': (
843            ('attachment; filename=genome.jpeg;'
844             '  modification-date="Wed, 12 Feb 1997 16:29:51 -0500";'),
845            'attachment',
846            {'filename': 'genome.jpeg',
847             'modification-date': 'Wed, 12 Feb 1997 16:29:51 -0500'},
848            [],
849            ('attachment; filename="genome.jpeg"; '
850                 'modification-date="Wed, 12 Feb 1997 16:29:51 -0500"'),
851            ('Content-Disposition: attachment; filename="genome.jpeg";\n'
852             ' modification-date="Wed, 12 Feb 1997 16:29:51 -0500"\n'),
853            ),
854
855        'no_value': (
856            '',
857            None,
858            {},
859            [errors.HeaderMissingRequiredValue],
860            '',
861            'Content-Disposition:\n'),
862
863        'invalid_value': (
864            'ab./k',
865            'ab.',
866            {},
867            [errors.InvalidHeaderDefect]),
868
869        'invalid_value_with_params': (
870            'ab./k; filename="foo"',
871            'ab.',
872            {'filename': 'foo'},
873            [errors.InvalidHeaderDefect]),
874
875    }
876
877
878@parameterize
879class TestMIMEVersionHeader(TestHeaderBase):
880
881    def version_string_as_MIME_Version(self,
882                                       source,
883                                       decoded,
884                                       version,
885                                       major,
886                                       minor,
887                                       defects):
888        h = self.make_header('MIME-Version', source)
889        self.assertEqual(h, decoded)
890        self.assertEqual(h.version, version)
891        self.assertEqual(h.major, major)
892        self.assertEqual(h.minor, minor)
893        self.assertDefectsEqual(h.defects, defects)
894        if source:
895            source = ' ' + source
896        self.assertEqual(h.fold(policy=policy.default),
897                         'MIME-Version:' + source + '\n')
898
899    version_string_params = {
900
901        # Examples from the RFC.
902
903        'RFC_2045_1': (
904            '1.0',
905            '1.0',
906            '1.0',
907            1,
908            0,
909            []),
910
911        'RFC_2045_2': (
912            '1.0 (produced by MetaSend Vx.x)',
913            '1.0 (produced by MetaSend Vx.x)',
914            '1.0',
915            1,
916            0,
917            []),
918
919        'RFC_2045_3': (
920            '(produced by MetaSend Vx.x) 1.0',
921            '(produced by MetaSend Vx.x) 1.0',
922            '1.0',
923            1,
924            0,
925            []),
926
927        'RFC_2045_4': (
928            '1.(produced by MetaSend Vx.x)0',
929            '1.(produced by MetaSend Vx.x)0',
930            '1.0',
931            1,
932            0,
933            []),
934
935        # Other valid values.
936
937        '1_1': (
938            '1.1',
939            '1.1',
940            '1.1',
941            1,
942            1,
943            []),
944
945        '2_1': (
946            '2.1',
947            '2.1',
948            '2.1',
949            2,
950            1,
951            []),
952
953        'whitespace': (
954            '1 .0',
955            '1 .0',
956            '1.0',
957            1,
958            0,
959            []),
960
961        'leading_trailing_whitespace_ignored': (
962            '  1.0  ',
963            '  1.0  ',
964            '1.0',
965            1,
966            0,
967            []),
968
969        # Recoverable invalid values.  We can recover here only because we
970        # already have a valid value by the time we encounter the garbage.
971        # Anywhere else, and we don't know where the garbage ends.
972
973        'non_comment_garbage_after': (
974            '1.0 <abc>',
975            '1.0 <abc>',
976            '1.0',
977            1,
978            0,
979            [errors.InvalidHeaderDefect]),
980
981        # Unrecoverable invalid values.  We *could* apply more heuristics to
982        # get something out of the first two, but doing so is not worth the
983        # effort.
984
985        'non_comment_garbage_before': (
986            '<abc> 1.0',
987            '<abc> 1.0',
988            None,
989            None,
990            None,
991            [errors.InvalidHeaderDefect]),
992
993        'non_comment_garbage_inside': (
994            '1.<abc>0',
995            '1.<abc>0',
996            None,
997            None,
998            None,
999            [errors.InvalidHeaderDefect]),
1000
1001        'two_periods': (
1002            '1..0',
1003            '1..0',
1004            None,
1005            None,
1006            None,
1007            [errors.InvalidHeaderDefect]),
1008
1009        '2_x': (
1010            '2.x',
1011            '2.x',
1012            None,  # This could be 2, but it seems safer to make it None.
1013            None,
1014            None,
1015            [errors.InvalidHeaderDefect]),
1016
1017        'foo': (
1018            'foo',
1019            'foo',
1020            None,
1021            None,
1022            None,
1023            [errors.InvalidHeaderDefect]),
1024
1025        'missing': (
1026            '',
1027            '',
1028            None,
1029            None,
1030            None,
1031            [errors.HeaderMissingRequiredValue]),
1032
1033        }
1034
1035
1036@parameterize
1037class TestAddressHeader(TestHeaderBase):
1038
1039    example_params = {
1040
1041        'empty':
1042            ('<>',
1043             [errors.InvalidHeaderDefect],
1044             '<>',
1045             '',
1046             '<>',
1047             '',
1048             '',
1049             None),
1050
1051        'address_only':
1052            ('zippy@pinhead.com',
1053             [],
1054             'zippy@pinhead.com',
1055             '',
1056             'zippy@pinhead.com',
1057             'zippy',
1058             'pinhead.com',
1059             None),
1060
1061        'name_and_address':
1062            ('Zaphrod Beblebrux <zippy@pinhead.com>',
1063             [],
1064             'Zaphrod Beblebrux <zippy@pinhead.com>',
1065             'Zaphrod Beblebrux',
1066             'zippy@pinhead.com',
1067             'zippy',
1068             'pinhead.com',
1069             None),
1070
1071        'quoted_local_part':
1072            ('Zaphrod Beblebrux <"foo bar"@pinhead.com>',
1073             [],
1074             'Zaphrod Beblebrux <"foo bar"@pinhead.com>',
1075             'Zaphrod Beblebrux',
1076             '"foo bar"@pinhead.com',
1077             'foo bar',
1078             'pinhead.com',
1079             None),
1080
1081        'quoted_parens_in_name':
1082            (r'"A \(Special\) Person" <person@dom.ain>',
1083             [],
1084             '"A (Special) Person" <person@dom.ain>',
1085             'A (Special) Person',
1086             'person@dom.ain',
1087             'person',
1088             'dom.ain',
1089             None),
1090
1091        'quoted_backslashes_in_name':
1092            (r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>',
1093             [],
1094             r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>',
1095             r'Arthur \Backslash\ Foobar',
1096             'person@dom.ain',
1097             'person',
1098             'dom.ain',
1099             None),
1100
1101        'name_with_dot':
1102            ('John X. Doe <jxd@example.com>',
1103             [errors.ObsoleteHeaderDefect],
1104             '"John X. Doe" <jxd@example.com>',
1105             'John X. Doe',
1106             'jxd@example.com',
1107             'jxd',
1108             'example.com',
1109             None),
1110
1111        'quoted_strings_in_local_part':
1112            ('""example" example"@example.com',
1113             [errors.InvalidHeaderDefect]*3,
1114             '"example example"@example.com',
1115             '',
1116             '"example example"@example.com',
1117             'example example',
1118             'example.com',
1119             None),
1120
1121        'escaped_quoted_strings_in_local_part':
1122            (r'"\"example\" example"@example.com',
1123             [],
1124             r'"\"example\" example"@example.com',
1125             '',
1126             r'"\"example\" example"@example.com',
1127             r'"example" example',
1128             'example.com',
1129            None),
1130
1131        'escaped_escapes_in_local_part':
1132            (r'"\\"example\\" example"@example.com',
1133             [errors.InvalidHeaderDefect]*5,
1134             r'"\\example\\\\ example"@example.com',
1135             '',
1136             r'"\\example\\\\ example"@example.com',
1137             r'\example\\ example',
1138             'example.com',
1139            None),
1140
1141        'spaces_in_unquoted_local_part_collapsed':
1142            ('merwok  wok  @example.com',
1143             [errors.InvalidHeaderDefect]*2,
1144             '"merwok wok"@example.com',
1145             '',
1146             '"merwok wok"@example.com',
1147             'merwok wok',
1148             'example.com',
1149             None),
1150
1151        'spaces_around_dots_in_local_part_removed':
1152            ('merwok. wok .  wok@example.com',
1153             [errors.ObsoleteHeaderDefect],
1154             'merwok.wok.wok@example.com',
1155             '',
1156             'merwok.wok.wok@example.com',
1157             'merwok.wok.wok',
1158             'example.com',
1159             None),
1160
1161        'rfc2047_atom_is_decoded':
1162            ('=?utf-8?q?=C3=89ric?= <foo@example.com>',
1163            [],
1164            'Éric <foo@example.com>',
1165            'Éric',
1166            'foo@example.com',
1167            'foo',
1168            'example.com',
1169            None),
1170
1171        'rfc2047_atom_in_phrase_is_decoded':
1172            ('The =?utf-8?q?=C3=89ric=2C?= Himself <foo@example.com>',
1173            [],
1174            '"The Éric, Himself" <foo@example.com>',
1175            'The Éric, Himself',
1176            'foo@example.com',
1177            'foo',
1178            'example.com',
1179            None),
1180
1181        'rfc2047_atom_in_quoted_string_is_decoded':
1182            ('"=?utf-8?q?=C3=89ric?=" <foo@example.com>',
1183            [errors.InvalidHeaderDefect],
1184            'Éric <foo@example.com>',
1185            'Éric',
1186            'foo@example.com',
1187            'foo',
1188            'example.com',
1189            None),
1190
1191        }
1192
1193        # XXX: Need many more examples, and in particular some with names in
1194        # trailing comments, which aren't currently handled.  comments in
1195        # general are not handled yet.
1196
1197    def example_as_address(self, source, defects, decoded, display_name,
1198                           addr_spec, username, domain, comment):
1199        h = self.make_header('sender', source)
1200        self.assertEqual(h, decoded)
1201        self.assertDefectsEqual(h.defects, defects)
1202        a = h.address
1203        self.assertEqual(str(a), decoded)
1204        self.assertEqual(len(h.groups), 1)
1205        self.assertEqual([a], list(h.groups[0].addresses))
1206        self.assertEqual([a], list(h.addresses))
1207        self.assertEqual(a.display_name, display_name)
1208        self.assertEqual(a.addr_spec, addr_spec)
1209        self.assertEqual(a.username, username)
1210        self.assertEqual(a.domain, domain)
1211        # XXX: we have no comment support yet.
1212        #self.assertEqual(a.comment, comment)
1213
1214    def example_as_group(self, source, defects, decoded, display_name,
1215                         addr_spec, username, domain, comment):
1216        source = 'foo: {};'.format(source)
1217        gdecoded = 'foo: {};'.format(decoded) if decoded else 'foo:;'
1218        h = self.make_header('to', source)
1219        self.assertEqual(h, gdecoded)
1220        self.assertDefectsEqual(h.defects, defects)
1221        self.assertEqual(h.groups[0].addresses, h.addresses)
1222        self.assertEqual(len(h.groups), 1)
1223        self.assertEqual(len(h.addresses), 1)
1224        a = h.addresses[0]
1225        self.assertEqual(str(a), decoded)
1226        self.assertEqual(a.display_name, display_name)
1227        self.assertEqual(a.addr_spec, addr_spec)
1228        self.assertEqual(a.username, username)
1229        self.assertEqual(a.domain, domain)
1230
1231    def test_simple_address_list(self):
1232        value = ('Fred <dinsdale@python.org>, foo@example.com, '
1233                    '"Harry W. Hastings" <hasty@example.com>')
1234        h = self.make_header('to', value)
1235        self.assertEqual(h, value)
1236        self.assertEqual(len(h.groups), 3)
1237        self.assertEqual(len(h.addresses), 3)
1238        for i in range(3):
1239            self.assertEqual(h.groups[i].addresses[0], h.addresses[i])
1240        self.assertEqual(str(h.addresses[0]), 'Fred <dinsdale@python.org>')
1241        self.assertEqual(str(h.addresses[1]), 'foo@example.com')
1242        self.assertEqual(str(h.addresses[2]),
1243            '"Harry W. Hastings" <hasty@example.com>')
1244        self.assertEqual(h.addresses[2].display_name,
1245            'Harry W. Hastings')
1246
1247    def test_complex_address_list(self):
1248        examples = list(self.example_params.values())
1249        source = ('dummy list:;, another: (empty);,' +
1250                 ', '.join([x[0] for x in examples[:4]]) + ', ' +
1251                 r'"A \"list\"": ' +
1252                    ', '.join([x[0] for x in examples[4:6]]) + ';,' +
1253                 ', '.join([x[0] for x in examples[6:]])
1254            )
1255        # XXX: the fact that (empty) disappears here is a potential API design
1256        # bug.  We don't currently have a way to preserve comments.
1257        expected = ('dummy list:;, another:;, ' +
1258                 ', '.join([x[2] for x in examples[:4]]) + ', ' +
1259                 r'"A \"list\"": ' +
1260                    ', '.join([x[2] for x in examples[4:6]]) + ';, ' +
1261                 ', '.join([x[2] for x in examples[6:]])
1262            )
1263
1264        h = self.make_header('to', source)
1265        self.assertEqual(h.split(','), expected.split(','))
1266        self.assertEqual(h, expected)
1267        self.assertEqual(len(h.groups), 7 + len(examples) - 6)
1268        self.assertEqual(h.groups[0].display_name, 'dummy list')
1269        self.assertEqual(h.groups[1].display_name, 'another')
1270        self.assertEqual(h.groups[6].display_name, 'A "list"')
1271        self.assertEqual(len(h.addresses), len(examples))
1272        for i in range(4):
1273            self.assertIsNone(h.groups[i+2].display_name)
1274            self.assertEqual(str(h.groups[i+2].addresses[0]), examples[i][2])
1275        for i in range(7, 7 + len(examples) - 6):
1276            self.assertIsNone(h.groups[i].display_name)
1277            self.assertEqual(str(h.groups[i].addresses[0]), examples[i-1][2])
1278        for i in range(len(examples)):
1279            self.assertEqual(str(h.addresses[i]), examples[i][2])
1280            self.assertEqual(h.addresses[i].addr_spec, examples[i][4])
1281
1282    def test_address_read_only(self):
1283        h = self.make_header('sender', 'abc@xyz.com')
1284        with self.assertRaises(AttributeError):
1285            h.address = 'foo'
1286
1287    def test_addresses_read_only(self):
1288        h = self.make_header('sender', 'abc@xyz.com')
1289        with self.assertRaises(AttributeError):
1290            h.addresses = 'foo'
1291
1292    def test_groups_read_only(self):
1293        h = self.make_header('sender', 'abc@xyz.com')
1294        with self.assertRaises(AttributeError):
1295            h.groups = 'foo'
1296
1297    def test_addresses_types(self):
1298        source = 'me <who@example.com>'
1299        h = self.make_header('to', source)
1300        self.assertIsInstance(h.addresses, tuple)
1301        self.assertIsInstance(h.addresses[0], Address)
1302
1303    def test_groups_types(self):
1304        source = 'me <who@example.com>'
1305        h = self.make_header('to', source)
1306        self.assertIsInstance(h.groups, tuple)
1307        self.assertIsInstance(h.groups[0], Group)
1308
1309    def test_set_from_Address(self):
1310        h = self.make_header('to', Address('me', 'foo', 'example.com'))
1311        self.assertEqual(h, 'me <foo@example.com>')
1312
1313    def test_set_from_Address_list(self):
1314        h = self.make_header('to', [Address('me', 'foo', 'example.com'),
1315                                    Address('you', 'bar', 'example.com')])
1316        self.assertEqual(h, 'me <foo@example.com>, you <bar@example.com>')
1317
1318    def test_set_from_Address_and_Group_list(self):
1319        h = self.make_header('to', [Address('me', 'foo', 'example.com'),
1320                                    Group('bing', [Address('fiz', 'z', 'b.com'),
1321                                                   Address('zif', 'f', 'c.com')]),
1322                                    Address('you', 'bar', 'example.com')])
1323        self.assertEqual(h, 'me <foo@example.com>, bing: fiz <z@b.com>, '
1324                            'zif <f@c.com>;, you <bar@example.com>')
1325        self.assertEqual(h.fold(policy=policy.default.clone(max_line_length=40)),
1326                        'to: me <foo@example.com>,\n'
1327                        ' bing: fiz <z@b.com>, zif <f@c.com>;,\n'
1328                        ' you <bar@example.com>\n')
1329
1330    def test_set_from_Group_list(self):
1331        h = self.make_header('to', [Group('bing', [Address('fiz', 'z', 'b.com'),
1332                                                   Address('zif', 'f', 'c.com')])])
1333        self.assertEqual(h, 'bing: fiz <z@b.com>, zif <f@c.com>;')
1334
1335
1336class TestAddressAndGroup(TestEmailBase):
1337
1338    def _test_attr_ro(self, obj, attr):
1339        with self.assertRaises(AttributeError):
1340            setattr(obj, attr, 'foo')
1341
1342    def test_address_display_name_ro(self):
1343        self._test_attr_ro(Address('foo', 'bar', 'baz'), 'display_name')
1344
1345    def test_address_username_ro(self):
1346        self._test_attr_ro(Address('foo', 'bar', 'baz'), 'username')
1347
1348    def test_address_domain_ro(self):
1349        self._test_attr_ro(Address('foo', 'bar', 'baz'), 'domain')
1350
1351    def test_group_display_name_ro(self):
1352        self._test_attr_ro(Group('foo'), 'display_name')
1353
1354    def test_group_addresses_ro(self):
1355        self._test_attr_ro(Group('foo'), 'addresses')
1356
1357    def test_address_from_username_domain(self):
1358        a = Address('foo', 'bar', 'baz')
1359        self.assertEqual(a.display_name, 'foo')
1360        self.assertEqual(a.username, 'bar')
1361        self.assertEqual(a.domain, 'baz')
1362        self.assertEqual(a.addr_spec, 'bar@baz')
1363        self.assertEqual(str(a), 'foo <bar@baz>')
1364
1365    def test_address_from_addr_spec(self):
1366        a = Address('foo', addr_spec='bar@baz')
1367        self.assertEqual(a.display_name, 'foo')
1368        self.assertEqual(a.username, 'bar')
1369        self.assertEqual(a.domain, 'baz')
1370        self.assertEqual(a.addr_spec, 'bar@baz')
1371        self.assertEqual(str(a), 'foo <bar@baz>')
1372
1373    def test_address_with_no_display_name(self):
1374        a = Address(addr_spec='bar@baz')
1375        self.assertEqual(a.display_name, '')
1376        self.assertEqual(a.username, 'bar')
1377        self.assertEqual(a.domain, 'baz')
1378        self.assertEqual(a.addr_spec, 'bar@baz')
1379        self.assertEqual(str(a), 'bar@baz')
1380
1381    def test_null_address(self):
1382        a = Address()
1383        self.assertEqual(a.display_name, '')
1384        self.assertEqual(a.username, '')
1385        self.assertEqual(a.domain, '')
1386        self.assertEqual(a.addr_spec, '<>')
1387        self.assertEqual(str(a), '<>')
1388
1389    def test_domain_only(self):
1390        # This isn't really a valid address.
1391        a = Address(domain='buzz')
1392        self.assertEqual(a.display_name, '')
1393        self.assertEqual(a.username, '')
1394        self.assertEqual(a.domain, 'buzz')
1395        self.assertEqual(a.addr_spec, '@buzz')
1396        self.assertEqual(str(a), '@buzz')
1397
1398    def test_username_only(self):
1399        # This isn't really a valid address.
1400        a = Address(username='buzz')
1401        self.assertEqual(a.display_name, '')
1402        self.assertEqual(a.username, 'buzz')
1403        self.assertEqual(a.domain, '')
1404        self.assertEqual(a.addr_spec, 'buzz')
1405        self.assertEqual(str(a), 'buzz')
1406
1407    def test_display_name_only(self):
1408        a = Address('buzz')
1409        self.assertEqual(a.display_name, 'buzz')
1410        self.assertEqual(a.username, '')
1411        self.assertEqual(a.domain, '')
1412        self.assertEqual(a.addr_spec, '<>')
1413        self.assertEqual(str(a), 'buzz <>')
1414
1415    def test_quoting(self):
1416        # Ideally we'd check every special individually, but I'm not up for
1417        # writing that many tests.
1418        a = Address('Sara J.', 'bad name', 'example.com')
1419        self.assertEqual(a.display_name, 'Sara J.')
1420        self.assertEqual(a.username, 'bad name')
1421        self.assertEqual(a.domain, 'example.com')
1422        self.assertEqual(a.addr_spec, '"bad name"@example.com')
1423        self.assertEqual(str(a), '"Sara J." <"bad name"@example.com>')
1424
1425    def test_il8n(self):
1426        a = Address('Éric', 'wok', 'exàmple.com')
1427        self.assertEqual(a.display_name, 'Éric')
1428        self.assertEqual(a.username, 'wok')
1429        self.assertEqual(a.domain, 'exàmple.com')
1430        self.assertEqual(a.addr_spec, 'wok@exàmple.com')
1431        self.assertEqual(str(a), 'Éric <wok@exàmple.com>')
1432
1433    # XXX: there is an API design issue that needs to be solved here.
1434    #def test_non_ascii_username_raises(self):
1435    #    with self.assertRaises(ValueError):
1436    #        Address('foo', 'wők', 'example.com')
1437
1438    def test_non_ascii_username_in_addr_spec_raises(self):
1439        with self.assertRaises(ValueError):
1440            Address('foo', addr_spec='wők@example.com')
1441
1442    def test_address_addr_spec_and_username_raises(self):
1443        with self.assertRaises(TypeError):
1444            Address('foo', username='bing', addr_spec='bar@baz')
1445
1446    def test_address_addr_spec_and_domain_raises(self):
1447        with self.assertRaises(TypeError):
1448            Address('foo', domain='bing', addr_spec='bar@baz')
1449
1450    def test_address_addr_spec_and_username_and_domain_raises(self):
1451        with self.assertRaises(TypeError):
1452            Address('foo', username='bong', domain='bing', addr_spec='bar@baz')
1453
1454    def test_space_in_addr_spec_username_raises(self):
1455        with self.assertRaises(ValueError):
1456            Address('foo', addr_spec="bad name@example.com")
1457
1458    def test_bad_addr_sepc_raises(self):
1459        with self.assertRaises(ValueError):
1460            Address('foo', addr_spec="name@ex[]ample.com")
1461
1462    def test_empty_group(self):
1463        g = Group('foo')
1464        self.assertEqual(g.display_name, 'foo')
1465        self.assertEqual(g.addresses, tuple())
1466        self.assertEqual(str(g), 'foo:;')
1467
1468    def test_empty_group_list(self):
1469        g = Group('foo', addresses=[])
1470        self.assertEqual(g.display_name, 'foo')
1471        self.assertEqual(g.addresses, tuple())
1472        self.assertEqual(str(g), 'foo:;')
1473
1474    def test_null_group(self):
1475        g = Group()
1476        self.assertIsNone(g.display_name)
1477        self.assertEqual(g.addresses, tuple())
1478        self.assertEqual(str(g), 'None:;')
1479
1480    def test_group_with_addresses(self):
1481        addrs = [Address('b', 'b', 'c'), Address('a', 'b','c')]
1482        g = Group('foo', addrs)
1483        self.assertEqual(g.display_name, 'foo')
1484        self.assertEqual(g.addresses, tuple(addrs))
1485        self.assertEqual(str(g), 'foo: b <b@c>, a <b@c>;')
1486
1487    def test_group_with_addresses_no_display_name(self):
1488        addrs = [Address('b', 'b', 'c'), Address('a', 'b','c')]
1489        g = Group(addresses=addrs)
1490        self.assertIsNone(g.display_name)
1491        self.assertEqual(g.addresses, tuple(addrs))
1492        self.assertEqual(str(g), 'None: b <b@c>, a <b@c>;')
1493
1494    def test_group_with_one_address_no_display_name(self):
1495        addrs = [Address('b', 'b', 'c')]
1496        g = Group(addresses=addrs)
1497        self.assertIsNone(g.display_name)
1498        self.assertEqual(g.addresses, tuple(addrs))
1499        self.assertEqual(str(g), 'b <b@c>')
1500
1501    def test_display_name_quoting(self):
1502        g = Group('foo.bar')
1503        self.assertEqual(g.display_name, 'foo.bar')
1504        self.assertEqual(g.addresses, tuple())
1505        self.assertEqual(str(g), '"foo.bar":;')
1506
1507    def test_display_name_blanks_not_quoted(self):
1508        g = Group('foo bar')
1509        self.assertEqual(g.display_name, 'foo bar')
1510        self.assertEqual(g.addresses, tuple())
1511        self.assertEqual(str(g), 'foo bar:;')
1512
1513    def test_set_message_header_from_address(self):
1514        a = Address('foo', 'bar', 'example.com')
1515        m = Message(policy=policy.default)
1516        m['To'] = a
1517        self.assertEqual(m['to'], 'foo <bar@example.com>')
1518        self.assertEqual(m['to'].addresses, (a,))
1519
1520    def test_set_message_header_from_group(self):
1521        g = Group('foo bar')
1522        m = Message(policy=policy.default)
1523        m['To'] = g
1524        self.assertEqual(m['to'], 'foo bar:;')
1525        self.assertEqual(m['to'].addresses, g.addresses)
1526
1527
1528class TestFolding(TestHeaderBase):
1529
1530    def test_short_unstructured(self):
1531        h = self.make_header('subject', 'this is a test')
1532        self.assertEqual(h.fold(policy=policy.default),
1533                         'subject: this is a test\n')
1534
1535    def test_long_unstructured(self):
1536        h = self.make_header('Subject', 'This is a long header '
1537            'line that will need to be folded into two lines '
1538            'and will demonstrate basic folding')
1539        self.assertEqual(h.fold(policy=policy.default),
1540                        'Subject: This is a long header line that will '
1541                            'need to be folded into two lines\n'
1542                        ' and will demonstrate basic folding\n')
1543
1544    def test_unstructured_short_max_line_length(self):
1545        h = self.make_header('Subject', 'this is a short header '
1546            'that will be folded anyway')
1547        self.assertEqual(
1548            h.fold(policy=policy.default.clone(max_line_length=20)),
1549            textwrap.dedent("""\
1550                Subject: this is a
1551                 short header that
1552                 will be folded
1553                 anyway
1554                """))
1555
1556    def test_fold_unstructured_single_word(self):
1557        h = self.make_header('Subject', 'test')
1558        self.assertEqual(h.fold(policy=policy.default), 'Subject: test\n')
1559
1560    def test_fold_unstructured_short(self):
1561        h = self.make_header('Subject', 'test test test')
1562        self.assertEqual(h.fold(policy=policy.default),
1563                        'Subject: test test test\n')
1564
1565    def test_fold_unstructured_with_overlong_word(self):
1566        h = self.make_header('Subject', 'thisisaverylonglineconsistingofa'
1567            'singlewordthatwontfit')
1568        self.assertEqual(
1569            h.fold(policy=policy.default.clone(max_line_length=20)),
1570            'Subject: \n'
1571            ' =?utf-8?q?thisisa?=\n'
1572            ' =?utf-8?q?verylon?=\n'
1573            ' =?utf-8?q?glineco?=\n'
1574            ' =?utf-8?q?nsistin?=\n'
1575            ' =?utf-8?q?gofasin?=\n'
1576            ' =?utf-8?q?gleword?=\n'
1577            ' =?utf-8?q?thatwon?=\n'
1578            ' =?utf-8?q?tfit?=\n'
1579            )
1580
1581    def test_fold_unstructured_with_two_overlong_words(self):
1582        h = self.make_header('Subject', 'thisisaverylonglineconsistingofa'
1583            'singlewordthatwontfit plusanotherverylongwordthatwontfit')
1584        self.assertEqual(
1585            h.fold(policy=policy.default.clone(max_line_length=20)),
1586            'Subject: \n'
1587            ' =?utf-8?q?thisisa?=\n'
1588            ' =?utf-8?q?verylon?=\n'
1589            ' =?utf-8?q?glineco?=\n'
1590            ' =?utf-8?q?nsistin?=\n'
1591            ' =?utf-8?q?gofasin?=\n'
1592            ' =?utf-8?q?gleword?=\n'
1593            ' =?utf-8?q?thatwon?=\n'
1594            ' =?utf-8?q?tfit_pl?=\n'
1595            ' =?utf-8?q?usanoth?=\n'
1596            ' =?utf-8?q?erveryl?=\n'
1597            ' =?utf-8?q?ongword?=\n'
1598            ' =?utf-8?q?thatwon?=\n'
1599            ' =?utf-8?q?tfit?=\n'
1600            )
1601
1602    # XXX Need test for when max_line_length is less than the chrome size.
1603
1604    def test_fold_unstructured_with_slightly_long_word(self):
1605        h = self.make_header('Subject', 'thislongwordislessthanmaxlinelen')
1606        self.assertEqual(
1607            h.fold(policy=policy.default.clone(max_line_length=35)),
1608            'Subject:\n thislongwordislessthanmaxlinelen\n')
1609
1610    def test_fold_unstructured_with_commas(self):
1611        # The old wrapper would fold this at the commas.
1612        h = self.make_header('Subject', "This header is intended to "
1613            "demonstrate, in a fairly succinct way, that we now do "
1614            "not give a , special treatment in unstructured headers.")
1615        self.assertEqual(
1616            h.fold(policy=policy.default.clone(max_line_length=60)),
1617            textwrap.dedent("""\
1618                Subject: This header is intended to demonstrate, in a fairly
1619                 succinct way, that we now do not give a , special treatment
1620                 in unstructured headers.
1621                 """))
1622
1623    def test_fold_address_list(self):
1624        h = self.make_header('To', '"Theodore H. Perfect" <yes@man.com>, '
1625            '"My address is very long because my name is long" <foo@bar.com>, '
1626            '"Only A. Friend" <no@yes.com>')
1627        self.assertEqual(h.fold(policy=policy.default), textwrap.dedent("""\
1628            To: "Theodore H. Perfect" <yes@man.com>,
1629             "My address is very long because my name is long" <foo@bar.com>,
1630             "Only A. Friend" <no@yes.com>
1631             """))
1632
1633    def test_fold_date_header(self):
1634        h = self.make_header('Date', 'Sat, 2 Feb 2002 17:00:06 -0800')
1635        self.assertEqual(h.fold(policy=policy.default),
1636                        'Date: Sat, 02 Feb 2002 17:00:06 -0800\n')
1637
1638    def test_fold_overlong_words_using_RFC2047(self):
1639        h = self.make_header(
1640            'X-Report-Abuse',
1641            '<https://www.mailitapp.com/report_abuse.php?'
1642              'mid=xxx-xxx-xxxxxxxxxxxxxxxxxxxxxxxx==-xxx-xx-xx>')
1643        self.assertEqual(
1644            h.fold(policy=policy.default),
1645            'X-Report-Abuse: =?utf-8?q?=3Chttps=3A//www=2Emailitapp=2E'
1646                'com/report=5F?=\n'
1647            ' =?utf-8?q?abuse=2Ephp=3Fmid=3Dxxx-xxx-xxxx'
1648                'xxxxxxxxxxxxxxxxxxxx=3D=3D-xxx-?=\n'
1649            ' =?utf-8?q?xx-xx=3E?=\n')
1650
1651
1652if __name__ == '__main__':
1653    unittest.main()
1654