In Django 6.1, how would you configure separate mailers for transactional and marketing email, and choose one when sending?
answer
- shaped like DATABASES and CACHES
- BACKEND plus OPTIONS per alias
- the using= keyword
- mailers[alias] for batches
basics
~10 sDefine 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 sDjango 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# 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
Recall that MAILERS maps aliases to a BACKEND and OPTIONS, and that using= picks which one sends.
Explain the 'default' fallback, MailerDoesNotExist for unknown aliases, mailers[alias] for batches, and why EMAIL_* cannot coexist with MAILERS.
Plan the migration off connection=, get_connection() and fail_silently, and split transactional from bulk streams so one cannot starve the other.
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.