1
0
mirror of https://github.com/django/django.git synced 2025-01-03 06:55:47 +00:00

Refs #35581 -- Used modern email parser and helpers in mail tests.

- Used modern email API (policy.default) for tests that reparse
  generated messages, and switched to modern accessors where helpful.
- Split get_raw_attachments() helper out of get_decoded_attachments(),
  and used modern iter_attachments() to avoid finding nested attachments
  in attached message/* emails.
- Stopped using legacy parseaddr.
This commit is contained in:
Mike Edmunds 2024-08-24 13:41:40 -07:00 committed by Sarah Boyce
parent bddd35cb1a
commit 5d7001b578

View File

@ -4,10 +4,10 @@ import shutil
import socket import socket
import sys import sys
import tempfile import tempfile
from email import charset, message_from_binary_file, message_from_bytes from email import charset, message_from_binary_file
from email.header import Header from email import message_from_bytes as _message_from_bytes
from email import policy
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.utils import parseaddr
from io import StringIO from io import StringIO
from pathlib import Path from pathlib import Path
from smtplib import SMTP, SMTPException from smtplib import SMTP, SMTPException
@ -41,6 +41,14 @@ except ImportError:
HAS_AIOSMTPD = False HAS_AIOSMTPD = False
def message_from_bytes(s):
"""
email.message_from_bytes() using modern email.policy.default.
Returns a modern email.message.EmailMessage.
"""
return _message_from_bytes(s, policy=policy.default)
class MailTestsMixin: class MailTestsMixin:
def assertMessageHasHeaders(self, message, headers): def assertMessageHasHeaders(self, message, headers):
""" """
@ -101,24 +109,35 @@ class MailTestsMixin:
"First string doesn't end with the second.", "First string doesn't end with the second.",
) )
def get_decoded_attachments(self, django_message): def get_raw_attachments(self, django_message):
""" """
Encode the specified django.core.mail.message.EmailMessage, then decode Return a list of the raw attachment parts in the MIME message generated
it using Python's email.parser module and, for each attachment of the by serializing django_message and reparsing the result.
message, return a list of tuples with (filename, content, mimetype).
This returns only "top-level" attachments. It will not descend into
message/* attached emails to find nested attachments.
""" """
msg_bytes = django_message.message().as_bytes() msg_bytes = django_message.message().as_bytes()
email_message = message_from_bytes(msg_bytes) message = message_from_bytes(msg_bytes)
return list(message.iter_attachments())
def iter_attachments(): def get_decoded_attachments(self, django_message):
for i in email_message.walk(): """
if i.get_content_disposition() == "attachment": Return a list of decoded attachments resulting from serializing
filename = i.get_filename() django_message and reparsing the result.
content = i.get_payload(decode=True)
mimetype = i.get_content_type()
yield filename, content, mimetype
return list(iter_attachments()) Each attachment is returned as an EmailAttachment named tuple with
fields filename, content, and mimetype. The content will be decoded
to str for mimetype text/*; retained as bytes for other mimetypes.
"""
return [
EmailAttachment(
attachment.get_filename(),
attachment.get_content(),
attachment.get_content_type(),
)
for attachment in self.get_raw_attachments(django_message)
]
class MailTests(MailTestsMixin, SimpleTestCase): class MailTests(MailTestsMixin, SimpleTestCase):
@ -684,7 +703,7 @@ class MailTests(MailTestsMixin, SimpleTestCase):
self.assertEqual(msg.attachments[0].mimetype, mime_type) self.assertEqual(msg.attachments[0].mimetype, mime_type)
attachments = self.get_decoded_attachments(msg) attachments = self.get_decoded_attachments(msg)
self.assertEqual(attachments[0], (file_name, file_content.encode(), mime_type)) self.assertEqual(attachments[0], (file_name, file_content, mime_type))
def test_attachments_constructor(self): def test_attachments_constructor(self):
file_name = "example.txt" file_name = "example.txt"
@ -706,7 +725,7 @@ class MailTests(MailTestsMixin, SimpleTestCase):
self.assertEqual(msg.attachments[0].mimetype, mime_type) self.assertEqual(msg.attachments[0].mimetype, mime_type)
attachments = self.get_decoded_attachments(msg) attachments = self.get_decoded_attachments(msg)
self.assertEqual(attachments[0], (file_name, file_content.encode(), mime_type)) self.assertEqual(attachments[0], (file_name, file_content, mime_type))
def test_attachments_constructor_from_tuple(self): def test_attachments_constructor_from_tuple(self):
file_name = "example.txt" file_name = "example.txt"
@ -726,7 +745,7 @@ class MailTests(MailTestsMixin, SimpleTestCase):
self.assertEqual(msg.attachments[0].mimetype, mime_type) self.assertEqual(msg.attachments[0].mimetype, mime_type)
attachments = self.get_decoded_attachments(msg) attachments = self.get_decoded_attachments(msg)
self.assertEqual(attachments[0], (file_name, file_content.encode(), mime_type)) self.assertEqual(attachments[0], (file_name, file_content, mime_type))
def test_attachments_constructor_omit_mimetype(self): def test_attachments_constructor_omit_mimetype(self):
""" """
@ -767,10 +786,8 @@ class MailTests(MailTestsMixin, SimpleTestCase):
msg = EmailMessage(body="Content") msg = EmailMessage(body="Content")
# Unicode in file name # Unicode in file name
msg.attach("une pièce jointe.pdf", b"%PDF-1.4.%...", mimetype="application/pdf") msg.attach("une pièce jointe.pdf", b"%PDF-1.4.%...", mimetype="application/pdf")
msg_bytes = msg.message().as_bytes() attachment = self.get_decoded_attachments(msg)[0]
message = message_from_bytes(msg_bytes) self.assertEqual(attachment.filename, "une pièce jointe.pdf")
payload = message.get_payload()
self.assertEqual(payload[1].get_filename(), "une pièce jointe.pdf")
def test_attach_file(self): def test_attach_file(self):
""" """
@ -830,7 +847,7 @@ class MailTests(MailTestsMixin, SimpleTestCase):
msg.attach("file.txt", b"file content") msg.attach("file.txt", b"file content")
filename, content, mimetype = self.get_decoded_attachments(msg)[0] filename, content, mimetype = self.get_decoded_attachments(msg)[0]
self.assertEqual(filename, "file.txt") self.assertEqual(filename, "file.txt")
self.assertEqual(content, b"file content") self.assertEqual(content, "file content") # (decoded)
self.assertEqual(mimetype, "text/plain") self.assertEqual(mimetype, "text/plain")
def test_attach_utf8_text_as_bytes(self): def test_attach_utf8_text_as_bytes(self):
@ -842,7 +859,7 @@ class MailTests(MailTestsMixin, SimpleTestCase):
msg.attach("file.txt", b"\xc3\xa4") # UTF-8 encoded a umlaut. msg.attach("file.txt", b"\xc3\xa4") # UTF-8 encoded a umlaut.
filename, content, mimetype = self.get_decoded_attachments(msg)[0] filename, content, mimetype = self.get_decoded_attachments(msg)[0]
self.assertEqual(filename, "file.txt") self.assertEqual(filename, "file.txt")
self.assertEqual(content, b"\xc3\xa4") self.assertEqual(content, "ä") # (decoded)
self.assertEqual(mimetype, "text/plain") self.assertEqual(mimetype, "text/plain")
def test_attach_non_utf8_text_as_bytes(self): def test_attach_non_utf8_text_as_bytes(self):
@ -1341,10 +1358,9 @@ class BaseEmailBackendTests(MailTestsMixin):
num_sent = mail.get_connection().send_messages([email]) num_sent = mail.get_connection().send_messages([email])
self.assertEqual(num_sent, 1) self.assertEqual(num_sent, 1)
message = self.get_the_message() message = self.get_the_message()
self.assertEqual(message["subject"], "=?utf-8?q?Ch=C3=A8re_maman?=") self.assertEqual(message["subject"], "Chère maman")
self.assertEqual( self.assertIn(b"Subject: =?utf-8?q?Ch=C3=A8re_maman?=", message.as_bytes())
message.get_payload(decode=True).decode(), "Je t'aime très fort" self.assertEqual(message.get_content(), "Je t'aime très fort")
)
def test_send_long_lines(self): def test_send_long_lines(self):
""" """
@ -1390,8 +1406,10 @@ class BaseEmailBackendTests(MailTestsMixin):
) )
email.send() email.send()
message = self.get_the_message() message = self.get_the_message()
self.assertEqual( self.assertEqual(message["from"], "Firstname Sürname <from@example.com>")
message["from"], "=?utf-8?q?Firstname_S=C3=BCrname?= <from@example.com>" self.assertIn(
b"From: =?utf-8?q?Firstname_S=C3=BCrname?= <from@example.com>",
message.as_bytes(),
) )
def test_plaintext_send_mail(self): def test_plaintext_send_mail(self):
@ -1623,7 +1641,10 @@ class LocmemBackendTests(BaseEmailBackendTests, SimpleTestCase):
email_backend = "django.core.mail.backends.locmem.EmailBackend" email_backend = "django.core.mail.backends.locmem.EmailBackend"
def get_mailbox_content(self): def get_mailbox_content(self):
return [m.message() for m in mail.outbox] # Reparse as modern messages to work with shared BaseEmailBackendTests.
# (Once EmailMessage.message() uses Python's modern email API, this
# can be changed back to `[m.message() for m in mail.outbox]`.)
return [message_from_bytes(m.message().as_bytes()) for m in mail.outbox]
def flush_mailbox(self): def flush_mailbox(self):
mail.outbox = [] mail.outbox = []
@ -1702,7 +1723,7 @@ class FileBackendTests(BaseEmailBackendTests, SimpleTestCase):
self.assertEqual(len(os.listdir(self.tmp_dir)), 1) self.assertEqual(len(os.listdir(self.tmp_dir)), 1)
with open(os.path.join(self.tmp_dir, os.listdir(self.tmp_dir)[0]), "rb") as fp: with open(os.path.join(self.tmp_dir, os.listdir(self.tmp_dir)[0]), "rb") as fp:
message = message_from_binary_file(fp) message = message_from_binary_file(fp, policy=policy.default)
self.assertEqual(message.get_content_type(), "text/plain") self.assertEqual(message.get_content_type(), "text/plain")
self.assertEqual(message.get("subject"), "Subject") self.assertEqual(message.get("subject"), "Subject")
self.assertEqual(message.get("from"), "from@example.com") self.assertEqual(message.get("from"), "from@example.com")
@ -1791,17 +1812,13 @@ class SMTPHandler:
mail_from = envelope.mail_from mail_from = envelope.mail_from
message = message_from_bytes(data.rstrip()) message = message_from_bytes(data.rstrip())
message_addr = parseaddr(message.get("from"))[1] try:
if mail_from != message_addr: header_from = message["from"].addresses[0].addr_spec
# According to the spec, mail_from does not necessarily match the except (KeyError, IndexError):
# From header - this is the case where the local part isn't header_from = None
# encoded, so try to correct that.
lp, domain = mail_from.split("@", 1)
lp = Header(lp, "utf-8").encode()
mail_from = "@".join([lp, domain])
if mail_from != message_addr: if mail_from != header_from:
return f"553 '{mail_from}' != '{message_addr}'" return f"553 '{mail_from}' != '{header_from}'"
self.mailbox.append(message) self.mailbox.append(message)
return "250 OK" return "250 OK"