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, ser. 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 m = self._str_msg(msg) 491 allparts = list(m.walk()) 492 parts = [allparts[n] for n in parts] 493 self.assertEqual(list(m.iter_parts()), parts) 494 495 class _TestContentManager: 496 def get_content(self, msg, *args, **kw): 497 return msg, args, kw 498 def set_content(self, msg, *args, **kw): 499 self.msg = msg 500 self.args = args 501 self.kw = kw 502 503 def test_get_content_with_cm(self): 504 m = self._str_msg('') 505 cm = self._TestContentManager() 506 self.assertEqual(m.get_content(content_manager=cm), (m, (), {})) 507 msg, args, kw = m.get_content('foo', content_manager=cm, bar=1, k=2) 508 self.assertEqual(msg, m) 509 self.assertEqual(args, ('foo',)) 510 self.assertEqual(kw, dict(bar=1, k=2)) 511 512 def test_get_content_default_cm_comes_from_policy(self): 513 p = policy.default.clone(content_manager=self._TestContentManager()) 514 m = self._str_msg('', policy=p) 515 self.assertEqual(m.get_content(), (m, (), {})) 516 msg, args, kw = m.get_content('foo', bar=1, k=2) 517 self.assertEqual(msg, m) 518 self.assertEqual(args, ('foo',)) 519 self.assertEqual(kw, dict(bar=1, k=2)) 520 521 def test_set_content_with_cm(self): 522 m = self._str_msg('') 523 cm = self._TestContentManager() 524 m.set_content(content_manager=cm) 525 self.assertEqual(cm.msg, m) 526 self.assertEqual(cm.args, ()) 527 self.assertEqual(cm.kw, {}) 528 m.set_content('foo', content_manager=cm, bar=1, k=2) 529 self.assertEqual(cm.msg, m) 530 self.assertEqual(cm.args, ('foo',)) 531 self.assertEqual(cm.kw, dict(bar=1, k=2)) 532 533 def test_set_content_default_cm_comes_from_policy(self): 534 cm = self._TestContentManager() 535 p = policy.default.clone(content_manager=cm) 536 m = self._str_msg('', policy=p) 537 m.set_content() 538 self.assertEqual(cm.msg, m) 539 self.assertEqual(cm.args, ()) 540 self.assertEqual(cm.kw, {}) 541 m.set_content('foo', bar=1, k=2) 542 self.assertEqual(cm.msg, m) 543 self.assertEqual(cm.args, ('foo',)) 544 self.assertEqual(cm.kw, dict(bar=1, k=2)) 545 546 # outcome is whether xxx_method should raise ValueError error when called 547 # on multipart/subtype. Blank outcome means it depends on xxx (add 548 # succeeds, make raises). Note: 'none' means there are content-type 549 # headers but payload is None...this happening in practice would be very 550 # unusual, so treating it as if there were content seems reasonable. 551 # method subtype outcome 552 subtype_params = ( 553 ('related', 'no_content', 'succeeds'), 554 ('related', 'none', 'succeeds'), 555 ('related', 'plain', 'succeeds'), 556 ('related', 'related', ''), 557 ('related', 'alternative', 'raises'), 558 ('related', 'mixed', 'raises'), 559 ('alternative', 'no_content', 'succeeds'), 560 ('alternative', 'none', 'succeeds'), 561 ('alternative', 'plain', 'succeeds'), 562 ('alternative', 'related', 'succeeds'), 563 ('alternative', 'alternative', ''), 564 ('alternative', 'mixed', 'raises'), 565 ('mixed', 'no_content', 'succeeds'), 566 ('mixed', 'none', 'succeeds'), 567 ('mixed', 'plain', 'succeeds'), 568 ('mixed', 'related', 'succeeds'), 569 ('mixed', 'alternative', 'succeeds'), 570 ('mixed', 'mixed', ''), 571 ) 572 573 def _make_subtype_test_message(self, subtype): 574 m = self.message() 575 payload = None 576 msg_headers = [ 577 ('To', 'foo@bar.com'), 578 ('From', 'bar@foo.com'), 579 ] 580 if subtype != 'no_content': 581 ('content-shadow', 'Logrus'), 582 msg_headers.append(('X-Random-Header', 'Corwin')) 583 if subtype == 'text': 584 payload = '' 585 msg_headers.append(('Content-Type', 'text/plain')) 586 m.set_payload('') 587 elif subtype != 'no_content': 588 payload = [] 589 msg_headers.append(('Content-Type', 'multipart/' + subtype)) 590 msg_headers.append(('X-Trump', 'Random')) 591 m.set_payload(payload) 592 for name, value in msg_headers: 593 m[name] = value 594 return m, msg_headers, payload 595 596 def _check_disallowed_subtype_raises(self, m, method_name, subtype, method): 597 with self.assertRaises(ValueError) as ar: 598 getattr(m, method)() 599 exc_text = str(ar.exception) 600 self.assertIn(subtype, exc_text) 601 self.assertIn(method_name, exc_text) 602 603 def _check_make_multipart(self, m, msg_headers, payload): 604 count = 0 605 for name, value in msg_headers: 606 if not name.lower().startswith('content-'): 607 self.assertEqual(m[name], value) 608 count += 1 609 self.assertEqual(len(m), count+1) # +1 for new Content-Type 610 part = next(m.iter_parts()) 611 count = 0 612 for name, value in msg_headers: 613 if name.lower().startswith('content-'): 614 self.assertEqual(part[name], value) 615 count += 1 616 self.assertEqual(len(part), count) 617 self.assertEqual(part.get_payload(), payload) 618 619 def subtype_as_make(self, method, subtype, outcome): 620 m, msg_headers, payload = self._make_subtype_test_message(subtype) 621 make_method = 'make_' + method 622 if outcome in ('', 'raises'): 623 self._check_disallowed_subtype_raises(m, method, subtype, make_method) 624 return 625 getattr(m, make_method)() 626 self.assertEqual(m.get_content_maintype(), 'multipart') 627 self.assertEqual(m.get_content_subtype(), method) 628 if subtype == 'no_content': 629 self.assertEqual(len(m.get_payload()), 0) 630 self.assertEqual(m.items(), 631 msg_headers + [('Content-Type', 632 'multipart/'+method)]) 633 else: 634 self.assertEqual(len(m.get_payload()), 1) 635 self._check_make_multipart(m, msg_headers, payload) 636 637 def subtype_as_make_with_boundary(self, method, subtype, outcome): 638 # Doing all variation is a bit of overkill... 639 m = self.message() 640 if outcome in ('', 'raises'): 641 m['Content-Type'] = 'multipart/' + subtype 642 with self.assertRaises(ValueError) as cm: 643 getattr(m, 'make_' + method)() 644 return 645 if subtype == 'plain': 646 m['Content-Type'] = 'text/plain' 647 elif subtype != 'no_content': 648 m['Content-Type'] = 'multipart/' + subtype 649 getattr(m, 'make_' + method)(boundary="abc") 650 self.assertTrue(m.is_multipart()) 651 self.assertEqual(m.get_boundary(), 'abc') 652 653 def test_policy_on_part_made_by_make_comes_from_message(self): 654 for method in ('make_related', 'make_alternative', 'make_mixed'): 655 m = self.message(policy=self.policy.clone(content_manager='foo')) 656 m['Content-Type'] = 'text/plain' 657 getattr(m, method)() 658 self.assertEqual(m.get_payload(0).policy.content_manager, 'foo') 659 660 class _TestSetContentManager: 661 def set_content(self, msg, content, *args, **kw): 662 msg['Content-Type'] = 'text/plain' 663 msg.set_payload(content) 664 665 def subtype_as_add(self, method, subtype, outcome): 666 m, msg_headers, payload = self._make_subtype_test_message(subtype) 667 cm = self._TestSetContentManager() 668 add_method = 'add_attachment' if method=='mixed' else 'add_' + method 669 if outcome == 'raises': 670 self._check_disallowed_subtype_raises(m, method, subtype, add_method) 671 return 672 getattr(m, add_method)('test', content_manager=cm) 673 self.assertEqual(m.get_content_maintype(), 'multipart') 674 self.assertEqual(m.get_content_subtype(), method) 675 if method == subtype or subtype == 'no_content': 676 self.assertEqual(len(m.get_payload()), 1) 677 for name, value in msg_headers: 678 self.assertEqual(m[name], value) 679 part = m.get_payload()[0] 680 else: 681 self.assertEqual(len(m.get_payload()), 2) 682 self._check_make_multipart(m, msg_headers, payload) 683 part = m.get_payload()[1] 684 self.assertEqual(part.get_content_type(), 'text/plain') 685 self.assertEqual(part.get_payload(), 'test') 686 if method=='mixed': 687 self.assertEqual(part['Content-Disposition'], 'attachment') 688 elif method=='related': 689 self.assertEqual(part['Content-Disposition'], 'inline') 690 else: 691 # Otherwise we don't guess. 692 self.assertIsNone(part['Content-Disposition']) 693 694 class _TestSetRaisingContentManager: 695 def set_content(self, msg, content, *args, **kw): 696 raise Exception('test') 697 698 def test_default_content_manager_for_add_comes_from_policy(self): 699 cm = self._TestSetRaisingContentManager() 700 m = self.message(policy=self.policy.clone(content_manager=cm)) 701 for method in ('add_related', 'add_alternative', 'add_attachment'): 702 with self.assertRaises(Exception) as ar: 703 getattr(m, method)('') 704 self.assertEqual(str(ar.exception), 'test') 705 706 def message_as_clear(self, body_parts, attachments, parts, msg): 707 m = self._str_msg(msg) 708 m.clear() 709 self.assertEqual(len(m), 0) 710 self.assertEqual(list(m.items()), []) 711 self.assertIsNone(m.get_payload()) 712 self.assertEqual(list(m.iter_parts()), []) 713 714 def message_as_clear_content(self, body_parts, attachments, parts, msg): 715 m = self._str_msg(msg) 716 expected_headers = [h for h in m.keys() 717 if not h.lower().startswith('content-')] 718 m.clear_content() 719 self.assertEqual(list(m.keys()), expected_headers) 720 self.assertIsNone(m.get_payload()) 721 self.assertEqual(list(m.iter_parts()), []) 722 723 def test_is_attachment(self): 724 m = self._make_message() 725 self.assertFalse(m.is_attachment()) 726 m['Content-Disposition'] = 'inline' 727 self.assertFalse(m.is_attachment()) 728 m.replace_header('Content-Disposition', 'attachment') 729 self.assertTrue(m.is_attachment()) 730 m.replace_header('Content-Disposition', 'AtTachMent') 731 self.assertTrue(m.is_attachment()) 732 m.set_param('filename', 'abc.png', 'Content-Disposition') 733 self.assertTrue(m.is_attachment()) 734 735 def test_iter_attachments_mutation(self): 736 # We had a bug where iter_attachments was mutating the list. 737 m = self._make_message() 738 m.set_content('arbitrary text as main part') 739 m.add_related('more text as a related part') 740 m.add_related('yet more text as a second "attachment"') 741 orig = m.get_payload().copy() 742 self.assertEqual(len(list(m.iter_attachments())), 2) 743 self.assertEqual(m.get_payload(), orig) 744 745 746class TestEmailMessage(TestEmailMessageBase, TestEmailBase): 747 message = EmailMessage 748 749 def test_set_content_adds_MIME_Version(self): 750 m = self._str_msg('') 751 cm = self._TestContentManager() 752 self.assertNotIn('MIME-Version', m) 753 m.set_content(content_manager=cm) 754 self.assertEqual(m['MIME-Version'], '1.0') 755 756 class _MIME_Version_adding_CM: 757 def set_content(self, msg, *args, **kw): 758 msg['MIME-Version'] = '1.0' 759 760 def test_set_content_does_not_duplicate_MIME_Version(self): 761 m = self._str_msg('') 762 cm = self._MIME_Version_adding_CM() 763 self.assertNotIn('MIME-Version', m) 764 m.set_content(content_manager=cm) 765 self.assertEqual(m['MIME-Version'], '1.0') 766 767 def test_as_string_uses_max_header_length_by_default(self): 768 m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n') 769 self.assertEqual(len(m.as_string().strip().splitlines()), 3) 770 771 def test_as_string_allows_maxheaderlen(self): 772 m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n') 773 self.assertEqual(len(m.as_string(maxheaderlen=0).strip().splitlines()), 774 1) 775 self.assertEqual(len(m.as_string(maxheaderlen=34).strip().splitlines()), 776 6) 777 778 def test_str_defaults_to_policy_max_line_length(self): 779 m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n') 780 self.assertEqual(len(str(m).strip().splitlines()), 3) 781 782 def test_str_defaults_to_utf8(self): 783 m = EmailMessage() 784 m['Subject'] = 'unicöde' 785 self.assertEqual(str(m), 'Subject: unicöde\n\n') 786 787 788class TestMIMEPart(TestEmailMessageBase, TestEmailBase): 789 # Doing the full test run here may seem a bit redundant, since the two 790 # classes are almost identical. But what if they drift apart? So we do 791 # the full tests so that any future drift doesn't introduce bugs. 792 message = MIMEPart 793 794 def test_set_content_does_not_add_MIME_Version(self): 795 m = self._str_msg('') 796 cm = self._TestContentManager() 797 self.assertNotIn('MIME-Version', m) 798 m.set_content(content_manager=cm) 799 self.assertNotIn('MIME-Version', m) 800 801 802if __name__ == '__main__': 803 unittest.main() 804