diff --git a/Lib/email/utils.py b/Lib/email/utils.py index c8a6d0af37532c9..682efc18a3fb571 100644 --- a/Lib/email/utils.py +++ b/Lib/email/utils.py @@ -101,6 +101,10 @@ def formataddr(pair, charset='utf-8', *, strict=True): from email.charset import Charset charset = Charset(charset) encoded_name = charset.header_encode(name) + if specialsre.search(encoded_name): + # Not RFC 2047-encoded, so quote it like the ASCII branch + # below to keep specials from leaking into the header. + encoded_name = '"' + escapesre.sub(r'\\\g<0>', encoded_name) + '"' return "%s <%s>" % (encoded_name, address) else: quotes = '' diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index e40c82bba9af426..f3143c2859be874 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -3308,6 +3308,32 @@ def test_name_with_dot(self): # formataddr() quotes the name if there's a dot in it self.assertEqual(utils.formataddr((a, b)), y) + def test_formataddr_non_ascii_name_with_specials(self): + # gh-100900: when the charset is configured not to RFC 2047-encode the + # display name, formataddr() must still quote a non-ASCII name that + # contains specials, the same way its ASCII branch does, so the result + # round-trips through getaddresses() instead of splitting on the comma. + from email import charset as _charset + sentinel = object() + previous = _charset.CHARSETS.get('utf-8', sentinel) + def restore(): + if previous is sentinel: + _charset.CHARSETS.pop('utf-8', None) + else: + _charset.CHARSETS['utf-8'] = previous + self.addCleanup(restore) + _charset.add_charset('utf-8', None) # do not RFC 2047-encode the name + formatted = utils.formataddr(('Fôo, Bar', 'a@b.com')) + self.assertEqual(formatted, '"Fôo, Bar" ') + self.assertEqual(utils.getaddresses([formatted]), + [('Fôo, Bar', 'a@b.com')]) + # A non-ASCII name without specials is still emitted unquoted. + self.assertEqual(utils.formataddr(('Fôo Bar', 'a@b.com')), + 'Fôo Bar ') + # The ASCII branch is unchanged. + self.assertEqual(utils.formataddr(('Foo, Bar', 'a@b.com')), + '"Foo, Bar" ') + def test_parseaddr_preserves_quoted_pairs_in_addresses(self): # issue 10005. Note that in the third test the second pair of # backslashes is not actually a quoted pair because it is not inside a diff --git a/Misc/NEWS.d/next/Library/2026-08-16-02-24-32.gh-issue-100900.y7FWJM.rst b/Misc/NEWS.d/next/Library/2026-08-16-02-24-32.gh-issue-100900.y7FWJM.rst new file mode 100644 index 000000000000000..d18f1b8d844a898 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-16-02-24-32.gh-issue-100900.y7FWJM.rst @@ -0,0 +1,4 @@ +Fix :func:`email.utils.formataddr` failing to quote a non-ASCII display name +that contains special characters when the charset is configured not to +RFC 2047-encode it, which broke round-tripping through +:func:`~email.utils.getaddresses`. Patch by Nikolaus Schuetz.