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