diff -r 36550e4f9b4c Lib/email/generator.py --- a/Lib/email/generator.py Tue Sep 13 12:21:47 2016 +0000 +++ b/Lib/email/generator.py Tue Sep 13 20:27:59 2016 -0400 @@ -86,6 +86,23 @@ from the policy associated with the msg. """ + # If this is a newly constructed message (there are no parser-derived + # headers) and we have at least one content- header, add the + # MIME-Version header. + add_version = False + if ('MIME-Version' not in msg + and all(hasattr(v, 'name') for n, v in msg.raw_items()) + and any(x.lower().startswith('content-') for x in msg)): + add_version = True + try: + if add_version: + msg['MIME-Version'] = '1.0' + return self._flatten(msg, unixfrom, linesep) + finally: + if add_version: + del msg['MIME-Version'] + + def _flatten(self, msg, unixfrom=False, linesep=None): # We use the _XXX constants for operating on data that comes directly # from the msg, and _encoded_XXX constants for operating on data that # has already been converted (to bytes in the BytesGenerator) and @@ -269,7 +286,7 @@ for part in subparts: s = self._new_buffer() g = self.clone(s) - g.flatten(part, unixfrom=False, linesep=self._NL) + g._flatten(part, unixfrom=False, linesep=self._NL) msgtexts.append(s.getvalue()) # BAW: What about boundaries that are wrapped in double-quotes? boundary = msg.get_boundary() @@ -328,7 +345,7 @@ for part in msg.get_payload(): s = self._new_buffer() g = self.clone(s) - g.flatten(part, unixfrom=False, linesep=self._NL) + g._flatten(part, unixfrom=False, linesep=self._NL) text = s.getvalue() lines = text.split(self._encoded_NL) # Strip off the unnecessary trailing empty line @@ -355,7 +372,7 @@ # in that case we just emit the string body. payload = msg._payload if isinstance(payload, list): - g.flatten(msg.get_payload(0), unixfrom=False, linesep=self._NL) + g._flatten(msg.get_payload(0), unixfrom=False, linesep=self._NL) payload = s.getvalue() else: payload = self._encode(payload) diff -r 36550e4f9b4c Lib/email/message.py --- a/Lib/email/message.py Tue Sep 13 12:21:47 2016 +0000 +++ b/Lib/email/message.py Tue Sep 13 20:27:59 2016 -0400 @@ -943,7 +943,7 @@ from email.iterators import walk -class MIMEPart(Message): +class EmailMessage(Message): def __init__(self, policy=None): if policy is None: @@ -1136,10 +1136,5 @@ if not n.lower().startswith('content-')] self._payload = None - -class EmailMessage(MIMEPart): - - def set_content(self, *args, **kw): - super().set_content(*args, **kw) - if 'MIME-Version' not in self: - self['MIME-Version'] = '1.0' +# Backward compatibility +MIMEPart = EmailMessage diff -r 36550e4f9b4c Lib/test/test_email/test_contentmanager.py --- a/Lib/test/test_email/test_contentmanager.py Tue Sep 13 12:21:47 2016 +0000 +++ b/Lib/test/test_email/test_contentmanager.py Tue Sep 13 20:27:59 2016 -0400 @@ -297,6 +297,7 @@ self.assertEqual(str(m), textwrap.dedent("""\ Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 Simple message. """)) @@ -310,6 +311,7 @@ self.assertEqual(str(m), textwrap.dedent("""\ Content-Type: text/html; charset="utf-8" Content-Transfer-Encoding: 7bit + MIME-Version: 1.0
Simple message.
""")) @@ -323,6 +325,7 @@ self.assertEqual(str(m), textwrap.dedent("""\ Content-Type: text/plain; charset="iso-8859-1" Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 Simple message. """)) @@ -336,6 +339,7 @@ self.assertEqual(bytes(m), textwrap.dedent("""\ Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 8bit + MIME-Version: 1.0 et là il est monté sur moi et il commence à m'éto. """).encode('utf-8')) @@ -351,6 +355,7 @@ self.assertEqual(bytes(m), textwrap.dedent("""\ Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable + MIME-Version: 1.0 j'ai un probl=C3=A8me de python. il est sorti de son vivari= um. et l=C3=A0 il est mont=C3=A9 sur moi et il commence = @@ -369,6 +374,7 @@ self.assertEqual(bytes(m), textwrap.dedent("""\ Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable + MIME-Version: 1.0 """ + '\n'*10 + """ j'ai un probl=C3=A8me de python. il est sorti de son vivari= um. et l=C3=A0 il est mont=C3=A9 sur moi et il commence = @@ -384,6 +390,7 @@ self.assertEqual(bytes(m), textwrap.dedent("""\ Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 8bit + MIME-Version: 1.0 áàäéèęöő. """).encode('utf-8')) @@ -397,6 +404,7 @@ self.assertEqual(bytes(m), textwrap.dedent("""\ Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 8bit + MIME-Version: 1.0 """ + '\n'*10 + """ áàäéèęöő. """).encode('utf-8')) @@ -412,6 +420,7 @@ self.assertEqual(bytes(m), textwrap.dedent("""\ Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: base64 + MIME-Version: 1.0 w6HDoMOkw6nDqMSZw7bFkcOhw6DDpMOpw6jEmcO2xZHDocOgw6TDqcOoxJnD tsWRw6HDoMOkw6nDqMSZw7bFkcOhw6DDpMOpw6jEmcO2xZHDocOgw6TDqcOo @@ -437,6 +446,7 @@ self.assertEqual(bytes(m), textwrap.dedent("""\ Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable + MIME-Version: 1.0 """ + '\n'*10 + """ =C3=A1=C3=A0=C3=A4=C3=A9=C3=A8=C4=99=C3=B6=C5=91=C3=A1=C3= =A0=C3=A4=C3=A9=C3=A8=C4=99=C3=B6=C5=91=C3=A1=C3=A0=C3=A4= @@ -481,13 +491,13 @@ Subject: Forwarded message Content-Type: message/rfc822 Content-Transfer-Encoding: 8bit + MIME-Version: 1.0 To: python@vivarium.org From: police@monty.org Subject: get back in your box Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 7bit - MIME-Version: 1.0 Or face the comfy chair. """)) @@ -511,13 +521,13 @@ Subject: Escape report Content-Type: message/rfc822 Content-Transfer-Encoding: 8bit + MIME-Version: 1.0 To: police@monty.org From: victim@monty.org Subject: Help Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 8bit - MIME-Version: 1.0 j'ai un problème de python. il est sorti de son vivarium. """).encode('utf-8')) @@ -531,13 +541,13 @@ Subject: Escape report Content-Type: message/rfc822 Content-Transfer-Encoding: 8bit + MIME-Version: 1.0 To: police@monty.org From: victim@monty.org Subject: Help Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: base64 - MIME-Version: 1.0 aidhaSB1biBwcm9ibMOobWUgZGUgcHl0aG9uLiBpbCBlc3Qgc29ydGkgZGUgc29uIHZpdmFyaXVt Lgo= @@ -575,6 +585,7 @@ self.assertEqual(str(m), textwrap.dedent("""\ Content-Type: image/jpeg Content-Transfer-Encoding: base64 + MIME-Version: 1.0 Ym9ndXMgY29udGVudA== """)) @@ -651,6 +662,7 @@ From: foo@example.com Subject: I'm talking to myself. Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 Simple message. """)) @@ -666,6 +678,7 @@ X-Foo-Header: foo X-Bar-Header: bar Content-Transfer-Encoding: 7bit + MIME-Version: 1.0 Simple message. """)) diff -r 36550e4f9b4c Lib/test/test_email/test_generator.py --- a/Lib/test/test_email/test_generator.py Tue Sep 13 12:21:47 2016 +0000 +++ b/Lib/test/test_email/test_generator.py Tue Sep 13 20:27:59 2016 -0400 @@ -12,6 +12,7 @@ class TestGeneratorBase: policy = policy.default + message = EmailMessage def msgmaker(self, msg, policy=None): policy = self.policy if policy is None else policy @@ -143,7 +144,7 @@ def test_set_mangle_from_via_policy(self): source = textwrap.dedent("""\ Subject: test that - from is mangeld in the body! + from is mangled in the body! From time to time I write a rhyme. """) @@ -162,6 +163,64 @@ g.flatten(msg) self.assertEqual(s.getvalue(), self.typ(expected)) + def test_mime_version_added_to_mime_message(self): + msg = self._make_message() + msg['To'] = 'foo@example.com' + msg.add_related('This is a test') + self.assertNotIn('MIME-Version', msg) + serialized = self.serialfunc(msg) + self.assertNotIn('MIME-Version', msg) + self.assertEqual(serialized, self.typ(textwrap.dedent("""\ + To: foo@example.com + Content-Type: multipart/related; boundary="{boundary}" + MIME-Version: 1.0 + + --{boundary} + Content-Type: text/plain; charset="utf-8" + Content-Transfer-Encoding: 7bit + Content-Disposition: inline + + This is a test + + --{boundary}-- + """).format(boundary=msg.get_boundary()))) + + def test_no_mime_version_in_non_mime_message(self): + msg = self._make_message() + msg['To'] = 'foo@example.com' + msg.set_payload('This is a test\n') + serialized = self.serialfunc(msg) + self.assertNotIn('MIME-Version', msg) + self.assertEqual(serialized, self.typ(textwrap.dedent("""\ + To: foo@example.com + + This is a test + """))) + + def test_no_mime_version_added_to_parsed_message(self): + source = self.typ(textwrap.dedent("""\ + Subject: test that broken message stays broken + Content-Type: text/plain + + This message is missing a MIME-Version header. + """)) + msg = self.msgmaker(source) + self.assertEqual(self.serialfunc(msg), source) + self.assertNotIn('MIME-Version', msg) + + def test_no_mime_version_added_to_parsed_message_with_changed_header(self): + source = self.typ(textwrap.dedent("""\ + Subject: test that broken message stays broken + Content-Type: text/plain + %s + This message is missing a MIME-Version header. + """)) + msg = self.msgmaker(source % self.typ('')) + msg['To'] = 'bob@cat.com' + self.assertEqual(self.serialfunc(msg), + source % self.typ('To: bob@cat.com\n')) + self.assertNotIn('MIME-Version', msg) + class TestGenerator(TestGeneratorBase, TestEmailBase): @@ -169,6 +228,7 @@ genclass = Generator ioclass = io.StringIO typ = str + serialfunc = str class TestBytesGenerator(TestGeneratorBase, TestEmailBase): @@ -177,6 +237,7 @@ genclass = BytesGenerator ioclass = io.BytesIO typ = lambda self, x: x.encode('ascii') + serialfunc = bytes def test_cte_type_7bit_handles_unknown_8bit(self): source = ("Subject: Maintenant je vous présente mon " diff -r 36550e4f9b4c Lib/test/test_email/test_message.py --- a/Lib/test/test_email/test_message.py Tue Sep 13 12:21:47 2016 +0000 +++ b/Lib/test/test_email/test_message.py Tue Sep 13 20:27:59 2016 -0400 @@ -36,8 +36,8 @@ @parameterize -class TestEmailMessageBase: - +class TestEmailMessage(TestEmailBase): + message = EmailMessage policy = policy.default # The first argument is a triple (related, html, plain) of indices into the @@ -743,41 +743,5 @@ self.assertEqual(m.get_payload(), orig) -class TestEmailMessage(TestEmailMessageBase, TestEmailBase): - message = EmailMessage - - def test_set_content_adds_MIME_Version(self): - m = self._str_msg('') - cm = self._TestContentManager() - self.assertNotIn('MIME-Version', m) - m.set_content(content_manager=cm) - self.assertEqual(m['MIME-Version'], '1.0') - - class _MIME_Version_adding_CM: - def set_content(self, msg, *args, **kw): - msg['MIME-Version'] = '1.0' - - def test_set_content_does_not_duplicate_MIME_Version(self): - m = self._str_msg('') - cm = self._MIME_Version_adding_CM() - self.assertNotIn('MIME-Version', m) - m.set_content(content_manager=cm) - self.assertEqual(m['MIME-Version'], '1.0') - - -class TestMIMEPart(TestEmailMessageBase, TestEmailBase): - # Doing the full test run here may seem a bit redundant, since the two - # classes are almost identical. But what if they drift apart? So we do - # the full tests so that any future drift doesn't introduce bugs. - message = MIMEPart - - def test_set_content_does_not_add_MIME_Version(self): - m = self._str_msg('') - cm = self._TestContentManager() - self.assertNotIn('MIME-Version', m) - m.set_content(content_manager=cm) - self.assertNotIn('MIME-Version', m) - - if __name__ == '__main__': unittest.main()