From 1dc298c5ea62dda936f9654b32527b72c48c5012 Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Sun, 19 Jul 2026 14:15:00 +0800 Subject: [PATCH] Fix broken string literal that disabled the quoted-printable attachment branch A line-continuation inside the string literal made the comparison target "quoted- printable" (the continuation line's indentation became part of the string), so it never equalled "quoted-printable". The branch handling quoted-printable application/* attachments was therefore dead: such attachments fell through to the text branch and were decoded as UTF-8 with errors="ignore", silently dropping every non-UTF8 byte and mis-flagging binary=False. Join the literal so the comparison works. Added a test with a quoted-printable application/octet-stream attachment that round-trips losslessly. --- src/mailparser/core.py | 4 +--- tests/test_mail_parser.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/mailparser/core.py b/src/mailparser/core.py index 44c0d5f..dc16bb1 100644 --- a/src/mailparser/core.py +++ b/src/mailparser/core.py @@ -407,9 +407,7 @@ def parse(self): binary = False log.debug(f"Filename {filename!r} part {i!r} is multipart") elif transfer_encoding == "base64" or ( - transfer_encoding - == "quoted-\ - printable" + transfer_encoding == "quoted-printable" and "application" in mail_content_type ): payload = p.get_payload(decode=False) diff --git a/tests/test_mail_parser.py b/tests/test_mail_parser.py index c3ef1bc..e1f5836 100644 --- a/tests/test_mail_parser.py +++ b/tests/test_mail_parser.py @@ -399,6 +399,31 @@ def test_defects_bug(self): result = len(mail.attachments) self.assertEqual(1, result) + def test_quoted_printable_application_attachment(self): + # A quoted-printable application/* attachment must be kept as binary + # (raw QP text), not decoded as UTF-8, which drops the non-UTF8 bytes. + import quopri + + original = b"\xff\xfe\x00\x01PDFdata\x80\x81\x82\xc0\xc1" + qp = quopri.encodestring(original).decode("ascii") + raw = ( + "From: a@b.com\r\nTo: c@d.com\r\nSubject: t\r\n" + "MIME-Version: 1.0\r\n" + 'Content-Type: multipart/mixed; boundary="B"\r\n\r\n' + "--B\r\nContent-Type: text/plain\r\n\r\nbody\r\n" + '--B\r\nContent-Type: application/octet-stream; name="f.bin"\r\n' + "Content-Transfer-Encoding: quoted-printable\r\n" + 'Content-Disposition: attachment; filename="f.bin"\r\n\r\n' + + qp + + "\r\n--B--\r\n" + ) + attachment = mailparser.parse_from_string(raw).attachments[0] + self.assertTrue(attachment["binary"]) + self.assertEqual(attachment["content_transfer_encoding"], "quoted-printable") + self.assertEqual( + quopri.decodestring(attachment["payload"].encode("ascii")), original + ) + def test_add_content_type(self): mail = mailparser.parse_from_file(mail_test_3)