1import unittest 2import textwrap 3from email import policy, message_from_string 4from email.message import EmailMessage, MIMEPart 5from test.test_email import TestEmailBase, parameterize 6 7 8# Helper. 9def first(iterable): 10 return next(filter(lambda x: x is not None, iterable), None) 11 12 13class Test(TestEmailBase): 14 15 policy = policy.default 16 17 def test_error_on_setitem_if_max_count_exceeded(self): 18 m = self._str_msg("") 19 m['To'] = 'abc@xyz' 20 with self.assertRaises(ValueError): 21 m['To'] = 'xyz@abc' 22 23 def test_rfc2043_auto_decoded_and_emailmessage_used(self): 24 m = message_from_string(textwrap.dedent("""\ 25 Subject: Ayons asperges pour le =?utf-8?q?d=C3=A9jeuner?= 26 From: =?utf-8?q?Pep=C3=A9?= Le Pew <pepe@example.com> 27 To: "Penelope Pussycat" <"penelope@example.com"> 28 MIME-Version: 1.0 29 Content-Type: text/plain; charset="utf-8" 30 31 sample text 32 """), policy=policy.default) 33 self.assertEqual(m['subject'], "Ayons asperges pour le déjeuner") 34 self.assertEqual(m['from'], "Pepé Le Pew <pepe@example.com>") 35 self.assertIsInstance(m, EmailMessage) 36 37 38@parameterize 39class TestEmailMessageBase: 40 41 policy = policy.default 42 43 # The first argument is a triple (related, html, plain) of indices into the 44 # list returned by 'walk' called on a Message constructed from the third. 45 # The indices indicate which part should match the corresponding part-type 46 # when passed to get_body (ie: the "first" part of that type in the 47 # message). The second argument is a list of indices into the 'walk' list 48 # of the attachments that should be returned by a call to 49 # 'iter_attachments'. The third argument is a list of indices into 'walk' 50 # that should be returned by a call to 'iter_parts'. Note that the first 51 # item returned by 'walk' is the Message itself. 52 53 message_params = { 54 55 'empty_message': ( 56 (None, None, 0), 57 (), 58 (), 59 ""), 60 61 'non_mime_plain': ( 62 (None, None, 0), 63 (), 64 (), 65 textwrap.dedent("""\ 66 To: foo@example.com 67 68 simple text body 69 """)), 70 71 'mime_non_text': ( 72 (None, None, None), 73 (), 74 (), 75 textwrap.dedent("""\ 76 To: foo@example.com 77 MIME-Version: 1.0 78 Content-Type: image/jpg 79 80 bogus body. 81 """)), 82 83 'plain_html_alternative': ( 84 (None, 2, 1), 85 (), 86 (1, 2), 87 textwrap.dedent("""\ 88 To: foo@example.com 89 MIME-Version: 1.0 90 Content-Type: multipart/alternative; boundary="===" 91 92 preamble 93 94 --=== 95 Content-Type: text/plain 96 97 simple body 98 99 --=== 100 Content-Type: text/html 101 102 <p>simple body</p> 103 --===-- 104 """)), 105 106 'plain_html_mixed': ( 107 (None, 2, 1), 108 (), 109 (1, 2), 110 textwrap.dedent("""\ 111 To: foo@example.com 112 MIME-Version: 1.0 113 Content-Type: multipart/mixed; boundary="===" 114 115 preamble 116 117 --=== 118 Content-Type: text/plain 119 120 simple body 121 122 --=== 123 Content-Type: text/html 124 125 <p>simple body</p> 126 127 --===-- 128 """)), 129 130 'plain_html_attachment_mixed': ( 131 (None, None, 1), 132 (2,), 133 (1, 2), 134 textwrap.dedent("""\ 135 To: foo@example.com 136 MIME-Version: 1.0 137 Content-Type: multipart/mixed; boundary="===" 138 139 --=== 140 Content-Type: text/plain 141 142 simple body 143 144 --=== 145 Content-Type: text/html 146 Content-Disposition: attachment 147 148 <p>simple body</p> 149 150 --===-- 151 """)), 152 153 'html_text_attachment_mixed': ( 154 (None, 2, None), 155 (1,), 156 (1, 2), 157 textwrap.dedent("""\ 158 To: foo@example.com 159 MIME-Version: 1.0 160 Content-Type: multipart/mixed; boundary="===" 161 162 --=== 163 Content-Type: text/plain 164 Content-Disposition: AtTaChment 165 166 simple body 167 168 --=== 169 Content-Type: text/html 170 171 <p>simple body</p> 172 173 --===-- 174 """)), 175 176 'html_text_attachment_inline_mixed': ( 177 (None, 2, 1), 178 (), 179 (1, 2), 180 textwrap.dedent("""\ 181 To: foo@example.com 182 MIME-Version: 1.0 183 Content-Type: multipart/mixed; boundary="===" 184 185 --=== 186 Content-Type: text/plain 187 Content-Disposition: InLine 188 189 simple body 190 191 --=== 192 Content-Type: text/html 193 Content-Disposition: inline 194 195 <p>simple body</p> 196 197 --===-- 198 """)), 199 200 # RFC 2387 201 'related': ( 202 (0, 1, None), 203 (2,), 204 (1, 2), 205 textwrap.dedent("""\ 206 To: foo@example.com 207 MIME-Version: 1.0 208 Content-Type: multipart/related; boundary="==="; type=text/html 209 210 --=== 211 Content-Type: text/html 212 213 <p>simple body</p> 214 215 --=== 216 Content-Type: image/jpg 217 Content-ID: <image1> 218 219 bogus data 220 221 --===-- 222 """)), 223 224 # This message structure will probably never be seen in the wild, but 225 # it proves we distinguish between text parts based on 'start'. The 226 # content would not, of course, actually work :) 227 'related_with_start': ( 228 (0, 2, None), 229 (1,), 230 (1, 2), 231 textwrap.dedent("""\ 232 To: foo@example.com 233 MIME-Version: 1.0 234 Content-Type: multipart/related; boundary="==="; type=text/html; 235 start="<body>" 236 237 --=== 238 Content-Type: text/html 239 Content-ID: <include> 240 241 useless text 242 243 --=== 244 Content-Type: text/html 245 Content-ID: <body> 246 247 <p>simple body</p> 248 <!--#include file="<include>"--> 249 250 --===-- 251 """)), 252 253 254 'mixed_alternative_plain_related': ( 255 (3, 4, 2), 256 (6, 7), 257 (1, 6, 7), 258 textwrap.dedent("""\ 259 To: foo@example.com 260 MIME-Version: 1.0 261 Content-Type: multipart/mixed; boundary="===" 262 263 --=== 264 Content-Type: multipart/alternative; boundary="+++" 265 266 --+++ 267 Content-Type: text/plain 268 269 simple body 270 271 --+++ 272 Content-Type: multipart/related; boundary="___" 273 274 --___ 275 Content-Type: text/html 276 277 <p>simple body</p> 278 279 --___ 280 Content-Type: image/jpg 281 Content-ID: <image1@cid> 282 283 bogus jpg body 284 285 --___-- 286 287 --+++-- 288 289 --=== 290 Content-Type: image/jpg 291 Content-Disposition: attachment 292 293 bogus jpg body 294 295 --=== 296 Content-Type: image/jpg 297 Content-Disposition: AttacHmenT 298 299 another bogus jpg body 300 301 --===-- 302 """)), 303 304 # This structure suggested by Stephen J. Turnbull...may not exist/be 305 # supported in the wild, but we want to support it. 306 'mixed_related_alternative_plain_html': ( 307 (1, 4, 3), 308 (6, 7), 309 (1, 6, 7), 310 textwrap.dedent("""\ 311 To: foo@example.com 312 MIME-Version: 1.0 313 Content-Type: multipart/mixed; boundary="===" 314 315 --=== 316 Content-Type: multipart/related; boundary="+++" 317 318 --+++ 319 Content-Type: multipart/alternative; boundary="___" 320 321 --___ 322 Content-Type: text/plain 323 324 simple body 325 326 --___ 327 Content-Type: text/html 328 329 <p>simple body</p> 330 331 --___-- 332 333 --+++ 334 Content-Type: image/jpg 335 Content-ID: <image1@cid> 336 337 bogus jpg body 338 339 --+++-- 340 341 --=== 342 Content-Type: image/jpg 343 Content-Disposition: attachment 344 345 bogus jpg body 346 347 --=== 348 Content-Type: image/jpg 349 Content-Disposition: attachment 350 351 another bogus jpg body 352 353 --===-- 354 """)), 355 356 # Same thing, but proving we only look at the root part, which is the 357 # first one if there isn't any start parameter. That is, this is a 358 # broken related. 359 'mixed_related_alternative_plain_html_wrong_order': ( 360 (1, None, None), 361 (6, 7), 362 (1, 6, 7), 363 textwrap.dedent("""\ 364 To: foo@example.com 365 MIME-Version: 1.0 366 Content-Type: multipart/mixed; boundary="===" 367 368 --=== 369 Content-Type: multipart/related; boundary="+++" 370 371 --+++ 372 Content-Type: image/jpg 373 Content-ID: <image1@cid> 374 375 bogus jpg body 376 377 --+++ 378 Content-Type: multipart/alternative; boundary="___" 379 380 --___ 381 Content-Type: text/plain 382 383 simple body 384 385 --___ 386 Content-Type: text/html 387 388 <p>simple body</p> 389 390 --___-- 391 392 --+++-- 393 394 --=== 395 Content-Type: image/jpg 396 Content-Disposition: attachment 397 398 bogus jpg body 399 400 --=== 401 Content-Type: image/jpg 402 Content-Disposition: attachment 403 404 another bogus jpg body 405 406 --===-- 407 """)), 408 409 'message_rfc822': ( 410 (None, None, None), 411 (), 412 (), 413 textwrap.dedent("""\ 414 To: foo@example.com 415 MIME-Version: 1.0 416 Content-Type: message/rfc822 417 418 To: bar@example.com 419 From: robot@examp.com 420 421 this is a message body. 422 """)), 423 424 'mixed_text_message_rfc822': ( 425 (None, None, 1), 426 (2,), 427 (1, 2), 428 textwrap.dedent("""\ 429 To: foo@example.com 430 MIME-Version: 1.0 431 Content-Type: multipart/mixed; boundary="===" 432 433 --=== 434 Content-Type: text/plain 435 436 Your message has bounced, sir. 437 438 --=== 439 Content-Type: message/rfc822 440 441 To: bar@example.com 442 From: robot@examp.com 443 444 this is a message body. 445 446 --===-- 447 """)), 448 449 } 450 451 def message_as_get_body(self, body_parts, attachments, parts, msg): 452 m = self._str_msg(msg) 453 allparts = list(m.walk()) 454 expected = [None if n is None else allparts[n] for n in body_parts] 455 related = 0; html = 1; plain = 2 456 self.assertEqual(m.get_body(), first(expected)) 457 self.assertEqual(m.get_body(preferencelist=( 458 'related', 'html', 'plain')), 459 first(expected)) 460 self.assertEqual(m.get_body(preferencelist=('related', 'html')), 461 first(expected[related:html+1])) 462 self.assertEqual(m.get_body(preferencelist=('related', 'plain')), 463 first([expected[related], expected[plain]])) 464 self.assertEqual(m.get_body(preferencelist=('html', 'plain')), 465 first(expected[html:plain+1])) 466 self.assertEqual(m.get_body(preferencelist=['related']), 467 expected[related]) 468 self.assertEqual(m.get_body(preferencelist=['html']), expected[html]) 469 self.assertEqual(m.get_body(preferencelist=['plain']), expected[plain]) 470 self.assertEqual(m.get_body(preferencelist=('plain', 'html')), 471 first(expected[plain:html-1:-1])) 472 self.assertEqual(m.get_body(preferencelist=('plain', 'related')), 473 first([expected[plain], expected[related]])) 474 self.assertEqual(m.get_body(preferencelist=('html', 'related')), 475 first(expected[html::-1])) 476 self.assertEqual(m.get_body(preferencelist=('plain', 'html', 'related')), 477 first(expected[::-1])) 478 self.assertEqual(m.get_body(preferencelist=('html', 'plain', 'related')), 479 first([expected[html], 480 expected[plain], 481 expected[related]])) 482 483 def message_as_iter_attachment(self, body_parts, attachments, parts, msg): 484 m = self._str_msg(msg) 485 allparts = list(m.walk()) 486 attachments = [allparts[n] for n in attachments] 487 self.assertEqual(list(m.iter_attachments()), attachments) 488 489 def message_as_iter_parts(self, body_parts, attachments, parts, msg): 490 def _is_multipart_msg(msg): 491 return 'Content-Type: multipart' in msg 492 493 m = self._str_msg(msg) 494 allparts = list(m.walk()) 495 parts = [allparts[n] for n in parts] 496 iter_parts = list(m.iter_parts()) if _is_multipart_msg(msg) else [] 497 self.assertEqual(iter_parts, parts) 498 499 class _TestContentManager: 500 def get_content(self, msg, *args, **kw): 501 return msg, args, kw 502 def set_content(self, msg, *args, **kw): 503 self.msg = msg 504 self.args = args 505 self.kw = kw 506 507 def test_get_content_with_cm(self): 508 m = self._str_msg('') 509 cm = self._TestContentManager() 510 self.assertEqual(m.get_content(content_manager=cm), (m, (), {})) 511 msg, args, kw = m.get_content('foo', content_manager=cm, bar=1, k=2) 512 self.assertEqual(msg, m) 513 self.assertEqual(args, ('foo',)) 514 self.assertEqual(kw, dict(bar=1, k=2)) 515 516 def test_get_content_default_cm_comes_from_policy(self): 517 p = policy.default.clone(content_manager=self._TestContentManager()) 518 m = self._str_msg('', policy=p) 519 self.assertEqual(m.get_content(), (m, (), {})) 520 msg, args, kw = m.get_content('foo', bar=1, k=2) 521 self.assertEqual(msg, m) 522 self.assertEqual(args, ('foo',)) 523 self.assertEqual(kw, dict(bar=1, k=2)) 524 525 def test_set_content_with_cm(self): 526 m = self._str_msg('') 527 cm = self._TestContentManager() 528 m.set_content(content_manager=cm) 529 self.assertEqual(cm.msg, m) 530 self.assertEqual(cm.args, ()) 531 self.assertEqual(cm.kw, {}) 532 m.set_content('foo', content_manager=cm, bar=1, k=2) 533 self.assertEqual(cm.msg, m) 534 self.assertEqual(cm.args, ('foo',)) 535 self.assertEqual(cm.kw, dict(bar=1, k=2)) 536 537 def test_set_content_default_cm_comes_from_policy(self): 538 cm = self._TestContentManager() 539 p = policy.default.clone(content_manager=cm) 540 m = self._str_msg('', policy=p) 541 m.set_content() 542 self.assertEqual(cm.msg, m) 543 self.assertEqual(cm.args, ()) 544 self.assertEqual(cm.kw, {}) 545 m.set_content('foo', bar=1, k=2) 546 self.assertEqual(cm.msg, m) 547 self.assertEqual(cm.args, ('foo',)) 548 self.assertEqual(cm.kw, dict(bar=1, k=2)) 549 550 # outcome is whether xxx_method should raise ValueError error when called 551 # on multipart/subtype. Blank outcome means it depends on xxx (add 552 # succeeds, make raises). Note: 'none' means there are content-type 553 # headers but payload is None...this happening in practice would be very 554 # unusual, so treating it as if there were content seems reasonable. 555 # method subtype outcome 556 subtype_params = ( 557 ('related', 'no_content', 'succeeds'), 558 ('related', 'none', 'succeeds'), 559 ('related', 'plain', 'succeeds'), 560 ('related', 'related', ''), 561 ('related', 'alternative', 'raises'), 562 ('related', 'mixed', 'raises'), 563 ('alternative', 'no_content', 'succeeds'), 564 ('alternative', 'none', 'succeeds'), 565 ('alternative', 'plain', 'succeeds'), 566 ('alternative', 'related', 'succeeds'), 567 ('alternative', 'alternative', ''), 568 ('alternative', 'mixed', 'raises'), 569 ('mixed', 'no_content', 'succeeds'), 570 ('mixed', 'none', 'succeeds'), 571 ('mixed', 'plain', 'succeeds'), 572 ('mixed', 'related', 'succeeds'), 573 ('mixed', 'alternative', 'succeeds'), 574 ('mixed', 'mixed', ''), 575 ) 576 577 def _make_subtype_test_message(self, subtype): 578 m = self.message() 579 payload = None 580 msg_headers = [ 581 ('To', 'foo@bar.com'), 582 ('From', 'bar@foo.com'), 583 ] 584 if subtype != 'no_content': 585 ('content-shadow', 'Logrus'), 586 msg_headers.append(('X-Random-Header', 'Corwin')) 587 if subtype == 'text': 588 payload = '' 589 msg_headers.append(('Content-Type', 'text/plain')) 590 m.set_payload('') 591 elif subtype != 'no_content': 592 payload = [] 593 msg_headers.append(('Content-Type', 'multipart/' + subtype)) 594 msg_headers.append(('X-Trump', 'Random')) 595 m.set_payload(payload) 596 for name, value in msg_headers: 597 m[name] = value 598 return m, msg_headers, payload 599 600 def _check_disallowed_subtype_raises(self, m, method_name, subtype, method): 601 with self.assertRaises(ValueError) as ar: 602 getattr(m, method)() 603 exc_text = str(ar.exception) 604 self.assertIn(subtype, exc_text) 605 self.assertIn(method_name, exc_text) 606 607 def _check_make_multipart(self, m, msg_headers, payload): 608 count = 0 609 for name, value in msg_headers: 610 if not name.lower().startswith('content-'): 611 self.assertEqual(m[name], value) 612 count += 1 613 self.assertEqual(len(m), count+1) # +1 for new Content-Type 614 part = next(m.iter_parts()) 615 count = 0 616 for name, value in msg_headers: 617 if name.lower().startswith('content-'): 618 self.assertEqual(part[name], value) 619 count += 1 620 self.assertEqual(len(part), count) 621 self.assertEqual(part.get_payload(), payload) 622 623 def subtype_as_make(self, method, subtype, outcome): 624 m, msg_headers, payload = self._make_subtype_test_message(subtype) 625 make_method = 'make_' + method 626 if outcome in ('', 'raises'): 627 self._check_disallowed_subtype_raises(m, method, subtype, make_method) 628 return 629 getattr(m, make_method)() 630 self.assertEqual(m.get_content_maintype(), 'multipart') 631 self.assertEqual(m.get_content_subtype(), method) 632 if subtype == 'no_content': 633 self.assertEqual(len(m.get_payload()), 0) 634 self.assertEqual(m.items(), 635 msg_headers + [('Content-Type', 636 'multipart/'+method)]) 637 else: 638 self.assertEqual(len(m.get_payload()), 1) 639 self._check_make_multipart(m, msg_headers, payload) 640 641 def subtype_as_make_with_boundary(self, method, subtype, outcome): 642 # Doing all variation is a bit of overkill... 643 m = self.message() 644 if outcome in ('', 'raises'): 645 m['Content-Type'] = 'multipart/' + subtype 646 with self.assertRaises(ValueError) as cm: 647 getattr(m, 'make_' + method)() 648 return 649 if subtype == 'plain': 650 m['Content-Type'] = 'text/plain' 651 elif subtype != 'no_content': 652 m['Content-Type'] = 'multipart/' + subtype 653 getattr(m, 'make_' + method)(boundary="abc") 654 self.assertTrue(m.is_multipart()) 655 self.assertEqual(m.get_boundary(), 'abc') 656 657 def test_policy_on_part_made_by_make_comes_from_message(self): 658 for method in ('make_related', 'make_alternative', 'make_mixed'): 659 m = self.message(policy=self.policy.clone(content_manager='foo')) 660 m['Content-Type'] = 'text/plain' 661 getattr(m, method)() 662 self.assertEqual(m.get_payload(0).policy.content_manager, 'foo') 663 664 class _TestSetContentManager: 665 def set_content(self, msg, content, *args, **kw): 666 msg['Content-Type'] = 'text/plain' 667 msg.set_payload(content) 668 669 def subtype_as_add(self, method, subtype, outcome): 670 m, msg_headers, payload = self._make_subtype_test_message(subtype) 671 cm = self._TestSetContentManager() 672 add_method = 'add_attachment' if method=='mixed' else 'add_' + method 673 if outcome == 'raises': 674 self._check_disallowed_subtype_raises(m, method, subtype, add_method) 675 return 676 getattr(m, add_method)('test', content_manager=cm) 677 self.assertEqual(m.get_content_maintype(), 'multipart') 678 self.assertEqual(m.get_content_subtype(), method) 679 if method == subtype or subtype == 'no_content': 680 self.assertEqual(len(m.get_payload()), 1) 681 for name, value in msg_headers: 682 self.assertEqual(m[name], value) 683 part = m.get_payload()[0] 684 else: 685 self.assertEqual(len(m.get_payload()), 2) 686 self._check_make_multipart(m, msg_headers, payload) 687 part = m.get_payload()[1] 688 self.assertEqual(part.get_content_type(), 'text/plain') 689 self.assertEqual(part.get_payload(), 'test') 690 if method=='mixed': 691 self.assertEqual(part['Content-Disposition'], 'attachment') 692 elif method=='related': 693 self.assertEqual(part['Content-Disposition'], 'inline') 694 else: 695 # Otherwise we don't guess. 696 self.assertIsNone(part['Content-Disposition']) 697 698 class _TestSetRaisingContentManager: 699 def set_content(self, msg, content, *args, **kw): 700 raise Exception('test') 701 702 def test_default_content_manager_for_add_comes_from_policy(self): 703 cm = self._TestSetRaisingContentManager() 704 m = self.message(policy=self.policy.clone(content_manager=cm)) 705 for method in ('add_related', 'add_alternative', 'add_attachment'): 706 with self.assertRaises(Exception) as ar: 707 getattr(m, method)('') 708 self.assertEqual(str(ar.exception), 'test') 709 710 def message_as_clear(self, body_parts, attachments, parts, msg): 711 m = self._str_msg(msg) 712 m.clear() 713 self.assertEqual(len(m), 0) 714 self.assertEqual(list(m.items()), []) 715 self.assertIsNone(m.get_payload()) 716 self.assertEqual(list(m.iter_parts()), []) 717 718 def message_as_clear_content(self, body_parts, attachments, parts, msg): 719 m = self._str_msg(msg) 720 expected_headers = [h for h in m.keys() 721 if not h.lower().startswith('content-')] 722 m.clear_content() 723 self.assertEqual(list(m.keys()), expected_headers) 724 self.assertIsNone(m.get_payload()) 725 self.assertEqual(list(m.iter_parts()), []) 726 727 def test_is_attachment(self): 728 m = self._make_message() 729 self.assertFalse(m.is_attachment()) 730 m['Content-Disposition'] = 'inline' 731 self.assertFalse(m.is_attachment()) 732 m.replace_header('Content-Disposition', 'attachment') 733 self.assertTrue(m.is_attachment()) 734 m.replace_header('Content-Disposition', 'AtTachMent') 735 self.assertTrue(m.is_attachment()) 736 m.set_param('filename', 'abc.png', 'Content-Disposition') 737 self.assertTrue(m.is_attachment()) 738 739 def test_iter_attachments_mutation(self): 740 # We had a bug where iter_attachments was mutating the list. 741 m = self._make_message() 742 m.set_content('arbitrary text as main part') 743 m.add_related('more text as a related part') 744 m.add_related('yet more text as a second "attachment"') 745 orig = m.get_payload().copy() 746 self.assertEqual(len(list(m.iter_attachments())), 2) 747 self.assertEqual(m.get_payload(), orig) 748 749 750class TestEmailMessage(TestEmailMessageBase, TestEmailBase): 751 message = EmailMessage 752 753 def test_set_content_adds_MIME_Version(self): 754 m = self._str_msg('') 755 cm = self._TestContentManager() 756 self.assertNotIn('MIME-Version', m) 757 m.set_content(content_manager=cm) 758 self.assertEqual(m['MIME-Version'], '1.0') 759 760 class _MIME_Version_adding_CM: 761 def set_content(self, msg, *args, **kw): 762 msg['MIME-Version'] = '1.0' 763 764 def test_set_content_does_not_duplicate_MIME_Version(self): 765 m = self._str_msg('') 766 cm = self._MIME_Version_adding_CM() 767 self.assertNotIn('MIME-Version', m) 768 m.set_content(content_manager=cm) 769 self.assertEqual(m['MIME-Version'], '1.0') 770 771 def test_as_string_uses_max_header_length_by_default(self): 772 m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n') 773 self.assertEqual(len(m.as_string().strip().splitlines()), 3) 774 775 def test_as_string_allows_maxheaderlen(self): 776 m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n') 777 self.assertEqual(len(m.as_string(maxheaderlen=0).strip().splitlines()), 778 1) 779 self.assertEqual(len(m.as_string(maxheaderlen=34).strip().splitlines()), 780 6) 781 782 def test_as_string_unixform(self): 783 m = self._str_msg('test') 784 m.set_unixfrom('From foo@bar Thu Jan 1 00:00:00 1970') 785 self.assertEqual(m.as_string(unixfrom=True), 786 'From foo@bar Thu Jan 1 00:00:00 1970\n\ntest') 787 self.assertEqual(m.as_string(unixfrom=False), '\ntest') 788 789 def test_str_defaults_to_policy_max_line_length(self): 790 m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n') 791 self.assertEqual(len(str(m).strip().splitlines()), 3) 792 793 def test_str_defaults_to_utf8(self): 794 m = EmailMessage() 795 m['Subject'] = 'unicöde' 796 self.assertEqual(str(m), 'Subject: unicöde\n\n') 797 798 def test_folding_with_utf8_encoding_1(self): 799 # bpo-36520 800 # 801 # Fold a line that contains UTF-8 words before 802 # and after the whitespace fold point, where the 803 # line length limit is reached within an ASCII 804 # word. 805 806 m = EmailMessage() 807 m['Subject'] = 'Hello Wörld! Hello Wörld! ' \ 808 'Hello Wörld! Hello Wörld!Hello Wörld!' 809 self.assertEqual(bytes(m), 810 b'Subject: Hello =?utf-8?q?W=C3=B6rld!_Hello_W' 811 b'=C3=B6rld!_Hello_W=C3=B6rld!?=\n' 812 b' Hello =?utf-8?q?W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n') 813 814 815 def test_folding_with_utf8_encoding_2(self): 816 # bpo-36520 817 # 818 # Fold a line that contains UTF-8 words before 819 # and after the whitespace fold point, where the 820 # line length limit is reached at the end of an 821 # encoded word. 822 823 m = EmailMessage() 824 m['Subject'] = 'Hello Wörld! Hello Wörld! ' \ 825 'Hello Wörlds123! Hello Wörld!Hello Wörld!' 826 self.assertEqual(bytes(m), 827 b'Subject: Hello =?utf-8?q?W=C3=B6rld!_Hello_W' 828 b'=C3=B6rld!_Hello_W=C3=B6rlds123!?=\n' 829 b' Hello =?utf-8?q?W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n') 830 831 def test_folding_with_utf8_encoding_3(self): 832 # bpo-36520 833 # 834 # Fold a line that contains UTF-8 words before 835 # and after the whitespace fold point, where the 836 # line length limit is reached at the end of the 837 # first word. 838 839 m = EmailMessage() 840 m['Subject'] = 'Hello-Wörld!-Hello-Wörld!-Hello-Wörlds123! ' \ 841 'Hello Wörld!Hello Wörld!' 842 self.assertEqual(bytes(m), \ 843 b'Subject: =?utf-8?q?Hello-W=C3=B6rld!-Hello-W' 844 b'=C3=B6rld!-Hello-W=C3=B6rlds123!?=\n' 845 b' Hello =?utf-8?q?W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n') 846 847 def test_folding_with_utf8_encoding_4(self): 848 # bpo-36520 849 # 850 # Fold a line that contains UTF-8 words before 851 # and after the fold point, where the first 852 # word is UTF-8 and the fold point is within 853 # the word. 854 855 m = EmailMessage() 856 m['Subject'] = 'Hello-Wörld!-Hello-Wörld!-Hello-Wörlds123!-Hello' \ 857 ' Wörld!Hello Wörld!' 858 self.assertEqual(bytes(m), 859 b'Subject: =?utf-8?q?Hello-W=C3=B6rld!-Hello-W' 860 b'=C3=B6rld!-Hello-W=C3=B6rlds123!?=\n' 861 b' =?utf-8?q?-Hello_W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n') 862 863 def test_folding_with_utf8_encoding_5(self): 864 # bpo-36520 865 # 866 # Fold a line that contains a UTF-8 word after 867 # the fold point. 868 869 m = EmailMessage() 870 m['Subject'] = '123456789 123456789 123456789 123456789 123456789' \ 871 ' 123456789 123456789 Hello Wörld!' 872 self.assertEqual(bytes(m), 873 b'Subject: 123456789 123456789 123456789 123456789' 874 b' 123456789 123456789 123456789\n' 875 b' Hello =?utf-8?q?W=C3=B6rld!?=\n\n') 876 877 def test_folding_with_utf8_encoding_6(self): 878 # bpo-36520 879 # 880 # Fold a line that contains a UTF-8 word before 881 # the fold point and ASCII words after 882 883 m = EmailMessage() 884 m['Subject'] = '123456789 123456789 123456789 123456789 Hello Wörld!' \ 885 ' 123456789 123456789 123456789 123456789 123456789' \ 886 ' 123456789' 887 self.assertEqual(bytes(m), 888 b'Subject: 123456789 123456789 123456789 123456789' 889 b' Hello =?utf-8?q?W=C3=B6rld!?=\n 123456789 ' 890 b'123456789 123456789 123456789 123456789 ' 891 b'123456789\n\n') 892 893 def test_folding_with_utf8_encoding_7(self): 894 # bpo-36520 895 # 896 # Fold a line twice that contains UTF-8 words before 897 # and after the first fold point, and ASCII words 898 # after the second fold point. 899 900 m = EmailMessage() 901 m['Subject'] = '123456789 123456789 Hello Wörld! Hello Wörld! ' \ 902 '123456789-123456789 123456789 Hello Wörld! 123456789' \ 903 ' 123456789' 904 self.assertEqual(bytes(m), 905 b'Subject: 123456789 123456789 Hello =?utf-8?q?' 906 b'W=C3=B6rld!_Hello_W=C3=B6rld!?=\n' 907 b' 123456789-123456789 123456789 Hello ' 908 b'=?utf-8?q?W=C3=B6rld!?= 123456789\n 123456789\n\n') 909 910 def test_folding_with_utf8_encoding_8(self): 911 # bpo-36520 912 # 913 # Fold a line twice that contains UTF-8 words before 914 # the first fold point, and ASCII words after the 915 # first fold point, and UTF-8 words after the second 916 # fold point. 917 918 m = EmailMessage() 919 m['Subject'] = '123456789 123456789 Hello Wörld! Hello Wörld! ' \ 920 '123456789 123456789 123456789 123456789 123456789 ' \ 921 '123456789-123456789 123456789 Hello Wörld! 123456789' \ 922 ' 123456789' 923 self.assertEqual(bytes(m), 924 b'Subject: 123456789 123456789 Hello ' 925 b'=?utf-8?q?W=C3=B6rld!_Hello_W=C3=B6rld!?=\n 123456789 ' 926 b'123456789 123456789 123456789 123456789 ' 927 b'123456789-123456789\n 123456789 Hello ' 928 b'=?utf-8?q?W=C3=B6rld!?= 123456789 123456789\n\n') 929 930 def test_get_body_malformed(self): 931 """test for bpo-42892""" 932 msg = textwrap.dedent("""\ 933 Message-ID: <674392CA.4347091@email.au> 934 Date: Wed, 08 Nov 2017 08:50:22 +0700 935 From: Foo Bar <email@email.au> 936 MIME-Version: 1.0 937 To: email@email.com <email@email.com> 938 Subject: Python Email 939 Content-Type: multipart/mixed; 940 boundary="------------879045806563892972123996" 941 X-Global-filter:Messagescannedforspamandviruses:passedalltests 942 943 This is a multi-part message in MIME format. 944 --------------879045806563892972123996 945 Content-Type: text/plain; charset=ISO-8859-1; format=flowed 946 Content-Transfer-Encoding: 7bit 947 948 Your message is ready to be sent with the following file or link 949 attachments: 950 XU89 - 08.11.2017 951 """) 952 m = self._str_msg(msg) 953 # In bpo-42892, this would raise 954 # AttributeError: 'str' object has no attribute 'is_attachment' 955 m.get_body() 956 957 958class TestMIMEPart(TestEmailMessageBase, TestEmailBase): 959 # Doing the full test run here may seem a bit redundant, since the two 960 # classes are almost identical. But what if they drift apart? So we do 961 # the full tests so that any future drift doesn't introduce bugs. 962 message = MIMEPart 963 964 def test_set_content_does_not_add_MIME_Version(self): 965 m = self._str_msg('') 966 cm = self._TestContentManager() 967 self.assertNotIn('MIME-Version', m) 968 m.set_content(content_manager=cm) 969 self.assertNotIn('MIME-Version', m) 970 971 def test_string_payload_with_multipart_content_type(self): 972 msg = message_from_string(textwrap.dedent("""\ 973 Content-Type: multipart/mixed; charset="utf-8" 974 975 sample text 976 """), policy=policy.default) 977 attachments = msg.iter_attachments() 978 self.assertEqual(list(attachments), []) 979 980 981if __name__ == '__main__': 982 unittest.main() 983