Skip to content

Add MailKite ESP backend - #482

Closed
bucabay wants to merge 1 commit into
anymail:mainfrom
bucabay:add-mailkite-backend
Closed

bucabay wants to merge 1 commit into
anymail:mainfrom
bucabay:add-mailkite-backend

Conversation

@bucabay

@bucabay bucabay commented Aug 3, 2026

Copy link
Copy Markdown

What this adds

A new MailKite (mailkite.dev) backend, modeled closely on the existing Resend backend — MailKite's send API is JSON/shape-compatible with Resend's (POST JSON, Authorization: Bearer, {from,to,subject,html,text,…}), so the contribution follows the same structure you already accept and maintain.

MailKite is an inbound-first developer email platform: it sends transactional mail, and also receives email (parsed body + SPF/DKIM/DMARC verdict) delivered to a signed webhook. This PR implements the send side.

Anymail features supported

MailKite's API actually exposes a few things Resend's doesn't, so the backend supports more Anymail features than the Resend one:

Feature MailKite (Resend, for reference)
template_id + merge_global_data (server-side templates, {{merge_tags}}) ✅ ❌
track_opens / track_clicks (per-message override) ✅ ❌
send_at (scheduledAt) ✅ ✅
merge_metadata / merge_headers (batch send, POST /v1/send/batch) ✅ ✅
metadata, tags (via X-Metadata / X-Tags headers) ✅ ✅
attachments (content/filename/contentType) ✅ ✅

Documented limitations (on the ESP page, raised as AnymailUnsupportedFeature)

  • No outbound tracking webhooks — MailKite does not currently emit delivery/open/click/bounce events as webhooks, so AnymailTrackingEvent signals are not available. (Open/click tracking still works in MailKite's own dashboard, and per-message track_opens/track_clicks are supported.)
  • No inbound via Anymail signals — MailKite does inbound (its headline feature), but delivers to the user's own webhook URL in its own payload format, so it isn't wired through AnymailInboundEvent.
  • No inline attachments (Content-ID) — MailKite's send API has no cid field, so inline images raise rather than silently degrading to regular attachments.
  • Single replyTo only — MailKite takes one reply-to string; multiple reply_to addresses raise.
  • No envelope_sender.

How to configure

# settings.py
EMAIL_BACKEND = "anymail.backends.mailkite.EmailBackend"

ANYMAIL = {
    "MAILKITE_API_KEY": "mk_live_...",   # or MAILKITE_API_KEY / ANYMAIL_MAILKITE_API_KEY
}
# Optional: ANYMAIL = { ..., "MAILKITE_API_URL": "https://api.mailkite.dev/" }

No extra install — MailKite uses only Anymail's existing requests dependency (hence mailkite = [] in optional-dependencies, no [mailkite] extra).

How I tested it

  • tests/test_mailkite_backend.py — 45 mocked tests covering standard Django email features and all the Anymail additions above, including the batch path (empty merge_data, merge_metadata, merge_headers) and a per-recipient rejection (MailKite reports batch failures per-recipient; verified they surface as "rejected").
  • tests/test_mailkite_integration.py — live tests against the real API, guarded by ANYMAIL_TEST_MAILKITE_API_KEY / ANYMAIL_TEST_MAILKITE_DOMAIN (skip cleanly without them).
  • Ran the full mocked suite locally (Django 6.0, Python 3.13): python runtests.py tests.test_mailkite_backend tests.test_mailkite_integration → 49 passed (4 skipped). The Resend backend tests still pass unchanged.
  • pre-commit (black, isort, flake8, doc8, pygrep-hooks) is clean on every file in this PR.

I did not run the live integration tests (I don't want to charge a real MailKite account from a PR). If you'd like, I can run them against a MailKite test domain before merge, or you can run them in CI with credentials.

Files

  • anymail/backends/mailkite.py
  • tests/test_mailkite_backend.py, tests/test_mailkite_integration.py
  • docs/esps/mailkite.rst (+ toctree, README, feature-matrix column, pyproject keywords/extra, changelog)

Maintenance

Happy to maintain this backend going forward — keeping it in step with MailKite's send API as it evolves, and adding outbound tracking webhook support (→ AnymailTrackingEvent) and Anymail inbound signal support if/when MailKite exposes the corresponding surfaces. I've read CONTRIBUTING and followed the existing ESP-backend pattern; happy to rework anything that doesn't match your conventions.

(Heads-up on the feature-matrix support-status row: I marked MailKite Unsupported per the footnote — Anymail doesn't have a MailKite account for live integration tests yet. Adjust to Limited/Full as you see fit once you have access.)

Add a MailKite (mailkite.dev) backend. MailKite is an inbound-first
developer email platform: it sends transactional mail via POST /v1/send
(a JSON API) and also *receives* email, delivering the parsed body plus an
SPF/DKIM/DMARC auth verdict to a signed webhook.

The backend implements the send side, modeled on the existing Resend
backend (MailKite's send API is JSON/shape-compatible), and supports the
Anymail features MailKite's API exposes that Resend's does not:

- per-message trackOpens / trackClicks overrides
- server-side templates: templateId + templateData (merge_global_data)
- batch sending (POST /v1/send/batch) with per-recipient merge_metadata
  and merge_headers; per-recipient failures surface as "rejected"
- metadata and tags via X-Metadata / X-Tags headers
- scheduled sending (scheduledAt)

Not wired up (documented as limitations on the ESP page):
- outbound tracking webhooks (MailKite does not emit them)
- inbound signals (MailKite delivers inbound mail directly to the user's
  own webhook URL, in its own payload format)
- inline attachments (MailKite's send API has no Content-ID field)
- multiple reply_to (MailKite takes a single replyTo string)

Includes:
- anymail/backends/mailkite.py
- tests/test_mailkite_backend.py (45 mocked tests)
- tests/test_mailkite_integration.py (live tests, skipped without keys)
- docs/esps/mailkite.rst
- README, pyproject, feature matrix, and changelog entries

All backend tests pass; pre-commit (black/isort/flake8/doc8/pygrep) clean.
@bucabay

bucabay commented Aug 3, 2026

Copy link
Copy Markdown
Author

Apologies — this duplicates #481, which is the maintained MailKite backend PR and the only one going forward (as promised there). This one was opened in error by our tooling; we've fixed the process so it can't happen again. Closing.

@bucabay bucabay closed this Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant