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)
| File | Size | Uploaded | |
|---|---|---|---|
| django_comms-0.2.0.tar.gz | 111.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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