diff --git a/Lib/email/generator.py b/Lib/email/generator.py --- a/Lib/email/generator.py +++ b/Lib/email/generator.py @@ -119,6 +119,19 @@ # BytesGenerator overrides this to encode strings to bytes. return s + def _write_lines(self, lines): + # We have to transform the line endings. + if not lines: + return + lines = lines.splitlines(True) + for line in lines[:-1]: + self.write(line.rstrip('\r\n')) + self.write(self._NL) + laststripped = lines[-1].rstrip('\r\n') + self.write(laststripped) + if len(lines[-1])!=len(laststripped): + self.write(self._NL) + def _write(self, msg): # We can't write the headers yet because of the following scenario: # say a multipart message includes the boundary string somewhere in @@ -198,7 +211,7 @@ payload = msg.get_payload() if self._mangle_from_: payload = fcre.sub('>From ', payload) - self.write(payload) + self._write_lines(payload) # Default body handler _writeBody = _handle_text @@ -237,7 +250,8 @@ preamble = fcre.sub('>From ', msg.preamble) else: preamble = msg.preamble - self.write(preamble + self._NL) + self._write_lines(preamble) + self.write(self._NL) # dash-boundary transport-padding CRLF self.write('--' + boundary + self._NL) # body-part @@ -259,7 +273,7 @@ epilogue = fcre.sub('>From ', msg.epilogue) else: epilogue = msg.epilogue - self.write(epilogue) + self._write_lines(epilogue) def _handle_multipart_signed(self, msg): # The contents of signed parts has to stay unmodified in order to keep @@ -393,7 +407,7 @@ if _has_surrogates(msg._payload): if self._mangle_from_: msg._payload = fcre.sub(">From ", msg._payload) - self.write(msg._payload) + self._write_lines(msg._payload) else: super(BytesGenerator,self)._handle_text(msg) diff --git a/Lib/email/test/test_email.py b/Lib/email/test/test_email.py --- a/Lib/email/test/test_email.py +++ b/Lib/email/test/test_email.py @@ -68,6 +68,7 @@ with openfile(findfile(filename)) as fp: return email.message_from_file(fp) + maxDiff = None # Test various aspects of the Message class's API @@ -2907,6 +2908,40 @@ email.utils.make_msgid(domain='testdomain-string')[-19:], '@testdomain-string>') + def test_Generator_linend(self): + # Issue 14645. + with openfile('msg_26.txt', newline='\n') as f: + msgtxt = f.read() + msgtxt_nl = msgtxt.replace('\r\n', '\n') + msg = email.message_from_string(msgtxt) + s = StringIO() + g = email.generator.Generator(s) + g.flatten(msg) + self.assertEqual(s.getvalue(), msgtxt_nl) + + def test_BytesGenerator_linend(self): + # Issue 14645. + with openfile('msg_26.txt', newline='\n') as f: + msgtxt = f.read() + msgtxt_nl = msgtxt.replace('\r\n', '\n') + msg = email.message_from_string(msgtxt_nl) + s = BytesIO() + g = email.generator.BytesGenerator(s) + g.flatten(msg, linesep='\r\n') + self.assertEqual(s.getvalue().decode('ascii'), msgtxt) + + def test_BytesGenerator_linend_with_non_ascii(self): + # Issue 14645. + with openfile('msg_26.txt', 'rb') as f: + msgtxt = f.read() + msgtxt = msgtxt.replace(b'with attachment', b'fo\xf6') + msgtxt_nl = msgtxt.replace(b'\r\n', b'\n') + msg = email.message_from_bytes(msgtxt_nl) + s = BytesIO() + g = email.generator.BytesGenerator(s) + g.flatten(msg, linesep='\r\n') + self.assertEqual(s.getvalue(), msgtxt) + # Test the iterator/generators class TestIterators(TestEmailBase):