Metadata-Version: 2.4
Name: django-comms
Version: 0.1.0
Summary: Reusable email communications, conversations, and delivery tracking for Django
Project-URL: Homepage, https://github.com/GaretJax/django-comms
Project-URL: Repository, https://github.com/GaretJax/django-comms
Project-URL: Issues, https://github.com/GaretJax/django-comms/issues
Author-email: Jonathan <jonathan@stoppani.name>
License-Expression: MIT
License-File: LICENSE
Keywords: celery,django,email,mailgun,smtp
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.11
Requires-Dist: attrs
Requires-Dist: beautifulsoup4
Requires-Dist: bleach[css]
Requires-Dist: celery
Requires-Dist: django-admin-autocomplete-filter
Requires-Dist: django-adminutils
Requires-Dist: django-import-export
Requires-Dist: django<7,>=5.2
Requires-Dist: lxml
Requires-Dist: psycopg[binary]
Requires-Dist: python-magic
Requires-Dist: requests
Description-Content-Type: text/markdown

# 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

```bash
uv add django-comms
```

Add the application:

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

Include the Mailgun webhook URLs:

```python
from django.urls import include, path

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

Apply the migrations:

```bash
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=`:

```python
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`:

```python
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:

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

A Django backend uses the configured `EMAIL_BACKEND` and email settings:

```python
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:

```python
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:

```python
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"},
)
```

## Preparing email

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

```python
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:

```python
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:

```python
from django_comms import tasks

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

Configure Celery beat to dispatch due messages:

```python
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:

```python
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:

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

Adapters derive values used by the admin and subscription exports:

```python
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:

```python
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

```python
# 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.
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

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

Tests require PostgreSQL. To test a custom contact model separately:

```bash
DJANGO_SETTINGS_MODULE=tests.custom_settings \
    uv run pytest custom_contact_tests
```
