duva-django
Django email backend for Duva, the transactional email API hosted in Canada. Use send_mail(),
EmailMessage and EmailMultiAlternatives as usual; the messages go out through Duva.
Requires Python 3.10+ and Django 5.2 or later (including the MAILERS setting of Django 6.1). Built on
duva-mail.
pip install duva-django
# or: uv add duva-django
You also need a Duva API key and a verified domain (documentation).
Configure
Django 5.2 and later, with EMAIL_BACKEND:
# settings.py
EMAIL_BACKEND = "duva_django.EmailBackend"
DUVA_API_KEY = os.environ["DUVA_API_KEY"]
DUVA_DOMAIN = "example.com"
DEFAULT_FROM_EMAIL = "Example <notifications@example.com>"
Django 6.1 and later, with MAILERS (EMAIL_BACKEND is deprecated there):
MAILERS = {
"default": {
"BACKEND": "duva_django.EmailBackend",
"OPTIONS": {"api_key": os.environ["DUVA_API_KEY"], "domain": "example.com"},
},
}
Options (OPTIONS, or the setting of the same name in capitals prefixed by DUVA_): api_key, domain, base_url
(default https://api.duva.ca), timeout (seconds, default 10), max_retries (default 2). DUVA_API_KEY and
DUVA_DOMAIN can also come from the environment. The From address must belong to the configured domain.
What is sent
| Django | Duva |
|---|---|
from_email, to, cc, bcc, reply_to |
from, to, cc, bcc, reply_to |
Plain body, content_subtype = "html", attach_alternative(html, "text/html") |
text, html |
attach() files and inline images (Content-ID) |
attachments (inline ones keep their content_id) |
message.tags = [...] |
tags |
message.metadata = {...} |
metadata |
List-Unsubscribe, List-Unsubscribe-Post, List-Id, In-Reply-To, References, Auto-Submitted, Precedence, Importance, Feedback-ID, and X-* headers in headers |
headers |
(tags and metadata follow the django-anymail convention, so code written for it keeps working.)
Notes:
- Other headers are not forwarded (Duva rejects them), nor are
X-Duva*,X-Kumo*,X-Tenant*andX-Campaign*. - A display name Duva refuses (it contains
@, quotes,<>, control characters or an encoded word=?…?=) is dropped; the address is kept. - Delivery is asynchronous: a successful send means Duva accepted the message, not that it was delivered. After a send,
message.duva_resultholds theduva.SendMessageResult(id,status,replayed). Use Duva's webhooks or events for the outcome. - A Duva error (
duva.DuvaErrorand its subclasses) is raised, unlessfail_silently=True, in which case the message is not counted in the return value ofsend_mail(). A missing key or domain raisesImproperlyConfigured.
Idempotency
A retried task builds a new message, so it would be sent twice. Set a stable key for it (an order ID, for example):
EmailMessage(
subject,
body,
from_email,
[to],
headers={"X-Idempotency-Key": f"order-{order.id}"},
).send()
The header is read by the backend and never sent. Duva ignores a second message with the same key.
Testing your application
Django's test runner swaps the backend for the in-memory one, so mail.outbox works as usual. To exercise this backend
without a network, give it a client built on a fake HTTP transport:
import duva, httpx
from duva_django import EmailBackend
client = duva.Duva(
api_key="dv_test",
domain="example.com",
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
)
EmailBackend(client=client).send_messages([message])
License
MIT.
Metadata
Release files for duva-django 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| duva_django-0.1.0.tar.gz | 10.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| duva_django-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 18.3 kB
Release files / duva_django-0.1.0.tar.gz
| Download URL | duva_django-0.1.0.tar.gz |
|---|---|
| Size | 10.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
37ae03cdfc694e6d900aa03bd44553b689eaaac9eaa8747f524e65f8dc8d9348
|
|
BLAKE2b-256 checksum How to use checksums |
a8f669509b3bcdca39cd7c60e585b615cdc1a8bed6c5ab3a2833a3adb6c5e697
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.
Transparency logRelease files / duva_django-0.1.0-py3-none-any.whl
| Download URL | duva_django-0.1.0-py3-none-any.whl |
|---|---|
| Size | 7.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f2fa9f04824262ed65f780196c3e2c2cdd469cfeaa1c1f78cd7018f4329ea19c
|
|
BLAKE2b-256 checksum How to use checksums |
5377deddd945d2e823c8de3b9f85c49fd2f44d1d2aa7c1bd649fd31525576113
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.
Transparency log