# Proposal: simplify sending/testing html in EmailMessage

**URL:** <https://forum.djangoproject.com/t/proposal-simplify-sending-testing-html-in-emailmessage/32844>\
**Category:** Django Internals\
**Created:** [July 10, 2024, 12:36am UTC](https://forum.djangoproject.com/t/proposal-simplify-sending-testing-html-in-emailmessage/32844 "2024-07-10T00:36:26Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![medmunds](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/medmunds/32/22140_2.png) [@medmunds](https://forum.djangoproject.com/u/medmunds)\
**Post date:** [July 10, 2024, 12:36am UTC](https://forum.djangoproject.com/t/proposal-simplify-sending-testing-html-in-emailmessage/32844/1 "2024-07-10T00:36:26Z")

</div>

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:

```python
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()](https://docs.djangoproject.com/en/5.0/topics/email/#send-mail), but EmailMessage already has a message() method that would conflict.]

Like [other EmailMessage options](https://docs.djangoproject.com/en/5.0/topics/email/#emailmessage-objects), `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():

```python
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()](https://docs.djangoproject.com/en/5.0/topics/testing/tools/#django.test.SimpleTestCase.assertInHTML):

```python
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](https://docs.djangoproject.com/en/5.0/topics/email/#sending-alternative-content-types) 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`](https://docs.djangoproject.com/en/5.0/topics/email/#sending-alternative-content-types:~:text=the%20content_subtype%20attribute). (Once simplified, we _might_ even consider finally moving the EmailMessage documentation up the page near the “[only for backwards compatibility](https://docs.djangoproject.com/en/5.0/topics/email/#send-mail:~:text=only%20for%20backwards%20compatibility)” functions at the top. 😀)

More nitty-gritty inside these disclosure triangles…

> **Some details**
>
> - Why not use the existing `body` option for the plaintext (and just add `html_body`)? Because it would break compatibility. The `body` is _usually_ text/plain, but you can set [`content_subtype`](https://docs.djangoproject.com/en/5.0/topics/email/#sending-alternative-content-types:~:text=the%20content_subtype%20attribute) to change that. More accurately, `body` represents 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 `body` along with either of `text_body` or `html_body` to 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**
>
> ```python
> # (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”.

---

<div class="post-metadata">

**Author:** ![GitRon](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/gitron/32/13088_2.png) [@GitRon](https://forum.djangoproject.com/u/GitRon)\
**Post date:** [July 10, 2024, 10:49am UTC](https://forum.djangoproject.com/t/proposal-simplify-sending-testing-html-in-emailmessage/32844/2 "2024-07-10T10:49:24Z")

</div>

I think I started a similar take on this issue in [this thread](https://forum.djangoproject.com/t/new-take-on-old-ticket-send-templated-email/32692/2). Happy to merge the discussions if it makes sense.

---

<div class="post-metadata">

**Author:** ![medmunds](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/medmunds/32/22140_2.png) [@medmunds](https://forum.djangoproject.com/u/medmunds)\
**Post date:** [July 10, 2024, 6:00pm UTC](https://forum.djangoproject.com/t/proposal-simplify-sending-testing-html-in-emailmessage/32844/3 "2024-07-10T18:00:50Z")

</div>

I think we agree that Django’s EmailMessage makes working with html + text email more complicated than it needs to be. (And more complex than it is in many other frameworks/APIs.) And this matters because html + text has become a common—perhaps _the most common_—use case.

This proposal tries to target that specific issue by moving some of the boilerplate complexity Django currently imposes on all EmailMessage users into the EmailMessage implementation. I’ve deliberately kept the scope narrow.

Your thread on [class-based email](https://forum.djangoproject.com/t/new-take-on-old-ticket-send-templated-email/32692) seems like a much larger scope, with broad goals that also include generating email from templates. I suspect it will take a longer discussion to reach consensus on that.

The two approaches are not at all mutually exclusive. But they are separate ideas, so I think it would be helpful to keep the threads separate.

---

<div class="post-metadata">

**Author:** ![GitRon](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/gitron/32/13088_2.png) [@GitRon](https://forum.djangoproject.com/u/GitRon)\
**Post date:** [July 11, 2024, 2:50pm UTC](https://forum.djangoproject.com/t/proposal-simplify-sending-testing-html-in-emailmessage/32844/4 "2024-07-11T14:50:57Z")

</div>

Sounds very reasonable. Thx for the answer and your thoughts in “my” thread!
