diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py index f396f4f9b32a9a..f6ff3264c2cb0a 100644 --- a/Lib/email/_header_value_parser.py +++ b/Lib/email/_header_value_parser.py @@ -152,7 +152,10 @@ def value(self): @property def all_defects(self): - return sum((x.all_defects for x in self), self.defects) + if not self: + return self.defects + return self.defects + [ + defect for token in self for defect in token.all_defects] def startswith_fws(self): return self[0].startswith_fws() diff --git a/Lib/email/headerregistry.py b/Lib/email/headerregistry.py index 48cd85a65ba9f6..329a0cb3c45f83 100644 --- a/Lib/email/headerregistry.py +++ b/Lib/email/headerregistry.py @@ -264,6 +264,7 @@ class UnstructuredHeader: def parse(cls, value, kwds): kwds['parse_tree'] = cls.value_parser(value) kwds['decoded'] = str(kwds['parse_tree']) + kwds['defects'].extend(kwds['parse_tree'].all_defects) class UniqueUnstructuredHeader(UnstructuredHeader): diff --git a/Lib/test/test_email/test__header_value_parser.py b/Lib/test/test_email/test__header_value_parser.py index 26e9249ba9e948..0ee1408e91a657 100644 --- a/Lib/test/test_email/test__header_value_parser.py +++ b/Lib/test/test_email/test__header_value_parser.py @@ -7,6 +7,23 @@ class TestTokens(TestEmailBase): + # TokenList + + def test_all_defects_empty(self): + token = parser.TokenList() + self.assertIs(token.all_defects, token.defects) + token.defects.append(errors.InvalidHeaderDefect()) + self.assertIs(token.all_defects, token.defects) + + def test_all_defects(self): + leaf = parser.ValueTerminal('text\x00', 'utext') + leaf.defects.append(errors.NonPrintableDefect('\x00')) + token = parser.TokenList([parser.TokenList([leaf]), leaf]) + token.defects.append(errors.InvalidHeaderDefect()) + self.assertEqual(token.all_defects, token.defects + leaf.defects * 2) + self.assertEqual(len(token.defects), 1) + self.assertEqual(len(leaf.defects), 1) + # EWWhiteSpaceTerminal def test_EWWhiteSpaceTerminal(self): diff --git a/Lib/test/test_email/test_headerregistry.py b/Lib/test/test_email/test_headerregistry.py index c118fd5e20d048..b0808b576535ee 100644 --- a/Lib/test/test_email/test_headerregistry.py +++ b/Lib/test/test_email/test_headerregistry.py @@ -129,21 +129,67 @@ def test_defects_is_tuple(self): @parameterize class TestUnstructuredHeader(TestHeaderBase): - def string_as_value(self, - source, - decoded, - *args): - l = len(args) - defects = args[0] if l>0 else [] - header = 'Subject:' + (' ' if source else '') - folded = header + (args[1] if l>1 else source) + '\n' - h = self.make_header('Subject', source) - self.assertEqual(h, decoded) - self.assertDefectsEqual(h.defects, defects) - self.assertEqual(h.fold(policy=policy.default), folded) + def string_as_value(self, source, decoded, defects=(), folded=None): + folded = source if folded is None else folded + for name in ('Subject', 'X-Test'): + with self.subTest(name=name): + h = self.make_header(name, source) + self.assertEqual(h, decoded) + self.assertDefectsEqual(h.defects, defects) + prefix = name + ':' + (' ' if source else '') + self.assertEqual(h.fold(policy=policy.default), + prefix + folded + '\n') string_params = { + 'empty': ('', ''), + + 'valid_encoded_word': ( + '=?utf-8?q?caf=C3=A9?=', + 'café', + []), + + 'invalid_base64_padding': ( + '=?utf-8?b?YQ?=', + 'a', + [errors.InvalidBase64PaddingDefect], + 'a'), + + 'invalid_base64_characters': ( + '=?utf-8?b?Y!Q==?=', + 'a', + [errors.InvalidBase64CharactersDefect], + 'a'), + + 'undecodable_bytes': ( + '=?utf-8?q?=FF?=', + '\ufffd', + [errors.UndecodableBytesDefect] * 2, + '=?unknown-8bit?q?=FF?='), + + 'unknown_charset': ( + '=?invalid-charset?q?word?=', + 'word', + [errors.CharsetError], + 'word'), + + 'non_printable_text': ( + 'text\x00value', + 'text\x00value', + [errors.NonPrintableDefect]), + + 'combined_defects': ( + 'prefix=?utf-8?b?YQ?=', + 'prefixa', + [errors.InvalidHeaderDefect, errors.InvalidBase64PaddingDefect], + 'prefixa'), + + 'missing_whitespace': ( + 'prefix=?utf-8?q?word?=', + 'prefixword', + [errors.InvalidHeaderDefect], + 'prefixword'), + 'rfc2047_simple_quopri': ( '=?utf-8?q?this_is_a_test?=', 'this is a test', diff --git a/Misc/NEWS.d/next/Library/2026-10-28-18-44-59.gh-issue-158780.J9qR4s.rst b/Misc/NEWS.d/next/Library/2026-10-28-18-44-59.gh-issue-158780.J9qR4s.rst new file mode 100644 index 00000000000000..db8fed83a3a3d1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-28-18-44-59.gh-issue-158780.J9qR4s.rst @@ -0,0 +1,2 @@ +Fix :class:`email.headerregistry.UnstructuredHeader` to include detected +encoded-word and text defects in its public ``defects`` attribute.