Skip to main content

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* and X-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_result holds the duva.SendMessageResult (id, status, replayed). Use Duva's webhooks or events for the outcome.
  • A Duva error (duva.DuvaError and its subclasses) is raised, unless fail_silently=True, in which case the message is not counted in the return value of send_mail(). A missing key or domain raises ImproperlyConfigured.

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)

Source distribution for duva-django 0.1.0
File Size Uploaded
duva_django-0.1.0.tar.gz 10.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for duva-django 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page