skip to content

Outgoing Mail

Django sends mail with send_mail and EmailMessage through a pluggable backend, and 6.1 adds MAILERS aliases chosen with using=. Interviewers probe dev backends and sending off the request.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Django, when is send_mail() enough, and when do you build an EmailMessage or EmailMultiAlternatives instead?

level: juniorimportance: must knowfreq 62%

answer

  1. frozen convenience wrapper
  2. everyone sees everyone in To
  3. bcc, reply_to, headers, attachments
  4. attach_alternative for the HTML part

basics

~20 s

send_mail() sends one message, optionally with an html_message alternative, to a recipient list that all appear in To. For CC, BCC, Reply-To, custom headers or attachments, build an EmailMessage, or an EmailMultiAlternatives to add body versions.

solid answer

~30 s

`send_mail(subject, message, from_email, recipient_list)` is a thin wrapper: it builds an `EmailMultiAlternatives` and calls `send()`. `from_email=None` falls back to `DEFAULT_FROM_EMAIL`, every address in `recipient_list` appears in the same `To:` header, and `html_message=` adds a `text/html` alternative. It returns the number of messages delivered, so `0` or `1`. Its signature is deliberately frozen, so anything beyond that needs an `EmailMessage` built directly: `cc`, `bcc`, `reply_to`, `headers`, `attachments` or `attach_file()`. `EmailMultiAlternatives` subclasses it and adds `attach_alternative(content, mimetype)`, which is how you send a plain-text body plus an HTML version. In Django 6.1 all of them take `using=` to pick a `MAILERS` alias.

code

python · 19 lines
python
from email.headerregistry import Address

from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string


def send_order_receipt(order):
    context = {"order": order}
    msg = EmailMultiAlternatives(
        subject=f"Receipt for order {order.number}",
        body=render_to_string("emails/receipt.txt", context),
        from_email=None,  # DEFAULT_FROM_EMAIL
        to=[Address(display_name=order.customer_name, addr_spec=order.email)],
        reply_to=["[email protected]"],
        bcc=["[email protected]"],
    )
    msg.attach_alternative(render_to_string("emails/receipt.html", context), "text/html")
    msg.attach_file(order.invoice_pdf.path)
    return msg.send()  # 1 on success

go deeper

for a junior

Recall the four required send_mail() arguments, that recipients share one To header, and that html_message adds an HTML part.

for a middle

Explain that send_mail() wraps EmailMultiAlternatives, list what only EmailMessage offers, and contrast attach_alternative with content_subtype.

for a senior

Show you build addresses with Address rather than string formatting and know which 6.0 and 6.1 argument changes will break old call sites.

for a principal

Argue for one shared message-building helper per email type so templates, headers and mailer choice stay consistent across the codebase.

