skip to content

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%

answer

  1. shaped like DATABASES and CACHES
  2. BACKEND plus OPTIONS per alias
  3. the using= keyword
  4. mailers[alias] for batches

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.

solid answer

~40 s

Django 6.1 added `MAILERS`, a dict of aliases shaped like `DATABASES` or `CACHES`. Each entry has a `BACKEND` import path (SMTP when omitted) and `OPTIONS` passed to that backend, such as `host`, `use_tls`, `username`, `password` and `timeout`. Make `"default"` the transactional relay for password resets and receipts, and add a `"marketing"` alias with its own server or credentials, so campaign traffic never shares a connection or account with account email. Sending code picks one with `using="marketing"`. For a batch, `mail.mailers["marketing"]` returns a backend you can use as a context manager with `send_messages()`. An unknown alias raises `MailerDoesNotExist`. `MAILERS` replaces `EMAIL_BACKEND` and the other `EMAIL_*` settings in 7.0, and `connection=`, `get_connection()` and `fail_silently` are deprecated in its favour.

code

python · 25 lines
python
# settings.py
import os

MAILERS = {
    "default": {  # transactional: resets, receipts
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {
            "host": "smtp-tx.example.net",
            "use_tls": True,
            "username": os.environ["TX_SMTP_USER"],
            "password": os.environ["TX_SMTP_PASSWORD"],
            "timeout": 10,
        },
    },
    "marketing": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {
            "host": "smtp-bulk.example.net",
            "use_tls": True,
            "username": os.environ["BULK_SMTP_USER"],
            "password": os.environ["BULK_SMTP_PASSWORD"],
            "timeout": 30,
        },
    },
}

go deeper

for a junior

Recall that MAILERS maps aliases to a BACKEND and OPTIONS, and that using= picks which one sends.

for a middle

Explain the 'default' fallback, MailerDoesNotExist for unknown aliases, mailers[alias] for batches, and why EMAIL_* cannot coexist with MAILERS.

for a senior

Plan the migration off connection=, get_connection() and fail_silently, and split transactional from bulk streams so one cannot starve the other.

for a principal

Decide which email streams deserve their own mailer and provider account, and who owns the credentials and quotas for each.

