I’d like to propose we add text_body and html_body options to django.core.mail.EmailMessage to simplify sending html + plaintext (or html-only) email:
from django.core.mail import EmailMessage
msg = EmailMessage(
to=["to@example.com"],
subject="Test subject",
text_body="plain text message body",
html_body="<p>html message body</p>",
)
msg.send()
(Many other email APIs seem to use these names, or just text and html. Unlike the existing EmailMessage body option, text_body would always be text/plain content.)
[Edit: I’d originally proposed calling these message and html_message to match Django’s existing send_mail(), but EmailMessage already has a message() method that would conflict.]
Like other EmailMessage options, text_body and html_body would be both optional constructor params and object properties that could be set at any time prior to calling send():
msg = EmailMessage(..., text_body="Welcome to Django!")
if user.preferences.prefers_html_email:
msg.html_body = "Welcome to <strong>Django!</strong>"
msg.send()
The new properties would simplify testing email content, and would work well with existing test methods like assertInHTML():
def test_email_contains_unsubscribe_links(self):
... # do something that sends email
sent_msg = django.core.mail.outbox[0]
self.assertIn("Unsubscribe: https://...", sent_msg.text_body)
self.assertInHTML('<a href="...">unsubscribe</a>', sent_msg.html_body)
We would make text_body and html_body compatible with existing code, implementing them as @property getters/setters that manipulate the existing body and alternatives properties. This would provide full interoperability with third-party email backends and other libraries that aren’t aware of the new properties. (Sample implementation below.)
One implication is we’d need support for alternative parts in the base EmailMessage class—this is currently handled by a separate EmailMultiAlternatives subclass. I propose hoisting all the alternatives code into EmailMessage, essentially combining the two classes. (A hollowed-out EmailMultiAlternatives would remain for compatibility—more on that in the details below.)
I believe this could also simplify the email documentation quite a bit. There would be no need to understand the difference between EmailMessage and EmailMultiAlternatives—EmailMessage would cover it all. And for the common case of sending html + plaintext messages, you wouldn’t need to know jargon like “alternative” and “MIME type” and content_subtype. (Once simplified, we might even consider finally moving the EmailMessage documentation up the page near the “only for backwards compatibility” functions at the top.
)
More nitty-gritty inside these disclosure triangles…
Some details
-
Why not use the existing
bodyoption for the plaintext (and just addhtml_body)? Because it would break compatibility. Thebodyis usually text/plain, but you can setcontent_subtypeto change that. More accurately,bodyrepresents the content of the first (primary) text part in the message, which might be text/plain or text/html or text/something-else. (Both now and with this proposed change.) -
Trying to pass
bodyalong with either oftext_bodyorhtml_bodyto the EmailMessage constructor would be a ValueError. (But you could freely set any of the properties after that—see the notes about interoperability above and the sample implementation below.) -
I think we should NOT deprecate EmailMultiAlternatives as part of this. A lot of code out there uses it (because there’s been no, um, alternative
). Deprecating it right away would create a lot of noise and busywork. Instead, I’d suggest leaving EmailMultiAlternatives in place as a stub class, documented as there solely to simplify maintenance of existing code. And then if/when its use has declined (in maybe a decade or so?), we could deprecate it. -
I’d suggest these—and other future new EmailMessage options—be keyword-only arguments to
EmailMessage.__init__. (The list is already pretty long, making posargs prone to error, and complicating subclassing in third-party code.)
Sample implementation of the html_body property
# (untested code)
class EmailMessage:
...
@property
def html_body(self):
if self.content_subtype == "html" and self.body is not None:
return self.body
else:
# Return the first text/html alternative.
for alternative in self.alternatives:
if alternative.mimetype == "text/html":
return alternative.content
return None
@html_body.setter
def html_body(self, content):
if self.content_subtype == "html":
self.body = content
else:
# Replace the first text/html alternative,
# or if there isn't one add the new alternative at the end.
new_alternative = EmailAlternative(content, "text/html")
for index, alternative in enumerate(self.alternatives):
if alternative.mimetype == "text/html":
self.alternatives[index] = new_alternative
break
else:
self.alternatives.append(new_alternative)
The text_body property implementation would be similar, but substituting
“text/plain”.