Skip to content

Commit 67562c4

Browse files
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.
1 parent 94a75f7 commit 67562c4

3 files changed

Lines changed: 34 additions & 0 deletions

File tree

Lib/email/utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ def formataddr(pair, charset='utf-8', *, strict=True):
101101
from email.charset import Charset
102102
charset = Charset(charset)
103103
encoded_name = charset.header_encode(name)
104+
if specialsre.search(encoded_name):
105+
# Not RFC 2047-encoded, so quote it like the ASCII branch
106+
# below to keep specials from leaking into the header.
107+
encoded_name = '"' + escapesre.sub(r'\\\g<0>', encoded_name) + '"'
104108
return "%s <%s>" % (encoded_name, address)
105109
else:
106110
quotes = ''

Lib/test/test_email/test_email.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3308,6 +3308,32 @@ def test_name_with_dot(self):
33083308
# formataddr() quotes the name if there's a dot in it
33093309
self.assertEqual(utils.formataddr((a, b)), y)
33103310

3311+
def test_formataddr_non_ascii_name_with_specials(self):
3312+
# gh-100900: when the charset is configured not to RFC 2047-encode the
3313+
# display name, formataddr() must still quote a non-ASCII name that
3314+
# contains specials, the same way its ASCII branch does, so the result
3315+
# round-trips through getaddresses() instead of splitting on the comma.
3316+
from email import charset as _charset
3317+
sentinel = object()
3318+
previous = _charset.CHARSETS.get('utf-8', sentinel)
3319+
def restore():
3320+
if previous is sentinel:
3321+
_charset.CHARSETS.pop('utf-8', None)
3322+
else:
3323+
_charset.CHARSETS['utf-8'] = previous
3324+
self.addCleanup(restore)
3325+
_charset.add_charset('utf-8', None) # do not RFC 2047-encode the name
3326+
formatted = utils.formataddr(('Fôo, Bar', 'a@b.com'))
3327+
self.assertEqual(formatted, '"Fôo, Bar" <a@b.com>')
3328+
self.assertEqual(utils.getaddresses([formatted]),
3329+
[('Fôo, Bar', 'a@b.com')])
3330+
# A non-ASCII name without specials is still emitted unquoted.
3331+
self.assertEqual(utils.formataddr(('Fôo Bar', 'a@b.com')),
3332+
'Fôo Bar <a@b.com>')
3333+
# The ASCII branch is unchanged.
3334+
self.assertEqual(utils.formataddr(('Foo, Bar', 'a@b.com')),
3335+
'"Foo, Bar" <a@b.com>')
3336+
33113337
def test_parseaddr_preserves_quoted_pairs_in_addresses(self):
33123338
# issue 10005. Note that in the third test the second pair of
33133339
# backslashes is not actually a quoted pair because it is not inside a
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :func:`email.utils.formataddr` failing to quote a non-ASCII display name
2+
that contains special characters when the charset is configured not to
3+
RFC 2047-encode it, which broke round-tripping through
4+
:func:`~email.utils.getaddresses`. Patch by Nikolaus Schuetz.

0 commit comments

Comments
 (0)