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