#37429 closed Bug (needsnewfeatureprocess)
Cannot create multipart alternative mail with subparts
| Reported by: | jddupas | Owned by: | |
|---|---|---|---|
| Component: | Core (Mail) | Version: | 6.1 |
| Severity: | Normal | Keywords: | |
| Cc: | Mike Edmunds | Triage Stage: | Unreviewed |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description (last modified by )
I want to create a mail with a plain/text alternative, and an html alternative containing images referenced by CID.
It used to be possible before Django 6, but the new API fails when trying to build such mail:
multipart/alternative
├── text/plain; charset=UTF-8
└── multipart/related; type="text/html"
├── text/html; charset=UTF-8
└── image/png
Content-ID: <image001>
Content-Disposition: inline
I tried to create the HTML + image part using a MIMEPart(), but there is no way to attach it to the Django EmailMultiAlternatives.
The attach_alternative() method does not support taking a MimePart as content parameter. It requires a content-type.
If the content-type is text, in _add_bodies(), it calls add_alternative() with a charset parameter, and fails as the charset argument is not supported by the content manager when inserting a MimePart.
If the content-type is something else, _add_bodies() converts the whole MimePart into base64 and insert it unstructured.
If I use the attach() method instead, the part is properly inserted, but the whole messages switch to mixed content-type, and not alternative content-type.
Attachments (1)
Change History (9)
comment:1 by , 3 weeks ago
| Description: | modified (diff) |
|---|
comment:2 by , 3 weeks ago
| Description: | modified (diff) |
|---|
comment:3 by , 3 weeks ago
comment:4 by , 3 weeks ago
| Has patch: | set |
|---|---|
| Triage Stage: | Unreviewed → Accepted |
comment:5 by , 3 weeks ago
| Has patch: | unset |
|---|---|
| Triage Stage: | Accepted → Unreviewed |
comment:6 by , 3 weeks ago
I've prepared a patch for this ticket and added a regression test for MIMEPart alternatives containing nested MIME structures.
The PR checker reports that the Trac ticket isn't ready for a pull request. Could someone clarify whether any further triage is needed before the proposed change can be submitted for review?
comment:7 by , 2 weeks ago
| Cc: | added |
|---|---|
| Resolution: | → needsinfo |
| Status: | new → closed |
Hello hello
Can you share the code you used to build this message before Django 6.0?
This would help us understand whether this is a regression or whether the release notes should explain the upgrade path more clearly.
The docs suggest overriding EmailMessage.message() to customize the resulting message. See https://docs.djangoproject.com/en/dev/topics/email/#django.core.mail.EmailMessage.message
Would that approach work for you? Maybe something along the lines of:
from email import policy from django.core.mail import EmailMultiAlternatives class InlineImageEmail(EmailMultiAlternatives): inline_images = None def message(self, *, policy=policy.default): message = super().message(policy=policy) if self.inline_images: html = message.get_body(preferencelist=("html",)) for content, cid in self.inline_images: html.add_related( content, maintype="image", subtype="png", cid=cid, disposition="inline", ) html.set_param("type", "text/html") return message email = InlineImageEmail( subject="Hello", body="Hello", from_email="from@example.com", to=["to@example.com"], ) email.inline_images = [ (first_image_bytes, "<image001>"), (second_image_bytes, "<image002>"), ] email.attach_alternative( '<p>Hello</p><img src="cid:image001"><img src="cid:image002">', "text/html", ) email.send()
Please reopen the ticket with more details if the above is not useful
by , 13 hours ago
| Attachment: | test_mail_structure.py added |
|---|
comment:8 by , 12 hours ago
| Resolution: | needsinfo → needsnewfeatureprocess |
|---|
I agree this is a hole in Django's email functionality. Django ought to have a straightforward way to construct an email message where inline images are correctly contained in a multipart/related part with their associated text/html content, rather than handled as multipart/mixed attachments.
But I don't think this is a new hole: I can't find any way to construct a message with the structure below in Django 5.2. There is a real need here, but it's a new feature request, not lost functionality in Django 6.0 (from what I can tell).
attachment:test_mail_structure.py has the various approaches I tried to create a message with this structure in Django 5.2, using legacy MIMEObject types compatible with the legacy email APIs Django used back then. None of them worked:
multipart/alternative
text/plain
multipart/related
text/html
image/gif: Content-Disposition: inline; Content-ID: <image-1>
Sarah's suggestion in comment:7 works with any EmailBackend that calls EmailMessage.message() to convert the Django email to a Python email.EmailMessage (like the SMTP backend). But it won't help with third-party email backends that inspect the Django email's attachments and alternatives directly to figure out what to send (such as django-anymail for most ESP APIs).
The docs cover passing a MIMEPart object to EmailMessage.attach() and give the specific example of an inline image. This places the image in the multipart/mixed content, which is not technically correct but is displayed as expected (inline in the html) by most email clients. (It's a reasonable workaround. Fwiw, django-anymail specifically detects this case and extracts inline attachments for ESP APIs that support them.)
multipart/mixed
multipart/alternative
text/plain
text/html
image/gif: Content-Disposition: inline; Content-ID: <image-1>
The proposal in comment:3 to add a new special case in EmailMultiAlternatives._add_bodies() would provide a solution, but leads to increasingly awkward calling code that mixes parts of Python's email API with Django's mail:
from email.message import MIMEPart from django.core.mail import EmailMultiAlternatives email = EmailMultiAlternatives(body="Text body") html_part = MIMEPart() html_part.set_content('HTML body <img src="cid:image-1">', subtype="html") html_part.add_related(b"GIF89a...", maintype="image", subtype="png", cid="<image-1>") email.attach_alternative(html_part, "multipart/related")
That new special case would also greatly complicate third-party backends that don't use EmailMessage.message(), as they would have to inspect and unpack arbitrary MIMEPart trees and try to translate them into ESP API fields.
I think trying to mix the two APIs like this is the wrong approach. The django.core.mail APIs were meant to be "wrappers for Python’s email and smtplib modules to simplify composing and sending email" [first paragraph of the email docs; emphasis added]. But since django.core.mail was created, Python has introduced modern email APIs that are considerably simpler than the legacy APIs Django was trying to simplify. Adding more arcane special cases to Django feels like a step in the wrong direction.
Given that inline images (for html email) are a fairly common need, I'd like to see Django add first class support for them. Perhaps inlines parallel to attachments and alternatives. Marking this "needsnewfeatureprocess" with that in mind.
One way to fix this, would be to modifiy EmailMultiAlternatives._add_bodies() to support MIMEPart:
if maintype == "text": if isinstance(content, bytes): content = content.decode() msg.add_alternative(content, subtype=subtype, charset=encoding) elif isinstance(content, email.message.MIMEPart): msg.attach(content) else: content = force_bytes(content, encoding=encoding, strings_only=True) msg.add_alternative(content, maintype=maintype, subtype=subtype)As the MIMEPart already define its content type, it should just be attached to the python Message.