Fixed #31784 -- Fixed crash when sending emails on Python 3.6.11+, 3.7.8+, and 3.8.4+.

Fixed sending emails crash on email addresses with display names longer
then 75 chars on Python 3.6.11+, 3.7.8+, and 3.8.4+.

Wrapped display names were passed to email.headerregistry.Address()
what caused raising an exception because address parts cannot contain
CR or LF.

See https://bugs.python.org/issue39073

Co-Authored-By: Mariusz Felisiak <felisiak.mariusz@gmail.com>
This commit is contained in:
Florian Apolloner 2020-07-15 07:30:15 +02:00 committed by Mariusz Felisiak
parent f405954ea2
commit 96a3ea39ef
4 changed files with 63 additions and 11 deletions

View File

@ -10,7 +10,7 @@ from email.mime.base import MIMEBase
from email.mime.message import MIMEMessage from email.mime.message import MIMEMessage
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.utils import formatdate, getaddresses, make_msgid from email.utils import formataddr, formatdate, getaddresses, make_msgid
from io import BytesIO, StringIO from io import BytesIO, StringIO
from pathlib import Path from pathlib import Path
@ -96,16 +96,24 @@ def sanitize_address(addr, encoding):
nm, address = addr nm, address = addr
localpart, domain = address.rsplit('@', 1) localpart, domain = address.rsplit('@', 1)
nm = Header(nm, encoding).encode() address_parts = nm + localpart + domain
if '\n' in address_parts or '\r' in address_parts:
raise ValueError('Invalid address; address parts cannot contain newlines.')
# Avoid UTF-8 encode, if it's possible. # Avoid UTF-8 encode, if it's possible.
try:
nm.encode('ascii')
nm = Header(nm).encode()
except UnicodeEncodeError:
nm = Header(nm, encoding).encode()
try: try:
localpart.encode('ascii') localpart.encode('ascii')
except UnicodeEncodeError: except UnicodeEncodeError:
localpart = Header(localpart, encoding).encode() localpart = Header(localpart, encoding).encode()
domain = punycode(domain) domain = punycode(domain)
parsed_address = Address(nm, username=localpart, domain=domain) parsed_address = Address(username=localpart, domain=domain)
return str(parsed_address) return formataddr((nm, parsed_address.addr_spec))
class MIMEMixin: class MIMEMixin:

View File

@ -4,10 +4,13 @@ Django 2.2.15 release notes
*Expected August 3, 2020* *Expected August 3, 2020*
Django 2.2.15 fixes a bug in 2.2.14. Django 2.2.15 fixes two bugs in 2.2.14.
Bugfixes Bugfixes
======== ========
* Allowed setting the ``SameSite`` cookie flag in * Allowed setting the ``SameSite`` cookie flag in
:meth:`.HttpResponse.delete_cookie` (:ticket:`31790`). :meth:`.HttpResponse.delete_cookie` (:ticket:`31790`).
* Fixed crash when sending emails to addresses with display names longer than
75 chars on Python 3.6.11+, 3.7.8+, and 3.8.4+ (:ticket:`31784`).

View File

@ -11,3 +11,6 @@ Bugfixes
* Allowed setting the ``SameSite`` cookie flag in * Allowed setting the ``SameSite`` cookie flag in
:meth:`.HttpResponse.delete_cookie` (:ticket:`31790`). :meth:`.HttpResponse.delete_cookie` (:ticket:`31790`).
* Fixed crash when sending emails to addresses with display names longer than
75 chars on Python 3.6.11+, 3.7.8+, and 3.8.4+ (:ticket:`31784`).

View File

