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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_comms-0.2.0.tar.gz (111.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_comms-0.2.0-py3-none-any.whl (59.0 kB view details)

Uploaded Python 3

File details

Details for the file django_comms-0.2.0.tar.gz.

File metadata

  • Download URL: django_comms-0.2.0.tar.gz
  • Upload date:
  • Size: 111.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for django_comms-0.2.0.tar.gz
Algorithm Hash digest
SHA256 3ceb11358638bda16eab69a57fb9f12d3c1f1e6daecba6ad6a2a161c578eb7ee
MD5 7b6e77b999935bede5e353299ad21576
BLAKE2b-256 2fa2b8ea0a07946b11e3350d8991417fb6a8e0efe20fbc5cd276e1f15dd63ba1

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_comms-0.2.0.tar.gz:

Publisher: ci.yml on GaretJax/django-comms

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_comms-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: django_comms-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 59.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for django_comms-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bf56383354b977c874b7d7dc036cdef2aa053b08b57f5ac60de351a8882ae727
MD5 cfaa83bc26e95948a4b4d912a5bbc819
BLAKE2b-256 f7baf5cc03dd101072e43bafe32d4fe4ef7cf7ae1b24db1dada6dfbc243d36aa

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_comms-0.2.0-py3-none-any.whl:

Publisher: ci.yml on GaretJax/django-comms

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.0

2 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