python (3.12.0)
1 import unittest
2 import textwrap
3 from email import policy, message_from_string
4 from email.message import EmailMessage, MIMEPart
5 from test.test_email import TestEmailBase, parameterize
6
7
8 # Helper.
9 def first(iterable):
10 return next(filter(lambda x: x is not None, iterable), None)
11
12
13 class ESC[4;38;5;81mTest(ESC[4;38;5;149mTestEmailBase):
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
39 class ESC[4;38;5;81mTestEmailMessageBase:
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 ESC[4;38;5;81m_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 ESC[4;38;5;81m_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 ESC[4;38;5;81m_TestSetRaisingContentManager:
699 class ESC[4;38;5;81mCustomError(ESC[4;38;5;149mException):
700 pass
701 def set_content(self, msg, content, *args, **kw):
702 raise self.CustomError('test')
703
704 def test_default_content_manager_for_add_comes_from_policy(self):
705 cm = self._TestSetRaisingContentManager()
706 m = self.message(policy=self.policy.clone(content_manager=cm))
707 for method in ('add_related', 'add_alternative', 'add_attachment'):
708 with self.assertRaises(self._TestSetRaisingContentManager.CustomError) as ar:
709 getattr(m, method)('')
710 self.assertEqual(str(ar.exception), 'test')
711
712 def message_as_clear(self, body_parts, attachments, parts, msg):
713 m = self._str_msg(msg)
714 m.clear()
715 self.assertEqual(len(m), 0)
716 self.assertEqual(list(m.items()), [])
717 self.assertIsNone(m.get_payload())
718 self.assertEqual(list(m.iter_parts()), [])
719
720 def message_as_clear_content(self, body_parts, attachments, parts, msg):
721 m = self._str_msg(msg)
722 expected_headers = [h for h in m.keys()
723 if not h.lower().startswith('content-')]
724 m.clear_content()
725 self.assertEqual(list(m.keys()), expected_headers)
726 self.assertIsNone(m.get_payload())
727 self.assertEqual(list(m.iter_parts()), [])
728
729 def test_is_attachment(self):
730 m = self._make_message()
731 self.assertFalse(m.is_attachment())
732 m['Content-Disposition'] = 'inline'
733 self.assertFalse(m.is_attachment())
734 m.replace_header('Content-Disposition', 'attachment')
735 self.assertTrue(m.is_attachment())
736 m.replace_header('Content-Disposition', 'AtTachMent')
737 self.assertTrue(m.is_attachment())
738 m.set_param('filename', 'abc.png', 'Content-Disposition')
739 self.assertTrue(m.is_attachment())
740
741 def test_iter_attachments_mutation(self):
742 # We had a bug where iter_attachments was mutating the list.
743 m = self._make_message()
744 m.set_content('arbitrary text as main part')
745 m.add_related('more text as a related part')
746 m.add_related('yet more text as a second "attachment"')
747 orig = m.get_payload().copy()
748 self.assertEqual(len(list(m.iter_attachments())), 2)
749 self.assertEqual(m.get_payload(), orig)
750
751
752 class ESC[4;38;5;81mTestEmailMessage(ESC[4;38;5;149mTestEmailMessageBase, ESC[4;38;5;149mTestEmailBase):
753 message = EmailMessage
754
755 def test_set_content_adds_MIME_Version(self):
756 m = self._str_msg('')
757 cm = self._TestContentManager()
758 self.assertNotIn('MIME-Version', m)
759 m.set_content(content_manager=cm)
760 self.assertEqual(m['MIME-Version'], '1.0')
761
762 class ESC[4;38;5;81m_MIME_Version_adding_CM:
763 def set_content(self, msg, *args, **kw):
764 msg['MIME-Version'] = '1.0'
765
766 def test_set_content_does_not_duplicate_MIME_Version(self):
767 m = self._str_msg('')
768 cm = self._MIME_Version_adding_CM()
769 self.assertNotIn('MIME-Version', m)
770 m.set_content(content_manager=cm)
771 self.assertEqual(m['MIME-Version'], '1.0')
772
773 def test_as_string_uses_max_header_length_by_default(self):
774 m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n')
775 self.assertEqual(len(m.as_string().strip().splitlines()), 3)
776
777 def test_as_string_allows_maxheaderlen(self):
778 m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n')
779 self.assertEqual(len(m.as_string(maxheaderlen=0).strip().splitlines()),
780 1)
781 self.assertEqual(len(m.as_string(maxheaderlen=34).strip().splitlines()),
782 6)
783
784 def test_as_string_unixform(self):
785 m = self._str_msg('test')
786 m.set_unixfrom('From foo@bar Thu Jan 1 00:00:00 1970')
787 self.assertEqual(m.as_string(unixfrom=True),
788 'From foo@bar Thu Jan 1 00:00:00 1970\n\ntest')
789 self.assertEqual(m.as_string(unixfrom=False), '\ntest')
790
791 def test_str_defaults_to_policy_max_line_length(self):
792 m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n')
793 self.assertEqual(len(str(m).strip().splitlines()), 3)
794
795 def test_str_defaults_to_utf8(self):
796 m = EmailMessage()
797 m['Subject'] = 'unicöde'
798 self.assertEqual(str(m), 'Subject: unicöde\n\n')
799
800 def test_folding_with_utf8_encoding_1(self):
801 # bpo-36520
802 #
803 # Fold a line that contains UTF-8 words before
804 # and after the whitespace fold point, where the
805 # line length limit is reached within an ASCII
806 # word.
807
808 m = EmailMessage()
809 m['Subject'] = 'Hello Wörld! Hello Wörld! ' \
810 'Hello Wörld! Hello Wörld!Hello Wörld!'
811 self.assertEqual(bytes(m),
812 b'Subject: Hello =?utf-8?q?W=C3=B6rld!_Hello_W'
813 b'=C3=B6rld!_Hello_W=C3=B6rld!?=\n'
814 b' Hello =?utf-8?q?W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n')
815
816
817 def test_folding_with_utf8_encoding_2(self):
818 # bpo-36520
819 #
820 # Fold a line that contains UTF-8 words before
821 # and after the whitespace fold point, where the
822 # line length limit is reached at the end of an
823 # encoded word.
824
825 m = EmailMessage()
826 m['Subject'] = 'Hello Wörld! Hello Wörld! ' \
827 'Hello Wörlds123! Hello Wörld!Hello Wörld!'
828 self.assertEqual(bytes(m),
829 b'Subject: Hello =?utf-8?q?W=C3=B6rld!_Hello_W'
830 b'=C3=B6rld!_Hello_W=C3=B6rlds123!?=\n'
831 b' Hello =?utf-8?q?W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n')
832
833 def test_folding_with_utf8_encoding_3(self):
834 # bpo-36520
835 #
836 # Fold a line that contains UTF-8 words before
837 # and after the whitespace fold point, where the
838 # line length limit is reached at the end of the
839 # first word.
840
841 m = EmailMessage()
842 m['Subject'] = 'Hello-Wörld!-Hello-Wörld!-Hello-Wörlds123! ' \
843 'Hello Wörld!Hello Wörld!'
844 self.assertEqual(bytes(m), \
845 b'Subject: =?utf-8?q?Hello-W=C3=B6rld!-Hello-W'
846 b'=C3=B6rld!-Hello-W=C3=B6rlds123!?=\n'
847 b' Hello =?utf-8?q?W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n')
848
849 def test_folding_with_utf8_encoding_4(self):
850 # bpo-36520
851 #
852 # Fold a line that contains UTF-8 words before
853 # and after the fold point, where the first
854 # word is UTF-8 and the fold point is within
855 # the word.
856
857 m = EmailMessage()
858 m['Subject'] = 'Hello-Wörld!-Hello-Wörld!-Hello-Wörlds123!-Hello' \
859 ' Wörld!Hello Wörld!'
860 self.assertEqual(bytes(m),
861 b'Subject: =?utf-8?q?Hello-W=C3=B6rld!-Hello-W'
862 b'=C3=B6rld!-Hello-W=C3=B6rlds123!?=\n'
863 b' =?utf-8?q?-Hello_W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n')
864
865 def test_folding_with_utf8_encoding_5(self):
866 # bpo-36520
867 #
868 # Fold a line that contains a UTF-8 word after
869 # the fold point.
870
871 m = EmailMessage()
872 m['Subject'] = '123456789 123456789 123456789 123456789 123456789' \
873 ' 123456789 123456789 Hello Wörld!'
874 self.assertEqual(bytes(m),
875 b'Subject: 123456789 123456789 123456789 123456789'
876 b' 123456789 123456789 123456789\n'
877 b' Hello =?utf-8?q?W=C3=B6rld!?=\n\n')
878
879 def test_folding_with_utf8_encoding_6(self):
880 # bpo-36520
881 #
882 # Fold a line that contains a UTF-8 word before
883 # the fold point and ASCII words after
884
885 m = EmailMessage()
886 m['Subject'] = '123456789 123456789 123456789 123456789 Hello Wörld!' \
887 ' 123456789 123456789 123456789 123456789 123456789' \
888 ' 123456789'
889 self.assertEqual(bytes(m),
890 b'Subject: 123456789 123456789 123456789 123456789'
891 b' Hello =?utf-8?q?W=C3=B6rld!?=\n 123456789 '
892 b'123456789 123456789 123456789 123456789 '
893 b'123456789\n\n')
894
895 def test_folding_with_utf8_encoding_7(self):
896 # bpo-36520
897 #
898 # Fold a line twice that contains UTF-8 words before
899 # and after the first fold point, and ASCII words
900 # after the second fold point.
901
902 m = EmailMessage()
903 m['Subject'] = '123456789 123456789 Hello Wörld! Hello Wörld! ' \
904 '123456789-123456789 123456789 Hello Wörld! 123456789' \
905 ' 123456789'
906 self.assertEqual(bytes(m),
907 b'Subject: 123456789 123456789 Hello =?utf-8?q?'
908 b'W=C3=B6rld!_Hello_W=C3=B6rld!?=\n'
909 b' 123456789-123456789 123456789 Hello '
910 b'=?utf-8?q?W=C3=B6rld!?= 123456789\n 123456789\n\n')
911
912 def test_folding_with_utf8_encoding_8(self):
913 # bpo-36520
914 #
915 # Fold a line twice that contains UTF-8 words before
916 # the first fold point, and ASCII words after the
917 # first fold point, and UTF-8 words after the second
918 # fold point.
919
920 m = EmailMessage()
921 m['Subject'] = '123456789 123456789 Hello Wörld! Hello Wörld! ' \
922 '123456789 123456789 123456789 123456789 123456789 ' \
923 '123456789-123456789 123456789 Hello Wörld! 123456789' \
924 ' 123456789'
925 self.assertEqual(bytes(m),
926 b'Subject: 123456789 123456789 Hello '
927 b'=?utf-8?q?W=C3=B6rld!_Hello_W=C3=B6rld!?=\n 123456789 '
928 b'123456789 123456789 123456789 123456789 '
929 b'123456789-123456789\n 123456789 Hello '
930 b'=?utf-8?q?W=C3=B6rld!?= 123456789 123456789\n\n')
931
932 def test_get_body_malformed(self):
933 """test for bpo-42892"""
934 msg = textwrap.dedent("""\
935 Message-ID: <674392CA.4347091@email.au>
936 Date: Wed, 08 Nov 2017 08:50:22 +0700
937 From: Foo Bar <email@email.au>
938 MIME-Version: 1.0
939 To: email@email.com <email@email.com>
940 Subject: Python Email
941 Content-Type: multipart/mixed;
942 boundary="------------879045806563892972123996"
943 X-Global-filter:Messagescannedforspamandviruses:passedalltests
944
945 This is a multi-part message in MIME format.
946 --------------879045806563892972123996
947 Content-Type: text/plain; charset=ISO-8859-1; format=flowed
948 Content-Transfer-Encoding: 7bit
949
950 Your message is ready to be sent with the following file or link
951 attachments:
952 XU89 - 08.11.2017
953 """)
954 m = self._str_msg(msg)
955 # In bpo-42892, this would raise
956 # AttributeError: 'str' object has no attribute 'is_attachment'
957 m.get_body()
958
959
960 class ESC[4;38;5;81mTestMIMEPart(ESC[4;38;5;149mTestEmailMessageBase, ESC[4;38;5;149mTestEmailBase):
961 # Doing the full test run here may seem a bit redundant, since the two
962 # classes are almost identical. But what if they drift apart? So we do
963 # the full tests so that any future drift doesn't introduce bugs.
964 message = MIMEPart
965
966 def test_set_content_does_not_add_MIME_Version(self):
967 m = self._str_msg('')
968 cm = self._TestContentManager()
969 self.assertNotIn('MIME-Version', m)
970 m.set_content(content_manager=cm)
971 self.assertNotIn('MIME-Version', m)
972
973 def test_string_payload_with_multipart_content_type(self):
974 msg = message_from_string(textwrap.dedent("""\
975 Content-Type: multipart/mixed; charset="utf-8"
976
977 sample text
978 """), policy=policy.default)
979 attachments = msg.iter_attachments()
980 self.assertEqual(list(attachments), [])
981
982
983 if __name__ == '__main__':
984 unittest.main()