@ -738,14 +738,14 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
( (
('A name', 'to@example.com'), ('A name', 'to@example.com'),
'utf-8', 'utf-8',
'=?utf-8?q?A_name?= <to@example.com>', 'A name <to@example.com>',
), ),
('localpartonly', 'ascii', 'localpartonly'), ('localpartonly', 'ascii', 'localpartonly'),
# ASCII addresses with display names. # ASCII addresses with display names.
('A name <to@example.com>', 'ascii', 'A name <to@example.com>'), ('A name <to@example.com>', 'ascii', 'A name <to@example.com>'),
('A name <to@example.com>', 'utf-8', '=?utf-8?q?A_name?= <to@example.com>'), ('A name <to@example.com>', 'utf-8', 'A name <to@example.com>'),
('"A name" <to@example.com>', 'ascii', 'A name <to@example.com>'), ('"A name" <to@example.com>', 'ascii', 'A name <to@example.com>'),
('"A name" <to@example.com>', 'utf-8', '=?utf-8?q?A_name?= <to@example.com>'), ('"A name" <to@example.com>', 'utf-8', 'A name <to@example.com>'),
# Unicode addresses (supported per RFC-6532). # Unicode addresses (supported per RFC-6532).
('tó@example.com', 'utf-8', '=?utf-8?b?dMOz?=@example.com'), ('tó@example.com', 'utf-8', '=?utf-8?b?dMOz?=@example.com'),
('to@éxample.com', 'utf-8', 'to@xn--xample-9ua.com'), ('to@éxample.com', 'utf-8', 'to@xn--xample-9ua.com'),
@ -764,20 +764,45 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
( (
'To Example <to@éxample.com>', 'To Example <to@éxample.com>',
'utf-8', 'utf-8',
'=?utf-8?q?To_Example?= <to@xn--xample-9ua.com>', 'To Example <to@xn--xample-9ua.com>',
), ),
# Addresses with two @ signs. # Addresses with two @ signs.
('"to@other.com"@example.com', 'utf-8', r'"to@other.com"@example.com'), ('"to@other.com"@example.com', 'utf-8', r'"to@other.com"@example.com'),
( (
'"to@other.com" <to@example.com>', '"to@other.com" <to@example.com>',
'utf-8', 'utf-8',
'=?utf-8?q?to=40other=2Ecom?= <to@example.com>', '"to@other.com" <to@example.com>',
), ),
( (
('To Example', 'to@other.com@example.com'), ('To Example', 'to@other.com@example.com'),
'utf-8', 'utf-8',
'=?utf-8?q?To_Example?= <"to@other.com"@example.com>', 'To Example <"to@other.com"@example.com>',
), ),
# Addresses with long unicode display names.
(
'Tó Example very long' * 4 + ' <to@example.com>',
'utf-8',
'=?utf-8?q?T=C3=B3_Example_very_longT=C3=B3_Example_very_longT'
'=C3=B3_Example_?=\n'
' =?utf-8?q?very_longT=C3=B3_Example_very_long?= '
'<to@example.com>',
),
(
('Tó Example very long' * 4, 'to@example.com'),
'utf-8',
'=?utf-8?q?T=C3=B3_Example_very_longT=C3=B3_Example_very_longT'
'=C3=B3_Example_?=\n'
' =?utf-8?q?very_longT=C3=B3_Example_very_long?= '
'<to@example.com>',
),
# Address with long display name and unicode domain.
(
('To Example very long' * 4, 'to@exampl€.com'),
'utf-8',
'To Example very longTo Example very longTo Example very longT'
'o Example very\n'
' long <to@xn--exampl-nc1c.com>'
)
): ):
with self.subTest(email_address=email_address, encoding=encoding): with self.subTest(email_address=email_address, encoding=encoding):
self.assertEqual(sanitize_address(email_address, encoding), expected_result) self.assertEqual(sanitize_address(email_address, encoding), expected_result)
@ -797,6 +822,19 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
sanitize_address(email_address, encoding='utf-8') sanitize_address(email_address, encoding='utf-8')
def test_sanitize_address_header_injection(self):
msg = 'Invalid address; address parts cannot contain newlines.'
tests = [
'Name\nInjection <to@example.com>',
('Name\nInjection', 'to@xample.com'),
'Name <to\ninjection@example.com>',
('Name', 'to\ninjection@example.com'),
]
for email_address in tests:
with self.subTest(email_address=email_address):
with self.assertRaisesMessage(ValueError, msg):
sanitize_address(email_address, encoding='utf-8')
@requires_tz_support @requires_tz_support
class MailTimeZoneTests(SimpleTestCase): class MailTimeZoneTests(SimpleTestCase):