skip to content

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%

answer

  1. a list of address strings
  2. not DEFAULT_FROM_EMAIL
  3. root@localhost by default
  4. a prefix with a trailing space

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.

solid answer

~40 s

`django.core.mail.mail_admins(subject, message)` builds one message to every address in `ADMINS`, with `html_message=` for an HTML version and, in 6.1, `using=` for a mailer alias. The `From:` header is `SERVER_EMAIL`, not `DEFAULT_FROM_EMAIL`. `SERVER_EMAIL` defaults to `'root@localhost'`, which many relays reject, so error emails silently never arrive until you set it. The subject gets `EMAIL_SUBJECT_PREFIX`, `'[Django] '` by default. `ADMINS` and `MANAGERS` both default to empty lists, and when the list is empty the function returns without sending anything. Since 6.0 these settings take plain address strings. The old `(name, address)` tuples are deprecated. `mail_managers()` is identical but reads `MANAGERS`, which does not inherit from `ADMINS`.

code

python · 14 lines
python
# settings.py
ADMINS = ["[email protected]", '"On-call" <[email protected]>']
MANAGERS = ADMINS  # not inherited automatically
SERVER_EMAIL = "[email protected]"  # default is 'root@localhost'
EMAIL_SUBJECT_PREFIX = "[shop-prod] "  # keep the trailing space

# management/commands/import_catalog.py
from django.core.mail import mail_admins

if failures:
    mail_admins(
        f"Catalog import: {len(failures)} rows failed",
        "\n".join(failures),
    )

go deeper

for a junior

Recall that mail_admins() writes to the ADMINS list and adds '[Django] ' to the subject.

for a middle

Explain that SERVER_EMAIL, not DEFAULT_FROM_EMAIL, is the sender, and that an empty ADMINS or MANAGERS list sends nothing without error.

for a senior

Diagnose missing error emails through SERVER_EMAIL, relay rejection and empty lists, and verify with sendtestemail --admins.

for a principal

Route operational alerts through a mailer that doesn't fail together with the customer-facing provider it is meant to report on.

## What `mail_admins()` is for `django.core.mail.mail_admins()` is a convenience function for mail aimed at **the people who run the site**, not at users. Django's own error reporting uses the same path: the logging `AdminEmailHandler` emails `ADMINS` about unhandled exceptions when `DEBUG` is `False`. You also call it from your own code, for example a nightly management command that reports failed imports. Its sibling `mail_managers()` targets `MANAGERS`, the audience for broken-link notifications from `BrokenLinkEmailsMiddleware`. ## The settings it reads | Setting | Role in `mail_admins()` | Default | |---|---|---| | `ADMINS` | recipients (all in one `To:` header) | `[]` | | `MANAGERS` | recipients for `mail_managers()` | `[]` | | `SERVER_EMAIL` | the `From:` address | `'root@localhost'` | | `EMAIL_SUBJECT_PREFIX` | prepended to `subject` | `'[Django] '` | | `DEFAULT_FROM_EMAIL` | **not used here**; it is the fallback for `send_mail(from_email=None)` | `'webmaster@localhost'` | Two defaults cause most of the trouble: - **`SERVER_EMAIL = 'root@localhost'`**. A real relay or provider usually refuses to send from an address on a domain you don't control, so error emails fail. If they are sent through an error handler that swallows failures, they vanish without a trace. Set it to an address your relay accepts, such as `'[email protected]'`. - **`MANAGERS` defaults to an empty list, not to `ADMINS`.** Setting `ADMINS` alone leaves `mail_managers()` doing nothing. ## How it behaves 1. It reads the recipient setting. If the list is empty it **returns immediately**, so there is no error and no email. 2. It validates the setting. Since **6.0** each entry should be an address string, for example `'"Ops Team" <[email protected]>'`. The old `(name, address)` tuples still work but raise a deprecation warning, and any other shape raises `ImproperlyConfigured`. 3. It builds an `EmailMultiAlternatives` with the prefixed subject, the body, `SERVER_EMAIL` as sender and the recipients as `To:`. It adds `html_message` as a `text/html` alternative if given. 4. It sends through the `"default"` mailer, or the alias given as `using=` in 6.1. The function has no useful return value. Unlike `send_mail()`, it does not return a delivered count. ## Routing admin mail to its own mailer (6.1) With `MAILERS` you can send operational mail through a different backend than customer mail, for example a local relay: - `mail_admins("Import failed", report, using="internal")` from your own code; - the logging `AdminEmailHandler` accepts a `using` option for the same purpose. Configuring the handler belongs to error reporting. Keeping admin alerts on a separate mailer means a problem with the customer-facing provider, like an exhausted quota or revoked credentials, doesn't also silence the alerts that would tell you about it. ## `mail_admins()` compared with `send_mail()` | Aspect | `mail_admins()` | `send_mail()` | |---|---|---| | Recipients | the `ADMINS` setting | the `recipient_list` argument | | Sender | `SERVER_EMAIL` | `from_email`, or `DEFAULT_FROM_EMAIL` when `None` | | Subject | prefixed with `EMAIL_SUBJECT_PREFIX` | used as given | | Empty recipients | returns silently | returns `0` | | Return value | none | messages delivered (`0` or `1`) | | Mailer choice (6.1) | `using=` | `using=` | The split is deliberate. Customer email should come from a branded address (`DEFAULT_FROM_EMAIL`), while operational alerts should come from an address your team filters on (`SERVER_EMAIL`), with a subject prefix that says which environment sent them. ## Common mistakes - Expecting `DEFAULT_FROM_EMAIL` to change the sender of error emails. It is `SERVER_EMAIL`. - Forgetting the trailing space in a custom `EMAIL_SUBJECT_PREFIX`, which glues the prefix to the subject. - Leaving `ADMINS` empty in production and assuming "no emails" means "no errors". - Building `ADMINS` entries with string formatting from variable input. Use `str(email.headerregistry.Address(...))`, because the setting requires strings. - Passing `fail_silently=True`, which is deprecated in 6.1. Catch the specific exception instead. To check the whole path end to end, `manage.py sendtestemail --admins` sends a test message to `ADMINS` (`--managers` for `MANAGERS`, `--using` to pick a mailer in 6.1).

  • Error emails were arriving in staging but never in production. What do you check first?
    Check `SERVER_EMAIL`. If it is still `'root@localhost'` or a domain the production relay won't send for, the relay rejects the message. Then confirm `ADMINS` is non-empty in production settings, because an empty list makes `mail_admins()` return silently. Run `manage.py sendtestemail --admins` to exercise that exact path.
  • Why might you send mail_admins() through a separate mailer alias?
    In 6.1, `using="internal"` can route operational mail through a different backend, such as a local relay, than customer-facing mail. If the main provider fails, for example with revoked credentials or an exhausted quota, the admin alerts about that failure still get out instead of failing through the same broken path.

saying these in an interview costs you the question

  • mail_admins() sends from DEFAULT_FROM_EMAIL.
  • MANAGERS automatically copies ADMINS when only ADMINS is set.
  • An empty ADMINS list makes mail_admins() raise an error.
  • ADMINS must still be a list of (name, address) tuples in Django 6.x.
  • EMAIL_SUBJECT_PREFIX also prefixes subjects sent with send_mail().