## The one-call helper: `send_mail()` `django.core.mail.send_mail()` is the shortest path from a view or a task to an outgoing message. It takes four required arguments, `subject`, `message`, `from_email` and `recipient_list`, and a handful of keyword-only options: - **`from_email=None`** falls back to the `DEFAULT_FROM_EMAIL` setting (`"webmaster@localhost"` by default, which production projects override). - **`recipient_list`** is a list of addresses that all land in **one** message's `To:` header, so every recipient sees the others. - **`html_message=`** turns the message into `multipart/alternative`, with `message` as `text/plain` and the HTML as `text/html`. - **`using=`** (new in **Django 6.1**) names a `MAILERS` alias. Without it the `"default"` mailer sends. It returns the number of messages successfully delivered, which can only be `0` or `1`. Internally it builds an `EmailMultiAlternatives` and calls `send()` on it. Django's docs call its API **frozen**: new features go into the class, not into more keyword arguments. ## `EmailMessage`: the object you configure `EmailMessage` is the real model. The first four constructor parameters (`subject`, `body`, `from_email`, `to`) can be positional. Everything else is keyword-only, and passing it positionally has been deprecated since 6.0: - **`cc`**, **`bcc`** and **`reply_to`**: lists of addresses. BCC addresses go into the SMTP envelope through `recipients()` but never into a header. Since 6.1, putting `Bcc` in `headers=` raises `ValueError`. - **`headers`**: a dict of extra headers, for example `List-Unsubscribe`. - **`attachments`**: tuples of `(filename, content, mimetype)`, `EmailAttachment` named tuples, or (since 6.0) Python `MIMEPart` objects. You can also call `attach()` or `attach_file(path)` later. `send(using=None)` delivers it and returns `1` or `0`. An empty recipient list returns `0` without opening a connection. `message()` returns the Python `email.message.EmailMessage` that would go on the wire. Since Django 6.0 that is Python's **modern email API** object, not the old `SafeMIMEText`/`SafeMIMEMultipart` classes, which are deprecated. ## Text plus HTML: `EmailMultiAlternatives` A well-formed marketing or notification email usually carries a plain-text body and an HTML version of the same content. `EmailMultiAlternatives` subclasses `EmailMessage` and adds: 1. `attach_alternative(content, mimetype)`, which appends an `EmailAlternative(content, mimetype)` to `.alternatives`; 2. `body_contains(text)`, which checks the body and every `text/*` alternative. Do not confuse this with `content_subtype = "html"` on a plain `EmailMessage`. That changes the **main** body's MIME subtype to `text/html` and adds no plain-text fallback. Django's docs recommend leaving the main body as `text/plain`. ## Choosing between them | Need | `send_mail()` | `EmailMessage` | `EmailMultiAlternatives` | |---|---|---|---| | One message, shared `To:` | yes | yes | yes | | Plain text plus HTML | via `html_message=` | no (`content_subtype` replaces the body) | `attach_alternative()` | | CC, BCC, Reply-To | no | yes | yes | | Custom headers | no | `headers=` | `headers=` | | Attachments | no | `attach()`, `attach_file()` | same | | Pick a mailer (6.1) | `using=` | `send(using=)` | `send(using=)` | `send_mass_mail(datatuple)` is a separate helper. Each tuple becomes its **own** message and they share one connection, which is how you give recipients individual `To:` headers without a loop of `send_mail()` calls. ## Rendering bodies from templates Real emails are rarely string literals. The common pattern keeps two templates per email type and renders both with the same context: - `render_to_string("emails/welcome.txt", context)` for the plain-text `body`; - `render_to_string("emails/welcome.html", context)` for the `text/html` alternative; - one small function per email type that builds and returns the message, so views, tasks and management commands all send the same thing. A few details matter here: - Template autoescaping is on for both templates. That is right for the HTML version, but in the text template a user's name containing `&` would come out as `&amp;`. Wrap the text template in `{% autoescape off %}`. - Build absolute URLs (for example with `request.build_absolute_uri()`), because relative links mean nothing inside a mail client. - Keep subjects on one line. Django raises `ValueError` rather than sending a subject containing a newline. ## Safety details interviewers probe - **Header injection:** a CR or LF in any header raises `ValueError` when the message is built. Before 6.0 Django raised `BadHeaderError`, which is now deprecated. - **Display names:** never build `f'"{name}" <{email}>'` from user input, because the name can smuggle extra addresses. Use `email.headerregistry.Address(display_name=..., addr_spec=...)`. Django's built-in backends accept `Address` objects in any address field. - **Errors surface by default.** The `fail_silently` argument still exists but is deprecated in 6.1, and the migration guide recommends dropping it unless you have a specific reason. ## Version notes - **6.0:** the modern email API, keyword-only optional arguments, and `ValueError` replacing `BadHeaderError`. - **6.1:** `using=` on every sender and `MAILERS`. `connection=`, `get_connection()`, `fail_silently`, `auth_user` and `auth_password` are deprecated ahead of removal in 7.0.

  • How do you include a user's full name in the To: address without opening a header-injection hole?
    Build the address with Python's `email.headerregistry.Address(display_name=user.get_full_name(), addr_spec=user.email)` and pass it in the `to` list; Django's built-in backends accept `Address` objects. Never format `f'"{name}" <{email}>'`, because a crafted name can inject extra recipients. CR or LF characters in any header make Django raise `ValueError` when the message is sent.
  • How does send_mass_mail() differ from calling send_mail() in a loop?
    `send_mass_mail(datatuple)` turns each `(subject, message, from_email, recipient_list)` tuple into a separate message and sends them all over one backend connection, returning the count delivered. A loop of `send_mail()` calls opens and closes a connection per call. Either way, recipients inside one tuple's list still share a `To:` header.

saying these in an interview costs you the question

  • send_mail() sends a separate email to each address in recipient_list.
  • send_mail() accepts cc, bcc and attachment keyword arguments.
  • Setting content_subtype to html adds an HTML version next to the plain text.
  • Building the To header with an f-string from the user's name is safe.
  • Django still raises BadHeaderError for a newline injected into a header.
open as a page

Which email backends does Django ship, and which would you configure for local development, automated tests and production?

level: middleimportance: should knowfreq 48%

basics

~20 s

Django ships smtp, console, filebased, locmem and dummy backends. Use console or filebased in development, let the test runner swap in locmem for tests, and use SMTP or a provider's backend in production. Only SMTP is meant for production.

open as a page

In Django, what does mail_admins() send, and which settings decide its recipients, its From address and its subject line?

level: middleimportance: should knowfreq 30%

basics

~10 s

mail_admins() emails the addresses in ADMINS, from SERVER_EMAIL (default 'root@localhost'), with EMAIL_SUBJECT_PREFIX ('[Django] ') prepended to the subject. It returns without sending if ADMINS is empty. mail_managers() does the same for MANAGERS.

open as a page

A Django signup view calls send_mail() and sometimes hangs for minutes when the SMTP relay is slow — why, and how would you fix it?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Django's email backends are synchronous, so send_mail() holds the request while it connects and talks SMTP, and the SMTP backend has no timeout unless you set one. Set the mailer's timeout, then move sending off the request into a task run by a real worker.

open as a page

In Django 6.1, how would you configure separate mailers for transactional and marketing email, and choose one when sending?

level: middleimportance: nice to knowfreq 22%

basics

~10 s

Define both aliases in the MAILERS setting, each with a BACKEND and OPTIONS. Pass using='marketing' to send_mail(), EmailMessage.send() or mail_admins(), or take a backend from django.core.mail.mailers['marketing']. Without using=, the 'default' alias sends.

open as a page