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

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page