Anymail wraps fourteen ESPs behind one Django backend, and SendGrid is no longer tested
Django email backends and webhooks for Amazon SES, Brevo, MailerSend, Mailgun, Mailjet, Mailtrap, Postmark, Postal, Resend, Scaleway TEM, SendGrid, SparkPost, Unisender Go and more
At a glance
- What is it?
- Anymail is a BSD-licensed Django email integration layer that maps fourteen transactional email providers onto Django's own mail API, so switching ESPs means changing a backend path rather than rewriting send calls. It is the right layer for a Django project on a transactional ESP and the wrong layer for marketing mail or for SendGrid, which the README marks as no longer tested.
- Who is it for?
- Use Anymail when a Django project sends through a transactional ESP and you want provider independent code, webhook driven status signals and inbound handling. Do not use it for campaigns or newsletters, which ESP merge features do not replace, and treat SendGrid as unsupported because the README marks it no longer tested.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem: django.core.mail knows nothing about your ESP
Django ships its own email package, and it talks to an SMTP server. Many transactional providers use APIs with their own authentication, payload shapes and support for features such as tags or tracking. Django's API has no place for any of that.
Anymail solves this by registering its own EmailBackend classes and by extending the message classes with provider features. The README describes the goal as a consistent API that "avoids locking your code to one specific ESP", which is the practical value: the provider name appears in one settings entry, and switching from Mailgun to Postmark is a configuration change rather than a refactor.
The second half of the problem is the return path. Sends are only half of transactional email; you also want to know what happened afterwards. Anymail connects ESP webhooks to Django signals for normalized sent-message status and tracking notifications, and it handles inbound messages received through those same webhooks with portable access to attachments. That inbound half is the piece most home-grown integrations never build.
Fourteen backends, one message class, and one provider marked untested
The supported list in the README is: Amazon SES, Brevo (formerly Sendinblue), MailerSend, Mailgun and Mailjet under Sinch transactional email, Mailtrap, Mandrill under MailChimp transactional email, Postal for self-hosting, Postmark under ActiveCampaign, Resend, Scaleway TEM, SendGrid, SparkPost under Bird, and Unisender Go.
That is fourteen, and each is a backend module under `anymail.backends`, so the import path in your settings tells you exactly what you are committing to.
One entry carries an explicit warning: SendGrid is annotated "no longer tested". The code path exists, but the project no longer runs its integration suite against it, which means a change in SendGrid's API will not be caught for you. Postal is a different case: it is the self-hosted ESP in the list, which makes it the provider to test against if you want to run your own mail stack rather than a vendor's.
The portable extras are the ones every ESP tends to have and Django does not: tags, metadata and tracking options that carry the same meaning whichever backend is loaded.
Installing with the ESP extra, then wiring MAILERS or ANYMAIL
Install from PyPI with the extra for your provider in brackets. The README's example uses Mailgun:
pip install "django-anymail[mailgun]"Mailgun needs no extra dependencies, which is why the quickstart picks it, but other ESPs do, so the bracket is not optional decoration.
Add the app and configure the backend. In Django 6.1 or later, settings use a MAILERS dict:
INSTALLED_APPS = [
# ...
"anymail",
# ...
]
MAILERS = {
"default": {
"BACKEND": "anymail.backends.mailgun.EmailBackend",
"OPTIONS": {
"api_key": "<your Mailgun key>",
"sender_domain": "mg.example.com",
},
},
}
DEFAULT_FROM_EMAIL = "[email protected]"
SERVER_EMAIL = "[email protected]"In Django 6.0 or earlier the same thing is spelled with EMAIL_BACKEND and an ANYMAIL dict:
EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend"
ANYMAIL = {
"MAILGUN_API_KEY": "<your Mailgun key>",
"MAILGUN_SENDER_DOMAIN": 'mg.example.com',
}That version split is the one upgrade hazard in this library, and it is spelled out explicitly in the quickstart.
Your existing send_mail calls keep working, unchanged
Nothing about ordinary sending changes. After the backend is configured, Django's regular email functions route through the ESP:
from django.core.mail import send_mail
send_mail("It works!", "This will get sent through Mailgun",
"Anymail Sender <[email protected]>", ["[email protected]"])HTML, attachments, extra headers and inline images use the standard classes plus one Anymail helper. The helper attaches a file and returns the content id you reference in the HTML:
from django.core.mail import EmailMultiAlternatives
from anymail.message import attach_inline_image_file
msg = EmailMultiAlternatives(
subject="Please activate your account",
body="Click to activate your account: https://example.com/activate",
from_email="Example <[email protected]>",
to=["New User <[email protected]>", "[email protected]"],
reply_to=["Helpdesk <[email protected]>"])
logo_cid = attach_inline_image_file(msg, "/path/to/logo.jpg")The returned `logo_cid` goes into the HTML as a cid: reference. This is the pattern that removes the usual hand-rolled base64 data URI out of your templates.
Anymail also handles "batch transactional" sends by delegating to the ESP's own merge and template features, and it works around Unicode handling on providers that do not get it right. That workaround layer is quiet, and it is one of the reasons to prefer a library over calling an ESP directly.
Unicode and inbound mail: where the abstraction is doing the work
The README lists full support for Unicode characters everywhere email allows them, with workarounds where providers fall short. Address display names, subjects and headers are exactly the places where providers disagree, and a project that hits those cases by hand ends up with conditionals per provider that Anymail would have absorbed.
Inbound is the mirror image. Rather than parsing each provider's inbound webhook payload yourself, Anymail gives you a normalized message object with simplified access to attachments and the rest of the inbound content. The cost is that you still configure a webhook endpoint on the provider's dashboard, and the URL and signature verification live in the documentation rather than in the README.
If you never receive inbound mail, this capability is irrelevant to you. If you do, it is usually the part of your mail stack that would otherwise be bespoke code with no tests.
Releases are frequent and follow semantic versioning
Anymail is not archived, the last push was on 2026-09-23, and the release history is dense: v15.0 on 2026-04-18, v15.1 on 2026-07-30 and v15.2 on 2026-09-05. Three listed releases precede a push one day after v15.2.
The project states that releases follow semantic versioning, so a major version bump signals a breaking change, and the MAILERS versus EMAIL_BACKEND split is the pattern to watch. It also states that Anymail maintains compatibility with all Django versions in mainstream or extended support, plus usually a few older ones, and that even older Django versions may be covered by an Anymail extended support release.
The licence is BSD, declared in pyproject.toml as BSD-3-Clause with the LICENSE file at the repository root. This is a licensing note rather than legal advice. For a package that sits between your application and an external service, the more relevant cost is the upgrade path: an ESP that changes its API becomes an Anymail release you need, and SendGrid shows what happens when a provider stops being tested.
Anymail against writing the provider client yourself
The honest alternative is not another library, it is the provider's own Python SDK plus Django's SMTP or a custom backend. That route gives you the full API surface of one provider, including features Anymail has not normalised, and removes a layer between you and the vendor.
What it costs you is everything portable. Tags, metadata, tracking, inline images, inbound parsing, Unicode edge cases and status webhooks each become your code. For one provider and one message type, that is a defensible trade. Once you send account activation, billing receipts and password resets, or you add a second provider, the abstraction pays for itself.
A third option worth naming is Django's console or file based backends plus a real SMTP relay, which removes the ESP from the picture entirely but also removes per-provider delivery data, tracking and inbound.
Anymail's bet is that the portability is worth the indirection. For a Django team that intends to change ESPs eventually, that is usually the correct bet.
Editorial conclusion
Use Anymail when a Django project sends through a transactional ESP and you want provider independent code, webhook driven status signals and inbound handling. Do not use it for campaigns or newsletters, which ESP merge features do not replace, and treat SendGrid as unsupported because the README marks it no longer tested. Verify first that your chosen ESP has an Anymail backend you actually need, then check the Django version split, since Django 6.1 uses MAILERS while 6.0 and earlier use EMAIL_BACKEND with ANYMAIL.
Frequently asked questions
Which ESPs does Anymail support?
The README lists Amazon SES, Brevo, MailerSend, Mailgun, Mailjet, Mailtrap, Mandrill, Postal, Postmark, Resend, Scaleway TEM, SendGrid, SparkPost and Unisender Go. SendGrid is annotated as no longer tested.
How do I install Anymail for a specific provider?
Install from PyPI with the ESP name in brackets, for example pip install "django-anymail[mailgun]". The extra pulls in whatever extra packages that ESP needs, which is why it is worth naming yours explicitly.
What is the difference between MAILERS and EMAIL_BACKEND in Anymail?
In Django 6.1 or later you configure Anymail through the MAILERS dict with BACKEND and OPTIONS keys. In Django 6.0 or earlier you set EMAIL_BACKEND to the anymail backend path and put provider credentials in an ANYMAIL dict.
Does Anymail work with Django's normal send_mail function?
Yes. Once anymail is in INSTALLED_APPS and a backend is configured, Django's built-in email functions including send_mail and EmailMultiAlternatives send through the chosen ESP without modification.
What licence is Anymail released under?
Anymail is released under the BSD licence, declared as BSD-3-Clause in pyproject.toml with the LICENSE file in the repository root. This is a licensing note and not legal advice.
Does Anymail handle receiving inbound email?
Yes. The README states that Anymail supports inbound messages received through the ESP's webhooks, with simplified, portable access to attachments and other inbound content.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/anymail-django-anymail)Was this useful?