Skip to content

Commit 61109a3

Browse files
authored
Merge pull request #26 from ActiveCampaign/feature/django-backend
feat: add Django email backend, supporting Django 4.2 LTS through 6.1
2 parents a509970 + 56a3892 commit 61109a3

25 files changed

Lines changed: 1324 additions & 7 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
name: Bug report
3+
about: Report something that isn't working as expected
4+
title: ""
5+
labels: bug
6+
assignees: ""
7+
---
8+
9+
**Describe the bug**
10+
A clear description of what's wrong.
11+
12+
**To reproduce**
13+
Minimal code sample that reproduces the issue:
14+
15+
```python
16+
17+
```
18+
19+
**Expected behavior**
20+
What you expected to happen instead.
21+
22+
**Environment**
23+
- `postmark-python` version:
24+
- Python version:
25+
- OS:
26+
27+
**Additional context**
28+
Anything else relevant (stack trace, `X-Request-Id` from an exception, etc.).

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
blank_issues_enabled: true
2+
contact_links:
3+
- name: Security vulnerability
4+
url: https://github.com/ActiveCampaign/postmark-python/security/policy
5+
about: Please report security vulnerabilities privately per SECURITY.md, not as a public issue.
6+
- name: Postmark API / account support
7+
url: mailto:support@postmarkapp.com
8+
about: For questions about the Postmark service itself, rather than this SDK.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
name: Feature request
3+
about: Suggest an addition or improvement to this SDK
4+
title: ""
5+
labels: enhancement
6+
assignees: ""
7+
---
8+
9+
**What are you trying to do?**
10+
Describe the use case this would unlock.
11+
12+
**Proposed solution**
13+
What you'd like the SDK to support, e.g. a new method, client, or option.
14+
15+
**Alternatives considered**
16+
Any workarounds you're using today, or other approaches you considered.
17+
18+
**Additional context**
19+
Links to relevant [Postmark API docs](https://postmarkapp.com/developer), if applicable.

.github/workflows/tests.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,42 @@ jobs:
4646
run: |
4747
poetry run pytest --cov=postmark --cov-report=xml --cov-report=term --cov-fail-under=85
4848
49+
django-tests:
50+
runs-on: ubuntu-latest
51+
strategy:
52+
matrix:
53+
# Currently supported Django release series. The main `test` job above
54+
# already exercises tests/django_backend/ against the pinned dev version
55+
# (Django 5.2) across every supported Python version; this job additionally
56+
# checks the Django backend against the rest of the support matrix.
57+
django-version: ["4.2", "5.2", "6.0", "6.1"]
58+
59+
steps:
60+
- uses: actions/checkout@v7
61+
62+
- name: Set up Python
63+
uses: actions/setup-python@v7
64+
with:
65+
# 3.12 is the one Python version every entry in the matrix supports:
66+
# Django 4.2 added 3.12 support in 4.2.8; Django 6.0/6.1 require 3.12+.
67+
python-version: "3.12"
68+
69+
- name: Install Poetry
70+
uses: snok/install-poetry@v1
71+
with:
72+
version: 2.4.1
73+
virtualenvs-create: true
74+
virtualenvs-in-project: true
75+
76+
- name: Install dependencies
77+
run: poetry install --no-interaction
78+
79+
- name: Install Django ${{ matrix.django-version }}
80+
run: poetry run pip install "django~=${{ matrix.django-version }}.0"
81+
82+
- name: Run Django backend tests
83+
run: poetry run pytest tests/django_backend/ -v
84+
4985
lint:
5086
runs-on: ubuntu-latest
5187

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
77

88
---
99

10+
## [Unreleased]
11+
12+
### Added
13+
14+
- Django email backend (`postmark.django.EmailBackend`), gated behind the new `django` extra (`pip install postmark-python[django]`). Supports Django 4.2 LTS, 5.2 LTS, 6.0, and 6.1 — including Django 6.0's ["modern email API" change](https://docs.djangoproject.com/en/6.0/releases/6.0/#adoption-of-python-s-modern-email-api). The Postmark payload is built from `EmailMessage`'s high-level attributes rather than `EmailMessage.message()`, so this backend is unaffected by that change.
15+
- `postmark.django.PostmarkEmailMessage` / `PostmarkEmailMultiAlternatives` / `PostmarkEmailMixin` for setting `tag`, `metadata`, and `message_stream`.
16+
- `postmark.django.pre_send` / `post_send` / `on_exception` signals.
17+
- New settings: `POSTMARK_SERVER_TOKEN`, `POSTMARK_TEST_MODE`, `POSTMARK_TRACK_OPENS`, `POSTMARK_MESSAGE_STREAM`.
18+
- See the [Django Backend wiki page](https://github.com/ActiveCampaign/postmark-python/wiki/Django-Backend) and `examples/django/`.
19+
20+
---
21+
1022
## [0.3.7] - 2026-08-05
1123

1224
### Fixed

CONTRIBUTING.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Contributing
2+
3+
Thanks for considering a contribution to `postmark-python`.
4+
5+
## Getting started
6+
7+
```bash
8+
git clone https://github.com/ActiveCampaign/postmark-python.git
9+
cd postmark-python
10+
poetry install
11+
poetry run pre-commit install
12+
```
13+
14+
## Making a change
15+
16+
1. Fork the repository and create a feature branch off `main`.
17+
2. Make your change, keeping it scoped to a single concern.
18+
3. Add or update tests under `tests/` — the suite follows a one-file-per-feature layout (e.g. `test_templates.py`, `test_bounces.py`) that mirrors `postmark/models/`.
19+
4. Run the full check suite locally before opening a PR:
20+
21+
```bash
22+
poetry run pytest
23+
poetry run ruff check
24+
poetry run ruff format --check
25+
poetry run mypy postmark/
26+
poetry run pre-commit run --all-files
27+
```
28+
29+
CI enforces a minimum test coverage of 85% (`--cov-fail-under=85`), so new code needs tests to match.
30+
5. Update `CHANGELOG.md` under an `Unreleased` heading (Keep a Changelog format).
31+
6. Open a pull request describing the change and why it's needed.
32+
33+
## Reporting bugs and requesting features
34+
35+
Please open a [GitHub issue](https://github.com/ActiveCampaign/postmark-python/issues) using the appropriate template. For security vulnerabilities, follow the process in [SECURITY.md](SECURITY.md) instead of filing a public issue.
36+
37+
## Code style
38+
39+
- Formatting and linting are enforced by `ruff` (see `[tool.ruff]` in `pyproject.toml`).
40+
- Type hints are required; `mypy postmark/` must pass cleanly.
41+
- Request/response schemas use Pydantic v2 models under `postmark/models/<feature>/schemas.py`.

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,22 @@ client = postmark.ServerClient(os.environ["POSTMARK_SERVER_TOKEN"])
105105
await client.close()
106106
```
107107

108+
## Django
109+
110+
`postmark.django.EmailBackend` is a drop-in `EMAIL_BACKEND` for Django's `django.core.mail`, supporting Django 4.2 LTS through 6.1. Requires the `django` extra:
111+
112+
```bash
113+
pip install postmark-python[django]
114+
```
115+
116+
```python
117+
# settings.py
118+
EMAIL_BACKEND = "postmark.django.EmailBackend"
119+
POSTMARK_SERVER_TOKEN = "your-server-token"
120+
```
121+
122+
See the [Django Backend wiki page](https://github.com/ActiveCampaign/postmark-python/wiki/Django-Backend) and [`examples/django/`](examples/django/) for tags, metadata, attachments, and signals.
123+
108124
## Development
109125

110126
```bash
@@ -124,11 +140,7 @@ poetry run pre-commit run --all-files
124140

125141
## Contributing
126142

127-
1. Fork the repository
128-
2. Create a feature branch
129-
3. Add tests for your changes
130-
4. Ensure all checks pass (`poetry run pre-commit run --all-files`)
131-
5. Open a Pull Request
143+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, testing, and PR guidelines.
132144

133145
## Support
134146

examples/django/send_batch.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
Send several distinct messages in one call. django.core.mail.send_mass_mail
3+
takes tuples of (subject, message, from_email, recipient_list) and sends them
4+
through a single connection — the Django backend batches them into Postmark's
5+
send_batch API (up to 500 per request) rather than one request per message.
6+
7+
Run:
8+
poetry run python examples/django/send_batch.py
9+
python examples/django/send_batch.py # with venv active
10+
"""
11+
12+
import os
13+
14+
import django
15+
from django.conf import settings
16+
17+
try:
18+
from dotenv import load_dotenv
19+
20+
load_dotenv()
21+
except ImportError:
22+
pass
23+
24+
if not settings.configured:
25+
settings.configure(
26+
EMAIL_BACKEND="postmark.django.EmailBackend",
27+
POSTMARK_SERVER_TOKEN=os.environ["POSTMARK_SERVER_TOKEN"],
28+
)
29+
django.setup()
30+
31+
from django.core.mail import send_mass_mail # noqa: E402
32+
33+
SENDER = os.environ["POSTMARK_SENDER_EMAIL"]
34+
35+
sent_count = send_mass_mail(
36+
(
37+
("Batch 1", "Hello Receiver 1", SENDER, ["receiver1@example.com"]),
38+
("Batch 2", "Hello Receiver 2", SENDER, ["receiver2@example.com"]),
39+
)
40+
)
41+
42+
print(f"Sent: {sent_count}")

examples/django/send_simple.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
Send a single email through Postmark's Django backend.
3+
4+
Standalone script, not a full Django project — real projects configure
5+
settings.py once (see settings_snippet.py) and just call
6+
django.core.mail.send_mail(...) anywhere.
7+
8+
Run:
9+
poetry run python examples/django/send_simple.py
10+
python examples/django/send_simple.py # with venv active
11+
"""
12+
13+
import os
14+
15+
import django
16+
from django.conf import settings
17+
18+
try:
19+
from dotenv import load_dotenv
20+
21+
load_dotenv()
22+
except ImportError:
23+
pass
24+
25+
if not settings.configured:
26+
settings.configure(
27+
EMAIL_BACKEND="postmark.django.EmailBackend",
28+
POSTMARK_SERVER_TOKEN=os.environ["POSTMARK_SERVER_TOKEN"],
29+
)
30+
django.setup()
31+
32+
from django.core.mail import send_mail # noqa: E402
33+
34+
SENDER = os.environ["POSTMARK_SENDER_EMAIL"]
35+
36+
send_mail(
37+
subject="Hello from Postmark",
38+
message="Sent with postmark.django.EmailBackend.",
39+
from_email=SENDER,
40+
recipient_list=["receiver@example.com"],
41+
html_message="<p>Sent with <b>postmark.django.EmailBackend</b>.</p>",
42+
)
43+
44+
print("Sent.")
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
Send an email with an attachment through Postmark's Django backend.
3+
4+
Attachment content is base64-encoded automatically — pass the raw
5+
bytes/str you'd normally give EmailMessage.attach(), same as any other
6+
Django email backend.
7+
8+
Run:
9+
poetry run python examples/django/send_simple_with_attachment.py
10+
python examples/django/send_simple_with_attachment.py # with venv active
11+
"""
12+
13+
import os
14+
15+
import django
16+
from django.conf import settings
17+
18+
try:
19+
from dotenv import load_dotenv
20+
21+
load_dotenv()
22+
except ImportError:
23+
pass
24+
25+
if not settings.configured:
26+
settings.configure(
27+
EMAIL_BACKEND="postmark.django.EmailBackend",
28+
POSTMARK_SERVER_TOKEN=os.environ["POSTMARK_SERVER_TOKEN"],
29+
)
30+
django.setup()
31+
32+
from django.core.mail import EmailMessage # noqa: E402
33+
34+
SENDER = os.environ["POSTMARK_SENDER_EMAIL"]
35+
36+
message = EmailMessage(
37+
subject="Your report and resources",
38+
body="Please find your report attached.",
39+
from_email=SENDER,
40+
to=["receiver@example.com"],
41+
)
42+
message.attach("report.txt", "Q3 sales are up 12%.", "text/plain")
43+
44+
with open("/path/to/book.pdf", "rb") as f:
45+
message.attach("book.pdf", f.read(), "application/pdf")
46+
47+
message.send()
48+
49+
print("Sent.")

0 commit comments

Comments
 (0)