diff --git a/Lib/email/policy.py b/Lib/email/policy.py index d09d2f5238d40b..1fb3d57fdb8394 100644 --- a/Lib/email/policy.py +++ b/Lib/email/policy.py @@ -225,7 +225,11 @@ def _fold(self, name, value, refold_binary=False): elif refold_binary: refold = _has_surrogates(value) if refold: - return self.header_factory(name, ''.join(lines)).fold(policy=self) + value = ''.join(lines) + if self.utf8: + value = value.encode('utf-8', 'surrogateescape').decode( + 'utf-8', 'surrogateescape') + return self.header_factory(name, value).fold(policy=self) return name + ': ' + self.linesep.join(lines) + self.linesep diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py index 90e8e5580295f9..ea7436c0d8b6de 100644 --- a/Lib/test/test_email/test_policy.py +++ b/Lib/test/test_email/test_policy.py @@ -153,6 +153,44 @@ def test_fold_utf8(self): self.assertEqual(p_ascii.fold('Subject', s), expected_ascii) self.assertEqual(p_utf8.fold('Subject', s), expected_utf8) + def test_smtputf8_round_trip_preserves_utf8_headers(self): + odata = textwrap.dedent("""\ + From: Unicôde Nâme + To: Name One , Name Two , Unicôde Nàme + Subject: Unicôde Subjéct + + This message contains Uniçôde Çôntent. + """) + msg = email.message_from_bytes( + odata.encode(), policy=email.policy.SMTPUTF8 + ) + output = msg.as_bytes(policy=email.policy.SMTPUTF8).decode() + + self.assertNotIn("unknown-8bit", output) + self.assertIn( + "Unicôde Nàme ", output + ) + + def test_smtputf8_round_trip_preserves_invalid_header_bytes(self): + msg = email.message_from_bytes( + b"Subject: invalid \xff\n\n", + policy=email.policy.SMTPUTF8, + ) + output = msg.as_bytes(policy=email.policy.SMTPUTF8) + + self.assertIn(b"invalid \xff", output) + + def test_smtputf8_parse_then_compat32_serialize_preserves_addresses(self): + # gh-99927: parsing with SMTPUTF8 and serializing with compat32 must + # keep address headers structured - regressing here encodes the + # whole header as one encoded word and destroys the addr-spec. + odata = (b"From: Sender \n" + + "To: J\xf6rg \n".encode("utf-8") + + b"Subject: test\n\nbody\n") + msg = email.message_from_bytes(odata, policy=email.policy.SMTPUTF8) + out = msg.as_bytes(policy=email.policy.compat32) + self.assertIn(b"", out) + def test_fold_zero_max_line_length(self): expected = 'Subject: =?utf-8?q?=C3=A1?=\n' diff --git a/Misc/NEWS.d/next/Library/2026-10-16-09-30-00.gh-issue-99927.kx2q.rst b/Misc/NEWS.d/next/Library/2026-10-16-09-30-00.gh-issue-99927.kx2q.rst new file mode 100644 index 00000000000000..dede5a8b55630e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-16-09-30-00.gh-issue-99927.kx2q.rst @@ -0,0 +1,3 @@ +Fix the :class:`email.policy.SMTPUTF8` policy so UTF-8 header values survive a +parse and regenerate round trip instead of being mangled or demoted to +``unknown-8bit`` bodies.