Skip to main content

django-comms

Reusable email communications, conversations, subscriptions, and delivery tracking for Django.

The package currently supports Mailgun, Django-configured email backends, SMTP, inbound email, delivery events, attachments, scheduled delivery through Celery, throttled sending, and PostgreSQL-backed subscription history.

Requirements

  • Python 3.11 or newer
  • Django 5.2 or 6.0
  • PostgreSQL
  • Celery
  • libmagic

Installation

uv add django-comms

Add the application:

INSTALLED_APPS = [
    # Django applications...
    "django_comms",
]

Include the Mailgun webhook URLs:

from django.urls import include, path

urlpatterns = [
    path("comms/", include("django_comms.urls")),
]

Apply the migrations:

python manage.py migrate

Mailboxes and backends

A mailbox scopes addresses, conversations, topics, and delivery backends. Create one default mailbox when callers should be able to omit mailbox=:

from django_comms.models import Mailbox

mailbox = Mailbox.objects.create(
    name="Main mailbox",
    identifier="main",
    default_from_name="Example Organization",
    default_from_email="hello@example.com",
    is_default=True,
)

Configure Mailgun through MessagingBackend.config:

from django_comms.constants import MessagingBackendClass
from django_comms.models import MessagingBackend

MessagingBackend.objects.create(
    mailbox=mailbox,
    label="Mailgun",
    identifier="mailgun",
    backend_class_path=MessagingBackendClass.MAILGUN,
    is_default=True,
    config={
        "base_url": "https://api.eu.mailgun.net/v3/example.com",
        "validation_url": "https://api.mailgun.net/v4/address/validate",
        "api_key": "...",
        "signing_key": "...",
    },
)

The webhook endpoints are then:

/comms/mailgun/<backend-id>/events/
/comms/mailgun/<backend-id>/inbound/

A Django backend uses the configured EMAIL_BACKEND and email settings:

MessagingBackend.objects.create(
    mailbox=mailbox,
    label="Django email",
    identifier="django-email",
    backend_class_path=MessagingBackendClass.DJANGO,
)

SMTP is also supported for outbound delivery. Configure it with the SMTP server connection details:

MessagingBackend.objects.create(
    mailbox=mailbox,
    label="SMTP",
    identifier="smtp",
    backend_class_path=MessagingBackendClass.SMTP,
    is_default=True,
    config={
        "host": "smtp.example.com",
        "port": 587,
        "username": "smtp-user",
        "password": "...",
        "use_tls": True,
        "use_ssl": False,
        "timeout": 30,
    },
)

SMTP submission provides synchronous acceptance only. It does not provide Mailgun delivery, open, click, bounce, complaint, or inbound-mail events.

Sending can be throttled per backend. A null throttle_limit disables throttling; deferred messages are picked up by the scheduled dispatcher:

from datetime import timedelta

MessagingBackend.objects.create(
    mailbox=mailbox,
    label="Throttled SMTP",
    identifier="throttled-smtp",
    backend_class_path=MessagingBackendClass.SMTP,
    throttle_limit=10,
    throttle_period=timedelta(minutes=1),
    config={"host": "smtp.example.com"},
)

Click tracking

Click tracking is disabled by default. It can be enabled globally or overridden at the mailbox, backend, or individual message level. None means inherit from the upper level:

DJANGO_COMMS_CLICK_TRACKING_ENABLED = True
DJANGO_COMMS_PUBLIC_URL = "https://example.com"

Add the tracking endpoints separately from the other communications URLs:

urlpatterns = [
    path("go/", include("django_comms.tracking_urls")),
    path("api/go/", include("django_comms.tracking_api_urls")),
]

The rendered HTML and plaintext bodies are rewritten after rendering. Each eligible HTTP(S) destination is replaced with an opaque message-level token. For a frontend route, configure the injected URL explicitly:

DJANGO_COMMS_TRACKING_LINK = {
    "type": "format",
    "format": "https://frontend.example/go/{token}",
}

The default configuration uses the Django-resolved django_comms_tracking:click URL. The API endpoint resolves the same token and returns the original destination for a frontend to navigate to. A frontend should call the API from the browser if the original visitor IP, referrer, and user agent are to be captured.