## Why a project wants more than one mailer Before Django 6.1 a project had exactly one global email configuration: `EMAIL_BACKEND` plus `EMAIL_HOST`, `EMAIL_PORT`, `EMAIL_HOST_USER` and the rest. Sending a second stream a different way meant building a backend by hand with `get_connection(...)` and threading it through every call as `connection=`. Real products often want separate streams: - **transactional** mail (password resets, sign-up confirmation, receipts), which must arrive quickly and reliably; - **marketing** or bulk mail (newsletters, campaigns), which is high-volume and often goes through a different provider or account; - **internal** mail (error reports to admins) through a local relay. Keeping them on separate configurations means a large campaign cannot tie up the connection, credentials or sending account that password resets depend on. ## The `MAILERS` setting `MAILERS` is a dict from **alias** to configuration, the same pattern as `DATABASES`, `CACHES`, `STORAGES` and `TASKS`: | Key | Meaning | Default | |---|---|---| | alias (e.g. `"default"`) | name used by `using=` and `mailers[...]` | `"default"` is used when `using` is omitted | | `BACKEND` | import path of the email backend class | `django.core.mail.backends.smtp.EmailBackend` | | `OPTIONS` | keyword arguments for the backend's constructor | `{}` | For the SMTP backend the options are `host` (required), `port`, `username`, `password`, `use_tls` or `use_ssl`, `timeout`, and `ssl_certfile`/`ssl_keyfile`. A third-party backend takes whatever its constructor accepts. `OPTIONS` may not contain `alias`, which Django passes itself. ## Sending through an alias Every sending API grew a keyword-only `using` argument in 6.1: - `send_mail(..., using="marketing")` and `send_mass_mail(..., using=...)`; - `EmailMessage.send(using="marketing")`; - `mail_admins(..., using=...)` and `mail_managers(..., using=...)`. If `using` is omitted, the `"default"` alias sends. If the alias is not defined, Django raises `MailerDoesNotExist`. There is **no** silent fallback to the default. Passing `using` together with a deprecated `connection`, `fail_silently=True`, `auth_user` or `auth_password` raises `TypeError` instead of guessing which one wins. ## Batching with `mailers[alias]` `django.core.mail.mailers` is a dict-like factory. `mailers["marketing"]` returns a **new backend instance** built from that alias, and `mailers.default` is a shortcut for `mailers["default"]`. For a campaign you open one connection and reuse it: 1. `with mail.mailers["marketing"] as backend:` opens the connection; 2. `backend.send_messages(batch)` sends each chunk over that open connection; 3. leaving the block closes it, even on an exception. A malformed entry, such as SMTP without a `host`, raises `InvalidMailer` when the backend is created. ## Migrating an existing project `MAILERS` is **opt-in** in 6.1: - Without it, the old `EMAIL_*` settings keep working with deprecation warnings, and `mailers.default` builds a backend from them. - Defining `MAILERS` **and** any deprecated `EMAIL_*` setting raises `ImproperlyConfigured`, so you move all of them at once. - `get_connection()` and the `connection=` argument are deprecated. Replace them with an alias and `using=`. - `fail_silently=True` is deprecated. Where you really want some errors ignored, put `"fail_silently": True` in one alias's `OPTIONS`, or catch the exception you mean (`OSError` for SMTP trouble, `MailerDoesNotExist` in a reusable app). - In **Django 7.0** the `EMAIL_*` settings are gone and there is no default mailer: sending without `MAILERS` raises `MailerDoesNotExist`. ## Choosing aliases in practice A workable convention for a product with both streams: | Alias | Carries | Typical options | |---|---|---| | `"default"` | password resets, sign-up confirmation, receipts | short `timeout`, the transactional account's credentials | | `"marketing"` | newsletters, campaigns | a different host or account, a longer `timeout` for large batches | | `"internal"` | `mail_admins()` alerts, reports | a local relay with no authentication | Make the transactional stream `"default"`. Django's own features that send mail without an alias (password reset, `sendtestemail` without `--using`) then land on the reliable path, and only code that opts in with `using=` reaches the bulk account. Keep the alias names in one module of constants so a typo becomes an import error, not a `MailerDoesNotExist` in production. ## Checks and tests `mail.W001` warns when `MAILERS` has no `"default"` entry. `mail.E001` (under `check --deploy`) rejects a development backend as `"default"`. During tests the runner rewrites **every** alias to the locmem backend, and each captured message records the alias in `sent_using`. That lets a test tell a marketing send from a transactional one, while the assertion techniques themselves belong to the testing leaf.

  • Your code calls send_mail(..., fail_silently=True). How do you migrate it once MAILERS is defined?
    Combining `fail_silently=True` with `using=` raises `TypeError`, and the argument is deprecated. Decide what you meant to ignore. Usually you drop it, because recipient typos aren't detected at send time anyway. Catch `OSError` to ignore SMTP trouble, catch `MailerDoesNotExist` in a reusable app, or give one alias `"fail_silently": True` in its `OPTIONS` and send through it with `using=`.
  • What happens in a 6.1 project that has not defined MAILERS at all?
    The deprecated settings still apply: `EMAIL_BACKEND` defaults to the SMTP backend on `localhost:25`, `mailers.default` builds from the `EMAIL_*` values, and Django emits deprecation warnings. Sending with an alias other than `"default"` raises `MailerDoesNotExist`, because no aliases exist. In Django 7.0 there is no default mailer, so any send without `MAILERS` fails.

saying these in an interview costs you the question

  • An unknown using= alias quietly falls back to the default mailer.
  • fail_silently=True can be combined with using= to suppress errors.
  • You can keep EMAIL_HOST next to MAILERS during the migration.
  • get_connection() is still the recommended way to get a backend in 6.1.
  • Without MAILERS defined, a 6.1 project can no longer send email at all.