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:
parent
bddd35cb1a
commit
5d7001b578
@ -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"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user