Preparing email

Given emails/invoice.html and emails/invoice.txt:

from django_comms import prepare_email

prepared = prepare_email(
    "invoice:123",
    mailbox=mailbox,  # Optional when a default mailbox exists.
    recipients=["Alice <alice@example.com>"],
    subject="Your invoice",
    template="emails/invoice",
    context={"invoice": invoice},
)

Choose one persistence operation:

message = prepared.persist()  # Store without dispatching.
message = prepared.send()  # Store and dispatch through Celery.
message = prepared.schedule(timestamp)  # Store for scheduled dispatch.

A manually dispatched message can bypass backend throttling:

from django_comms import tasks

tasks.dispatch_message.delay(message.pk, ignore_throttling=True)

Configure Celery beat to dispatch due messages:

CELERY_BEAT_SCHEDULE = {
    "django-comms-dispatch": {
        "task": "django_comms.tasks.dispatch_scheduled_messages",
        "schedule": 60,
    },
}

Plaintext templates render with autoescape=False. A custom Django template engine alias can be supplied for either format:

template = {
    "html": ("emails/invoice.html", "email_html"),
    "plaintext": ("emails/invoice.txt", "email_plaintext"),
}

Contact model

Email addresses and subscriptions refer to the configured contact model. It defaults to AUTH_USER_MODEL.

Set a different model before the first migration:

DJANGO_COMMS_CONTACT_MODEL = "contacts.Contact"
DJANGO_COMMS_CONTACT_ADAPTER = "contacts.adapters.ContactAdapter"

Adapters derive values used by the admin and subscription exports:

from django_comms.adapters import ContactAdapter


class ContactAdapter(ContactAdapter):
    search_fields = ("display_name", "primary_email")

    def get_email(self, contact):
        return contact.primary_email

    def get_first_name(self, contact):
        return contact.given_name

    def get_last_name(self, contact):
        return contact.family_name

Changing the contact model after applying the initial migration is not supported.

Storage

Attachments use the default Django storage unless an alias is configured:

DJANGO_COMMS_STORAGE_ALIAS = "private"

STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
    "private": {
        "BACKEND": "storages.backends.s3.S3Storage",
        "OPTIONS": {
            "bucket_name": "private-files",
            "default_acl": "private",
        },
    },
}

The storage setting reference is preserved in migrations.

Other settings

# Suppress outbound delivery while marking messages as dispatched.
# Defaults to DEBUG when omitted. Mailgun uses its test mode; SMTP does not
# connect to the server.
DJANGO_COMMS_TEST_MODE = True

# Namespace used by package-local admin URL helpers.
DJANGO_COMMS_ADMIN_SITE_NAME = "admin"

For a custom AdminSite, call django_comms.admin.register_admin(site).

Development

See docs/development.md for the branching, commit, changelog, review, testing, and merging workflow.

The short version is:

uv sync
uv run ruff format .
uv run ruff check .
uv run pytest

Tests require PostgreSQL.

Metadata

Release files for django-comms 0.2.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 django-comms 0.2.0
File Size Uploaded
django_comms-0.2.0.tar.gz 111.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-comms 0.2.0
File Interpreter ABI Platform
django_comms-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 170.7 kB

Release files / django_comms-0.2.0.tar.gz

Download URL django_comms-0.2.0.tar.gz
Size 111.7 kB
Tags Source
SHA-256 checksum
How to use checksums
3ceb11358638bda16eab69a57fb9f12d3c1f1e6daecba6ad6a2a161c578eb7ee
BLAKE2b-256 checksum
How to use checksums
2fa2b8ea0a07946b11e3350d8991417fb6a8e0efe20fbc5cd276e1f15dd63ba1
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 Aug 4, 2026.

Transparency log

Release files / django_comms-0.2.0-py3-none-any.whl

Download URL django_comms-0.2.0-py3-none-any.whl
Size 59.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bf56383354b977c874b7d7dc036cdef2aa053b08b57f5ac60de351a8882ae727
BLAKE2b-256 checksum
How to use checksums
f7baf5cc03dd101072e43bafe32d4fe4ef7cf7ae1b24db1dada6dfbc243d36aa
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 Aug 4, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

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