From 8e44cc32c5b5f7707aeb9384bd75f4add7c2be68 Mon Sep 17 00:00:00 2001 From: Ivo Bellin Salarin Date: Sat, 6 Sep 2025 14:22:26 +0200 Subject: [PATCH 1/4] fix: reporting the patch made by @vadmium --- Lib/email/feedparser.py | 7 +-- Lib/test/test_email/test_defect_handling.py | 43 ++++++++++++- Lib/test/test_email/test_email.py | 68 --------------------- 3 files changed, 44 insertions(+), 74 deletions(-) diff --git a/Lib/email/feedparser.py b/Lib/email/feedparser.py index 9d80a5822af48d..432410572ddf74 100644 --- a/Lib/email/feedparser.py +++ b/Lib/email/feedparser.py @@ -504,10 +504,9 @@ def _parse_headers(self, lines): self._input.unreadline(line) return else: - # Weirdly placed unix-from line. Note this as a defect - # and ignore it. + # Weirdly placed unix-from line. defect = errors.MisplacedEnvelopeHeaderDefect(line) - self._cur.defects.append(defect) + self.policy.handle_defect(self._cur, defect) continue # Split the line on the colon separating field name from value. # There will always be a colon, because if there wasn't the part of @@ -519,7 +518,7 @@ def _parse_headers(self, lines): # message. Track the error but keep going. if i == 0: defect = errors.InvalidHeaderDefect("Missing header name.") - self._cur.defects.append(defect) + self.policy.handle_defect(self._cur, defect) continue assert i>0, "_parse_headers fed line with no : and no leading WS" diff --git a/Lib/test/test_email/test_defect_handling.py b/Lib/test/test_email/test_defect_handling.py index 44e76c8ce5e03a..94d103347e6a5e 100644 --- a/Lib/test/test_email/test_defect_handling.py +++ b/Lib/test/test_email/test_defect_handling.py @@ -15,6 +15,17 @@ class TestDefectsBase: def _raise_point(self, defect): yield + def get_defects(self, obj): + return obj.defects + + def check_defect(self, defect, string): + msg = None + with self._raise_point(defect): + msg = self._str_msg(string) + self.assertEqual(len(self.get_defects(msg)), 1) + self.assertDefectsEqual(self.get_defects(msg), [defect]) + return msg + def test_same_boundary_inner_outer(self): source = textwrap.dedent("""\ Subject: XX @@ -126,12 +137,10 @@ def test_multipart_invalid_cte(self): errors.InvalidMultipartContentTransferEncodingDefect) def test_multipart_no_cte_no_defect(self): - if self.raise_expected: return msg = self._str_msg(self.multipart_msg.format('')) self.assertEqual(len(self.get_defects(msg)), 0) def test_multipart_valid_cte_no_defect(self): - if self.raise_expected: return for cte in ('7bit', '8bit', 'BINary'): msg = self._str_msg( self.multipart_msg.format("\nContent-Transfer-Encoding: "+cte)) @@ -300,8 +309,38 @@ def test_missing_ending_boundary(self): self.assertDefectsEqual(self.get_defects(msg), [errors.CloseBoundaryNotFoundDefect]) + def test_line_beginning_colon(self): + msg = self.check_defect(errors.InvalidHeaderDefect, + 'Subject: Dummy subject\r\n' + ': faulty header line\r\n' + '\r\n' + 'body\r\n' + ) + if msg: + self.assertEqual(msg.items(), [('Subject', 'Dummy subject')]) + self.assertEqual(msg.get_payload(), 'body\r\n') + + def test_misplaced_envelope(self): + msg = self.check_defect(errors.MisplacedEnvelopeHeaderDefect, + 'Subject: Dummy subject\r\n' + 'From wtf\r\n' + 'To: abc\r\n' + '\r\n' + 'body\r\n' + ) + if msg: + headers = [('Subject', 'Dummy subject'), ('To', 'abc')] + self.assertEqual(msg.items(), headers) + self.assertEqual(msg.get_payload(), 'body\r\n') + + +class TestCompat32(TestDefectsBase, TestEmailBase): + + policy = policy.compat32 + class TestDefectDetection(TestDefectsBase, TestEmailBase): + pass def get_defects(self, obj): return obj.defects diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index b8116d073a2670..1c6ab9b928b352 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -2261,50 +2261,6 @@ def test_multipart_no_boundary(self): self.assertIsInstance(msg.defects[1], errors.MultipartInvariantViolationDefect) - multipart_msg = textwrap.dedent("""\ - Date: Wed, 14 Nov 2007 12:56:23 GMT - From: foo@bar.invalid - To: foo@bar.invalid - Subject: Content-Transfer-Encoding: base64 and multipart - MIME-Version: 1.0 - Content-Type: multipart/mixed; - boundary="===============3344438784458119861=="{} - - --===============3344438784458119861== - Content-Type: text/plain - - Test message - - --===============3344438784458119861== - Content-Type: application/octet-stream - Content-Transfer-Encoding: base64 - - YWJj - - --===============3344438784458119861==-- - """) - - # test_defect_handling - def test_multipart_invalid_cte(self): - msg = self._str_msg( - self.multipart_msg.format("\nContent-Transfer-Encoding: base64")) - self.assertEqual(len(msg.defects), 1) - self.assertIsInstance(msg.defects[0], - errors.InvalidMultipartContentTransferEncodingDefect) - - # test_defect_handling - def test_multipart_no_cte_no_defect(self): - msg = self._str_msg(self.multipart_msg.format('')) - self.assertEqual(len(msg.defects), 0) - - # test_defect_handling - def test_multipart_valid_cte_no_defect(self): - for cte in ('7bit', '8bit', 'BINary'): - msg = self._str_msg( - self.multipart_msg.format( - "\nContent-Transfer-Encoding: {}".format(cte))) - self.assertEqual(len(msg.defects), 0) - # test_headerregistry.TestContentTypeHeader invalid_1 and invalid_2. def test_invalid_content_type(self): eq = self.assertEqual @@ -2381,30 +2337,6 @@ def test_missing_start_boundary(self): self.assertIsInstance(bad.defects[0], errors.StartBoundaryNotFoundDefect) - # test_defect_handling - def test_first_line_is_continuation_header(self): - eq = self.assertEqual - m = ' Line 1\nSubject: test\n\nbody' - msg = email.message_from_string(m) - eq(msg.keys(), ['Subject']) - eq(msg.get_payload(), 'body') - eq(len(msg.defects), 1) - self.assertDefectsEqual(msg.defects, - [errors.FirstHeaderLineIsContinuationDefect]) - eq(msg.defects[0].line, ' Line 1\n') - - # test_defect_handling - def test_missing_header_body_separator(self): - # Our heuristic if we see a line that doesn't look like a header (no - # leading whitespace but no ':') is to assume that the blank line that - # separates the header from the body is missing, and to stop parsing - # headers and start parsing the body. - msg = self._str_msg('Subject: test\nnot a header\nTo: abc\n\nb\n') - self.assertEqual(msg.keys(), ['Subject']) - self.assertEqual(msg.get_payload(), 'not a header\nTo: abc\n\nb\n') - self.assertDefectsEqual(msg.defects, - [errors.MissingHeaderBodySeparatorDefect]) - def test_string_payload_with_extra_space_after_cte(self): # https://github.com/python/cpython/issues/98188 cte = "base64 " From 6a42111811926f6f7d18dad52839f5e9d9eb5974 Mon Sep 17 00:00:00 2001 From: Ivo Bellin Salarin Date: Sun, 30 Nov 2025 09:03:01 +0100 Subject: [PATCH 2/4] fix: mirror the existing test pattern --- Lib/test/test_email/test_defect_handling.py | 63 +++++++++++---------- Lib/test/test_email/test_email.py | 20 ------- 2 files changed, 33 insertions(+), 50 deletions(-) diff --git a/Lib/test/test_email/test_defect_handling.py b/Lib/test/test_email/test_defect_handling.py index 94d103347e6a5e..acc4accccac756 100644 --- a/Lib/test/test_email/test_defect_handling.py +++ b/Lib/test/test_email/test_defect_handling.py @@ -15,17 +15,6 @@ class TestDefectsBase: def _raise_point(self, defect): yield - def get_defects(self, obj): - return obj.defects - - def check_defect(self, defect, string): - msg = None - with self._raise_point(defect): - msg = self._str_msg(string) - self.assertEqual(len(self.get_defects(msg)), 1) - self.assertDefectsEqual(self.get_defects(msg), [defect]) - return msg - def test_same_boundary_inner_outer(self): source = textwrap.dedent("""\ Subject: XX @@ -310,37 +299,48 @@ def test_missing_ending_boundary(self): [errors.CloseBoundaryNotFoundDefect]) def test_line_beginning_colon(self): - msg = self.check_defect(errors.InvalidHeaderDefect, - 'Subject: Dummy subject\r\n' - ': faulty header line\r\n' - '\r\n' - 'body\r\n' + string = ( + "Subject: Dummy subject\r\n: faulty header line\r\n\r\nbody\r\n" ) - if msg: - self.assertEqual(msg.items(), [('Subject', 'Dummy subject')]) - self.assertEqual(msg.get_payload(), 'body\r\n') + + with self._raise_point(errors.InvalidHeaderDefect): + msg = self._str_msg(string) + self.assertEqual(len(self.get_defects(msg)), 1) + self.assertDefectsEqual( + self.get_defects(msg), [errors.InvalidHeaderDefect] + ) + + if msg: + self.assertEqual(msg.items(), [("Subject", "Dummy subject")]) + self.assertEqual(msg.get_payload(), "body\r\n") def test_misplaced_envelope(self): - msg = self.check_defect(errors.MisplacedEnvelopeHeaderDefect, - 'Subject: Dummy subject\r\n' - 'From wtf\r\n' - 'To: abc\r\n' - '\r\n' - 'body\r\n' + string = ( + "Subject: Dummy subject\r\nFrom wtf\r\nTo: abc\r\n\r\nbody\r\n" ) - if msg: - headers = [('Subject', 'Dummy subject'), ('To', 'abc')] - self.assertEqual(msg.items(), headers) - self.assertEqual(msg.get_payload(), 'body\r\n') + with self._raise_point(errors.MisplacedEnvelopeHeaderDefect): + msg = self._str_msg(string) + self.assertEqual(len(self.get_defects(msg)), 1) + self.assertDefectsEqual( + self.get_defects(msg), [errors.MisplacedEnvelopeHeaderDefect] + ) + + if msg: + headers = [("Subject", "Dummy subject"), ("To", "abc")] + self.assertEqual(msg.items(), headers) + self.assertEqual(msg.get_payload(), "body\r\n") + class TestCompat32(TestDefectsBase, TestEmailBase): policy = policy.compat32 + def get_defects(self, obj): + return obj.defects + class TestDefectDetection(TestDefectsBase, TestEmailBase): - pass def get_defects(self, obj): return obj.defects @@ -371,6 +371,9 @@ def _raise_point(self, defect): with self.assertRaises(defect): yield + def get_defects(self, obj): + return obj.defects + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index 1c6ab9b928b352..5e7d01819038ca 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -2241,26 +2241,6 @@ def test_parse_missing_minor_type(self): eq(msg.get_content_maintype(), 'text') eq(msg.get_content_subtype(), 'plain') - # test_defect_handling - def test_same_boundary_inner_outer(self): - msg = self._msgobj('msg_15.txt') - # XXX We can probably eventually do better - inner = msg.get_payload(0) - self.assertHasAttr(inner, 'defects') - self.assertEqual(len(inner.defects), 1) - self.assertIsInstance(inner.defects[0], - errors.StartBoundaryNotFoundDefect) - - # test_defect_handling - def test_multipart_no_boundary(self): - msg = self._msgobj('msg_25.txt') - self.assertIsInstance(msg.get_payload(), str) - self.assertEqual(len(msg.defects), 2) - self.assertIsInstance(msg.defects[0], - errors.NoBoundaryInMultipartDefect) - self.assertIsInstance(msg.defects[1], - errors.MultipartInvariantViolationDefect) - # test_headerregistry.TestContentTypeHeader invalid_1 and invalid_2. def test_invalid_content_type(self): eq = self.assertEqual From 174c334295e4391a0da632e40931cde23541033b Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 09:22:33 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-12-04-09-22-31.gh-issue-68552.I_v-xB.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-12-04-09-22-31.gh-issue-68552.I_v-xB.rst diff --git a/Misc/NEWS.d/next/Library/2025-12-04-09-22-31.gh-issue-68552.I_v-xB.rst b/Misc/NEWS.d/next/Library/2025-12-04-09-22-31.gh-issue-68552.I_v-xB.rst new file mode 100644 index 00000000000000..e78d1378237cb3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-04-09-22-31.gh-issue-68552.I_v-xB.rst @@ -0,0 +1 @@ +``MisplacedEnvelopeHeaderDefect`` and ``Missing header name`` defects are now correctly passed to the ``handle_defect`` method of ``policy`` in :mod:`email.FeedParser`. From b76ec9b2567a798ac315c18c42a01903c427c038 Mon Sep 17 00:00:00 2001 From: Ivo Bellin Salarin Date: Thu, 4 Dec 2025 10:27:02 +0100 Subject: [PATCH 4/4] fix(news): reference --- .../next/Library/2025-12-04-09-22-31.gh-issue-68552.I_v-xB.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-12-04-09-22-31.gh-issue-68552.I_v-xB.rst b/Misc/NEWS.d/next/Library/2025-12-04-09-22-31.gh-issue-68552.I_v-xB.rst index e78d1378237cb3..bd3e53c9f8193f 100644 --- a/Misc/NEWS.d/next/Library/2025-12-04-09-22-31.gh-issue-68552.I_v-xB.rst +++ b/Misc/NEWS.d/next/Library/2025-12-04-09-22-31.gh-issue-68552.I_v-xB.rst @@ -1 +1 @@ -``MisplacedEnvelopeHeaderDefect`` and ``Missing header name`` defects are now correctly passed to the ``handle_defect`` method of ``policy`` in :mod:`email.FeedParser`. +``MisplacedEnvelopeHeaderDefect`` and ``Missing header name`` defects are now correctly passed to the ``handle_defect`` method of ``policy`` in :class:`~email.parser.FeedParser`.