• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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