Skip to content

Commit

Permalink
[2.2.x] Fixed #31784 -- Fixed crash when sending emails on Python 3.6…
Browse files Browse the repository at this point in the history
….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>

Backport of 96a3ea3 from master.
  • Loading branch information
apollo13 authored and felixxm committed Jul 20, 2020
1 parent f1a6e6c commit 1a3835f
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 9 deletions.
29 changes: 22 additions & 7 deletions django/core/mail/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
from email.mime.message import MIMEMessage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate, getaddresses, make_msgid, parseaddr
from email.utils import (
formataddr, formatdate, getaddresses, make_msgid, parseaddr,
)
from io import BytesIO, StringIO
from pathlib import Path

Expand Down Expand Up @@ -103,7 +105,15 @@ def sanitize_address(addr, encoding):
addr = parseaddr(addr)
nm, addr = addr
localpart, domain = None, None
nm = Header(nm, encoding).encode()
if '\n' in nm or '\r' in nm:
raise ValueError('Invalid address; address parts cannot contain newlines.')

# Avoid UTF-8 encode, if it's possible.
try:
nm.encode('ascii')
nm = Header(nm).encode()
except UnicodeEncodeError:
nm = Header(nm, encoding).encode()
try:
addr.encode('ascii')
except UnicodeEncodeError: # IDN or non-ascii in the local part
Expand All @@ -112,15 +122,20 @@ def sanitize_address(addr, encoding):
# An `email.headerregistry.Address` object is used since
# email.utils.formataddr() naively encodes the name as ascii (see #25986).
if localpart and domain:
address = Address(nm, username=localpart, domain=domain)
return str(address)
address_parts = localpart + domain
if '\n' in address_parts or '\r' in address_parts:
raise ValueError('Invalid address; address parts cannot contain newlines.')
address = Address(username=localpart, domain=domain)
return formataddr((nm, address.addr_spec))

try:
address = Address(nm, addr_spec=addr)
if '\n' in addr or '\r' in addr:
raise ValueError('Invalid address; address parts cannot contain newlines.')
address = Address(addr_spec=addr)
except (InvalidHeaderDefect, NonASCIILocalPartDefect):
localpart, domain = split_addr(addr, encoding)
address = Address(nm, username=localpart, domain=domain)
return str(address)
address = Address(username=localpart, domain=domain)
return formataddr((nm, address.addr_spec))


class MIMEMixin:
Expand Down
5 changes: 4 additions & 1 deletion docs/releases/2.2.15.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ Django 2.2.15 release notes

*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
========

* Allowed setting the ``SameSite`` cookie flag in
: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`).
33 changes: 32 additions & 1 deletion tests/mail/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ def test_sanitize_address(self):
)
self.assertEqual(
sanitize_address(('A name', 'to@example.com'), 'utf-8'),
'=?utf-8?q?A_name?= <to@example.com>'
'A name <to@example.com>'
)

# Unicode characters are are supported in RFC-6532.
Expand All @@ -732,6 +732,37 @@ def test_sanitize_address(self):
sanitize_address(('Tó Example', 'tó@example.com'), 'utf-8'),
'=?utf-8?q?T=C3=B3_Example?= <=?utf-8?b?dMOz?=@example.com>'
)
# Addresses with long unicode display names.
self.assertEqual(
sanitize_address('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>'
)
self.assertEqual(
sanitize_address(('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.
self.assertEqual(
sanitize_address(('To Example very long' * 4, 'to@exampl€.com'), 'utf-8'),
'To Example very longTo Example very longTo Example very longTo Ex'
'ample very\n'
' long <to@xn--exampl-nc1c.com>'
)

def test_sanitize_address_header_injection(self):
msg = 'Invalid address; address parts cannot contain newlines.'
tests = [
('Name\nInjection', 'to@xample.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
Expand Down

0 comments on commit 1a3835f

Please sign in to comment.