From 67562c4e253152a241810e31492379850a0d9ce7 Mon Sep 17 00:00:00 2001 From: Nikolaus Schuetz Date: Sat, 15 Aug 2026 23:42:09 -0400 Subject: [PATCH] gh-100900: Quote non-ASCII display names containing specials in formataddr formataddr() quotes an ASCII display name that contains specials, but the non-ASCII branch returned charset.header_encode(name) unquoted. When the charset is configured not to RFC 2047-encode the name (e.g. after add_charset('utf-8', None)), header_encode is a no-op, so specials such as a comma were emitted bare and the header no longer round-tripped through getaddresses(). Quote such a name the same way the ASCII branch does. --- Lib/email/utils.py | 4 +++ Lib/test/test_email/test_email.py | 26 +++++++++++++++++++ ...-08-16-02-24-32.gh-issue-100900.y7FWJM.rst | 4 +++ 3 files changed, 34 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-16-02-24-32.gh-issue-100900.y7FWJM.rst diff --git a/Lib/email/utils.py b/Lib/email/utils.py index c8a6d0af37532c..682efc18a3fb57 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 e40c82bba9af42..f3143c2859be87 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 00000000000000..d18f1b8d844a89 --- /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.