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