You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
email.parser.BytesParser.parse() cannot handle binary data that include \x0d \x0a correctly. #128949
I would like to extract a binary file in a multipart MIME file using email.parser.BytesParser, but a byte sequence "0x0d 0x0a" (CR + LF) in the binary file is replaced by "0x0a" (LF). Below is a minimal reproducible example.
fromemail.parserimportBytesParserfromemail.policyimportdefaultfromioimportBytesIOmime_file_byte_array=b'MIME-Version: 1.0\r\nContent-Type: multipart/mixed; boundary="MIME\_boundary-1";\r\n\r\n--MIME_boundary-1\r\nContent-Type: application/octet-stream\r\nContent\-Location: test.bin\r\n\r\na\r\nb\r\n--MIME_boundary-1--\r\n\r\n'fp=BytesIO(mime_file_byte_array)
parser=BytesParser(policy=default)
msg=parser.parse(fp)
parts= [partforpartinmsg.walk()]
binary_data=parts[1].get_payload(decode=True)
print('===== Beginning of Original MIME File =====')
print(mime_file_byte_array.decode())
print('===== End of Original MIME File =====')
print('')
print('===== test.bin after parse =====')
print(binary_data)
print('===== test.bin after parse =====')
As can be seen in the fifth line, the multipart MIME file includes a binary file "test.bin". The contents of the binary file is b"a\r\nb".
Therefore, the variable binary_data is supposed to contain b"a\r\nb", but it was actually b"a\nb".
It is probably because TextIOWrapper in BytesParser.parse() translates CR+LF to LF on Linux.
Using the parameter newline='' resolves this issue. By default (when newline is None), the wrapper translates CRLF/LF to os.linesep. When newline is set to an empty string, the wrapper does not modify CRLF/LF.
Thank you for your comment, @RanKKI . Yes, it is understandable if this translation from CRLF to os.linesep is activated only for the text/* types. I guess that some python applications that use the email.parser module may be relying on this feature of the line break translation. So, in order to maintain the backward compatibility, a possible solution is to deactivate this translation if the content type is not text/*.
It does not seem inconceivable to add the newline parameter to BytesParser.parse as it seems to have been done in other cases where disabling universal newline conversion is desirable or, alternatively, using BytesParser.parsebytes in some cases.
This issue results in corrupt images and attachments when parsing messages that use Content-Transfer-Encoding: 8bit. In case that's not obvious from the example in the original report, here's another:
fromemailimportpolicyfromemail.parserimportBytesParserfromioimportBytesIO# A 10x2573 GIF image (some data omitted)gif=b"GIF89a\x0a\x00\x0d\x0a...\x3b"raw_message=b"""\Subject: Here's a GIF\rContent-Type: image/gif\rContent-Transfer-Encoding: 8bit\r\r"""+gifmsg1=BytesParser(policy=policy.default).parsebytes(raw_message)
print(repr(msg1.get_content())) # b'GIF89a\n\x00\r\n...;'assertmsg1.get_content() ==gif# Works correctlymsg2=BytesParser(policy=policy.default).parse(BytesIO(raw_message))
print(repr(msg2.get_content())) # b'GIF89a\n\x00\n...;assertmsg2.get_content() ==gif# AssertionError (corrupted image)
A related problem is that text parts will have inconsistent line endings, depending which BytesParser method was used to parse the message.
Disabling newline conversion where BytesParser.parse() uses a TextIOWrapper seems workable, and a minimal fix.
Another option would be switching to a BytesFeedParser within BytesParser, rather than trying to force bytes-as-text intoParser. (Eventually everything ends up in a FeedParser anyway; this would just change the intermediary and avoid the TextIOWrapper. The distinction between bytes and non-bytes parsers is kind of left over from Python 2.)
Activity
RanKKI commentedon Feb 16, 2025
Using the parameter
newline=''resolves this issue. By default (whennewlineis None), the wrapper translates CRLF/LF toos.linesep. When newline is set to an empty string, the wrapper does not modify CRLF/LF.cpython/Modules/_io/textio.c
Lines 1085 to 1089 in a7d41a8
But,
according to RFC 2046#4.1.1,
text/*types must always use CRLF line endingsThe current implementation has a bug (I guess), and I don't see an easy solution to fix it.
tnakamot commentedon Feb 16, 2025
Thank you for your comment, @RanKKI . Yes, it is understandable if this translation from CRLF to
os.linesepis activated only for thetext/*types. I guess that some python applications that use theemail.parsermodule may be relying on this feature of the line break translation. So, in order to maintain the backward compatibility, a possible solution is to deactivate this translation if the content type is nottext/*.aeurielesn commentedon Jul 20, 2025
It does not seem inconceivable to add the
newlineparameter toBytesParser.parseas it seems to have been done in other cases where disabling universal newline conversion is desirable or, alternatively, usingBytesParser.parsebytesin some cases.medmunds commentedon Sep 5, 2026
This issue results in corrupt images and attachments when parsing messages that use
Content-Transfer-Encoding: 8bit. In case that's not obvious from the example in the original report, here's another:A related problem is that text parts will have inconsistent line endings, depending which
BytesParsermethod was used to parse the message.Disabling newline conversion where
BytesParser.parse()uses aTextIOWrapperseems workable, and a minimal fix.Another option would be switching to a
BytesFeedParserwithinBytesParser, rather than trying to force bytes-as-text intoParser. (Eventually everything ends up in aFeedParseranyway; this would just change the intermediary and avoid theTextIOWrapper. The distinction between bytes and non-bytes parsers is kind of left over from Python 2.)I'd defer to @bitdancer on the better approach.
pythongh-128949: keep mailbox binary-stream newline normalization
pythongh-128949: fix BytesHeaderParser NEWS reference
Merge branch 'main' into fix/pythongh-128949-email-newlines
Merge branch 'main' into fix/pythongh-128949-email-newlines