mirror of
https://github.com/django/django.git
synced 2025-01-03 06:55:47 +00:00
Fixed #25986 -- Fixed crash sending email with non-ASCII in local part of the address.
On Python 3, sending emails failed for addresses containing non-ASCII characters due to the usage of the legacy Python email.utils.formataddr() function. This is fixed by using the proper Address object on Python 3.
This commit is contained in:
parent
086510fde0
commit
ec009ef1d8
1
AUTHORS
1
AUTHORS
@ -660,6 +660,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
Sengtha Chay <sengtha@e-khmer.com>
|
Sengtha Chay <sengtha@e-khmer.com>
|
||||||
Senko Rašić <senko.rasic@dobarkod.hr>
|
Senko Rašić <senko.rasic@dobarkod.hr>
|
||||||
serbaut@gmail.com
|
serbaut@gmail.com
|
||||||
|
Sergei Maertens <sergeimaertens@gmail.com>
|
||||||
Sergey Fedoseev <fedoseev.sergey@gmail.com>
|
Sergey Fedoseev <fedoseev.sergey@gmail.com>
|
||||||
Sergey Kolosov <m17.admin@gmail.com>
|
Sergey Kolosov <m17.admin@gmail.com>
|
||||||
Seth Hill <sethrh@gmail.com>
|
Seth Hill <sethrh@gmail.com>
|
||||||
|
@ -115,9 +115,9 @@ class EmailBackend(BaseEmailBackend):
|
|||||||
"""A helper method that does the actual sending."""
|
"""A helper method that does the actual sending."""
|
||||||
if not email_message.recipients():
|
if not email_message.recipients():
|
||||||
return False
|
return False
|
||||||
from_email = sanitize_address(email_message.from_email, email_message.encoding)
|
encoding = email_message.encoding or settings.DEFAULT_CHARSET
|
||||||
recipients = [sanitize_address(addr, email_message.encoding)
|
from_email = sanitize_address(email_message.from_email, encoding)
|
||||||
for addr in email_message.recipients()]
|
recipients = [sanitize_address(addr, encoding) for addr in email_message.recipients()]
|
||||||
message = email_message.message()
|
message = email_message.message()
|
||||||
try:
|
try:
|
||||||
self.connection.sendmail(from_email, recipients, message.as_bytes(linesep='\r\n'))
|
self.connection.sendmail(from_email, recipients, message.as_bytes(linesep='\r\n'))
|
||||||
|
@ -13,7 +13,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 formataddr, formatdate, getaddresses, parseaddr
|
from email.utils import formatdate, getaddresses, parseaddr
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -103,23 +103,67 @@ def forbid_multi_line_headers(name, val, encoding):
|
|||||||
return str(name), val
|
return str(name), val
|
||||||
|
|
||||||
|
|
||||||
|
def split_addr(addr, encoding):
|
||||||
|
"""
|
||||||
|
Split the address into local part and domain, properly encoded.
|
||||||
|
|
||||||
|
When non-ascii characters are present in the local part, it must be
|
||||||
|
MIME-word encoded. The domain name must be idna-encoded if it contains
|
||||||
|
non-ascii characters.
|
||||||
|
"""
|
||||||
|
if '@' in addr:
|
||||||
|
localpart, domain = addr.split('@', 1)
|
||||||
|
# Try to get the simplest encoding - ascii if possible so that
|
||||||
|
# to@example.com doesn't become =?utf-8?q?to?=@example.com. This
|
||||||
|
# makes unit testing a bit easier and more readable.
|
||||||
|
try:
|
||||||
|
localpart.encode('ascii')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
localpart = Header(localpart, encoding).encode()
|
||||||
|
domain = domain.encode('idna').decode('ascii')
|
||||||
|
else:
|
||||||
|
localpart = Header(addr, encoding).encode()
|
||||||
|
domain = ''
|
||||||
|
return (localpart, domain)
|
||||||
|
|
||||||
|
|
||||||
def sanitize_address(addr, encoding):
|
def sanitize_address(addr, encoding):
|
||||||
|
"""
|
||||||
|
Format a pair of (name, address) or an email address string.
|
||||||
|
"""
|
||||||
if not isinstance(addr, tuple):
|
if not isinstance(addr, tuple):
|
||||||
addr = parseaddr(force_text(addr))
|
addr = parseaddr(force_text(addr))
|
||||||
nm, addr = addr
|
nm, addr = addr
|
||||||
|
localpart, domain = None, None
|
||||||
nm = Header(nm, encoding).encode()
|
nm = Header(nm, encoding).encode()
|
||||||
try:
|
try:
|
||||||
addr.encode('ascii')
|
addr.encode('ascii')
|
||||||
except UnicodeEncodeError: # IDN
|
except UnicodeEncodeError: # IDN or non-ascii in the local part
|
||||||
if '@' in addr:
|
localpart, domain = split_addr(addr, encoding)
|
||||||
localpart, domain = addr.split('@', 1)
|
|
||||||
localpart = str(Header(localpart, encoding))
|
if six.PY2:
|
||||||
domain = domain.encode('idna').decode('ascii')
|
# On Python 2, use the stdlib since `email.headerregistry` doesn't exist.
|
||||||
|
from email.utils import formataddr
|
||||||
|
if localpart and domain:
|
||||||
addr = '@'.join([localpart, domain])
|
addr = '@'.join([localpart, domain])
|
||||||
else:
|
|
||||||
addr = Header(addr, encoding).encode()
|
|
||||||
return formataddr((nm, addr))
|
return formataddr((nm, addr))
|
||||||
|
|
||||||
|
# On Python 3, an `email.headerregistry.Address` object is used since
|
||||||
|
# email.utils.formataddr() naively encodes the name as ascii (see #25986).
|
||||||
|
from email.headerregistry import Address
|
||||||
|
from email.errors import InvalidHeaderDefect, NonASCIILocalPartDefect
|
||||||
|
|
||||||
|
if localpart and domain:
|
||||||
|
address = Address(nm, username=localpart, domain=domain)
|
||||||
|
return str(address)
|
||||||
|
|
||||||
|
try:
|
||||||
|
address = Address(nm, addr_spec=addr)
|
||||||
|
except (InvalidHeaderDefect, NonASCIILocalPartDefect):
|
||||||
|
localpart, domain = split_addr(addr, encoding)
|
||||||
|
address = Address(nm, username=localpart, domain=domain)
|
||||||
|
return str(address)
|
||||||
|
|
||||||
|
|
||||||
class MIMEMixin():
|
class MIMEMixin():
|
||||||
def as_string(self, unixfrom=False, linesep='\n'):
|
def as_string(self, unixfrom=False, linesep='\n'):
|
||||||
|
@ -9,6 +9,7 @@ import smtpd
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
|
from email.header import Header
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from smtplib import SMTP, SMTPException
|
from smtplib import SMTP, SMTPException
|
||||||
from ssl import SSLError
|
from ssl import SSLError
|
||||||
@ -19,7 +20,7 @@ from django.core.mail import (
|
|||||||
send_mail, send_mass_mail,
|
send_mail, send_mass_mail,
|
||||||
)
|
)
|
||||||
from django.core.mail.backends import console, dummy, filebased, locmem, smtp
|
from django.core.mail.backends import console, dummy, filebased, locmem, smtp
|
||||||
from django.core.mail.message import BadHeaderError
|
from django.core.mail.message import BadHeaderError, sanitize_address
|
||||||
from django.test import SimpleTestCase, override_settings
|
from django.test import SimpleTestCase, override_settings
|
||||||
from django.utils._os import upath
|
from django.utils._os import upath
|
||||||
from django.utils.encoding import force_bytes, force_text
|
from django.utils.encoding import force_bytes, force_text
|
||||||
@ -567,6 +568,42 @@ class MailTests(HeadersCheckMixin, SimpleTestCase):
|
|||||||
# Verify that the child message header is not base64 encoded
|
# Verify that the child message header is not base64 encoded
|
||||||
self.assertIn(str('Child Subject'), parent_s)
|
self.assertIn(str('Child Subject'), parent_s)
|
||||||
|
|
||||||
|
def test_sanitize_address(self):
|
||||||
|
"""
|
||||||
|
Email addresses are properly sanitized.
|
||||||
|
"""
|
||||||
|
# Simple ASCII address - string form
|
||||||
|
self.assertEqual(sanitize_address('to@example.com', 'ascii'), 'to@example.com')
|
||||||
|
self.assertEqual(sanitize_address('to@example.com', 'utf-8'), 'to@example.com')
|
||||||
|
# Bytestrings are transformed to normal strings.
|
||||||
|
self.assertEqual(sanitize_address(b'to@example.com', 'utf-8'), 'to@example.com')
|
||||||
|
|
||||||
|
# Simple ASCII address - tuple form
|
||||||
|
self.assertEqual(
|
||||||
|
sanitize_address(('A name', 'to@example.com'), 'ascii'),
|
||||||
|
'A name <to@example.com>'
|
||||||
|
)
|
||||||
|
if PY3:
|
||||||
|
self.assertEqual(
|
||||||
|
sanitize_address(('A name', 'to@example.com'), 'utf-8'),
|
||||||
|
'=?utf-8?q?A_name?= <to@example.com>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertEqual(
|
||||||
|
sanitize_address(('A name', 'to@example.com'), 'utf-8'),
|
||||||
|
'A name <to@example.com>'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unicode characters are are supported in RFC-6532.
|
||||||
|
self.assertEqual(
|
||||||
|
sanitize_address('tó@example.com', 'utf-8'),
|
||||||
|
'=?utf-8?b?dMOz?=@example.com'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
sanitize_address(('Tó Example', 'tó@example.com'), 'utf-8'),
|
||||||
|
'=?utf-8?q?T=C3=B3_Example?= <=?utf-8?b?dMOz?=@example.com>'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PythonGlobalState(SimpleTestCase):
|
class PythonGlobalState(SimpleTestCase):
|
||||||
"""
|
"""
|
||||||
@ -1026,6 +1063,15 @@ class FakeSMTPServer(smtpd.SMTPServer, threading.Thread):
|
|||||||
data = data.encode('utf-8')
|
data = data.encode('utf-8')
|
||||||
m = message_from_bytes(data)
|
m = message_from_bytes(data)
|
||||||
maddr = parseaddr(m.get('from'))[1]
|
maddr = parseaddr(m.get('from'))[1]
|
||||||
|
|
||||||
|
if mailfrom != maddr:
|
||||||
|
# According to the spec, mailfrom does not necessarily match the
|
||||||
|
# From header - on Python 3 this is the case where the local part
|
||||||
|
# isn't encoded, so try to correct that.
|
||||||
|
lp, domain = mailfrom.split('@', 1)
|
||||||
|
lp = Header(lp, 'utf-8').encode()
|
||||||
|
mailfrom = '@'.join([lp, domain])
|
||||||
|
|
||||||
if mailfrom != maddr:
|
if mailfrom != maddr:
|
||||||
return "553 '%s' != '%s'" % (mailfrom, maddr)
|
return "553 '%s' != '%s'" % (mailfrom, maddr)
|
||||||
with self.sink_lock:
|
with self.sink_lock:
|
||||||
|
Loading…
Reference in New Issue
Block a user