Inbound: improve inline content handling

* refactor: derive `AnymailInboundMessage` from `email.message.EmailMessage`
  rather than legacy Python 2.7 `email.message.Message`

* feat(inbound): replace confusing `inline_attachments` with `content_id_map`
  and `inlines`; rename `is_inline_attachment` to `is_inline`; deprecate old names 

Closes #328

---------

Co-authored-by: Mike Edmunds <medmunds@gmail.com>
This commit is contained in:
Léo Martinez
2023-07-28 00:10:58 +02:00
committed by GitHub
parent bc8ef9af0f
commit 0ac248254e
13 changed files with 212 additions and 60 deletions

View File

@@ -1,31 +1,33 @@
import warnings
from base64 import b64decode
from email.message import Message
from email.message import EmailMessage
from email.parser import BytesParser, Parser
from email.policy import default as default_policy
from email.utils import unquote
from django.core.files.uploadedfile import SimpleUploadedFile
from .exceptions import AnymailDeprecationWarning
from .utils import angle_wrap, parse_address_list, parse_rfc2822date
class AnymailInboundMessage(Message):
class AnymailInboundMessage(EmailMessage):
"""
A normalized, parsed inbound email message.
A subclass of email.message.Message, with some additional
convenience properties, plus helpful methods backported
from Python 3.6+ email.message.EmailMessage (or really, MIMEPart)
A subclass of email.message.EmailMessage, with some additional
convenience properties.
"""
# Why Python email.message.Message rather than django.core.mail.EmailMessage?
# Why Python email.message.EmailMessage rather than django.core.mail.EmailMessage?
# Django's EmailMessage is really intended for constructing a (limited subset of)
# Message to send; Message is better designed for representing arbitrary messages:
# an EmailMessage to send; Python's EmailMessage is better designed for representing
# arbitrary messages:
#
# * Message is easily parsed from raw mime (which is an inbound format provided
# by many ESPs), and can accurately represent any mime email received
# * Message can represent repeated header fields (e.g., "Received") which
# are common in inbound messages
# * Python's EmailMessage is easily parsed from raw mime (which is an inbound format
# provided by many ESPs), and can accurately represent any mime email received
# * Python's EmailMessage can represent repeated header fields (e.g., "Received")
# which are common in inbound messages
# * Django's EmailMessage defaults a bunch of properties in ways that aren't helpful
# (e.g., from_email from settings)
@@ -103,13 +105,30 @@ class AnymailInboundMessage(Message):
"""list of attachments (as MIMEPart objects); excludes inlines"""
return [part for part in self.walk() if part.is_attachment()]
@property
def inlines(self):
"""list of inline parts (as MIMEPart objects)"""
return [part for part in self.walk() if part.is_inline()]
@property
def inline_attachments(self):
"""DEPRECATED: use content_id_map instead"""
warnings.warn(
"inline_attachments has been renamed to content_id_map and will be removed"
" in the near future.",
AnymailDeprecationWarning,
)
return self.content_id_map
@property
def content_id_map(self):
"""dict of Content-ID: attachment (as MIMEPart objects)"""
return {
unquote(part["Content-ID"]): part
for part in self.walk()
if part.is_inline_attachment() and part["Content-ID"] is not None
if part.is_inline() and part["Content-ID"] is not None
}
def get_address_header(self, header):
@@ -143,13 +162,19 @@ class AnymailInboundMessage(Message):
return part.get_content_text()
return None
# Hoisted from email.message.MIMEPart
def is_attachment(self):
return self.get_content_disposition() == "attachment"
def is_inline(self):
return self.get_content_disposition() == "inline"
# New for Anymail
def is_inline_attachment(self):
return self.get_content_disposition() == "inline"
"""DEPRECATED: use in_inline instead"""
warnings.warn(
"is_inline_attachment has been renamed to is_inline and will be removed"
" in the near future.",
AnymailDeprecationWarning,
)
return self.is_inline()
def get_content_bytes(self):
"""Return the raw payload bytes"""
@@ -331,7 +356,7 @@ class AnymailInboundMessage(Message):
if attachments is not None:
for attachment in attachments:
if attachment.is_inline_attachment():
if attachment.is_inline():
related.attach(attachment)
else:
msg.attach(attachment)