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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ceb11358638bda16eab69a57fb9f12d3c1f1e6daecba6ad6a2a161c578eb7ee
|
|
| MD5 |
7b6e77b999935bede5e353299ad21576
|
|
| BLAKE2b-256 |
2fa2b8ea0a07946b11e3350d8991417fb6a8e0efe20fbc5cd276e1f15dd63ba1
|
Provenance
The following attestation bundles were made for django_comms-0.2.0.tar.gz:
Publisher:
ci.yml on GaretJax/django-comms
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_comms-0.2.0.tar.gz -
Subject digest:
3ceb11358638bda16eab69a57fb9f12d3c1f1e6daecba6ad6a2a161c578eb7ee - Sigstore transparency entry: 2341604576
- Sigstore integration time:
-
Permalink:
GaretJax/django-comms@39e5a2658e0b6ce8f718a08acf56d1b7ef381144 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/GaretJax
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@39e5a2658e0b6ce8f718a08acf56d1b7ef381144 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bf56383354b977c874b7d7dc036cdef2aa053b08b57f5ac60de351a8882ae727
|
|
| MD5 |
cfaa83bc26e95948a4b4d912a5bbc819
|
|
| BLAKE2b-256 |
f7baf5cc03dd101072e43bafe32d4fe4ef7cf7ae1b24db1dada6dfbc243d36aa
|
Provenance
The following attestation bundles were made for django_comms-0.2.0-py3-none-any.whl:
Publisher:
ci.yml on GaretJax/django-comms
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_comms-0.2.0-py3-none-any.whl -
Subject digest:
bf56383354b977c874b7d7dc036cdef2aa053b08b57f5ac60de351a8882ae727 - Sigstore transparency entry: 2341604587
- Sigstore integration time:
-
Permalink:
GaretJax/django-comms@39e5a2658e0b6ce8f718a08acf56d1b7ef381144 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/GaretJax
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@39e5a2658e0b6ce8f718a08acf56d1b7ef381144 -
Trigger Event:
push
-
Statement type: