Guides
Framework integration
MailingKit does not depend on any web framework. The pattern is the same everywhere: create one client when the application starts, and use it wherever you need to send.
FastAPI and other async apps
from contextlib import asynccontextmanager
from fastapi import FastAPI
from mailingkit import MailingKit
mail = MailingKit.from_env(templates="templates")
@asynccontextmanager
async def lifespan(app: FastAPI):
yield
await mail.aclose() # releases provider connections
app = FastAPI(lifespan=lifespan)
@app.post("/signup")
async def signup(name: str, email: str):
# ... create the user ...
await mail.send_template("welcome", {"name": name}, to=email, idempotency_key=f"welcome:{email}")
return {"ok": True}The same works for Starlette, Litestar, Quart, aiohttp and any other asyncio code. A complete example with error handling is in examples/fastapi.
Django
MailingKit ships a Django email backend. Configure it once and every email Django sends, including
your send_mail() calls, password resets and admin error emails, goes through
MailingKit's validation, providers and retries. No application code changes.
# Django 6.1 and newer
MAILERS = {
"default": {
"BACKEND": "mailingkit.contrib.django.EmailBackend",
# Optional: share a client you configured yourself.
# "OPTIONS": {"client": "myproject.mail.mail"},
},
}
# Older Django
EMAIL_BACKEND = "mailingkit.contrib.django.EmailBackend"
# MAILINGKIT_CLIENT = "myproject.mail.mail"
Without a client setting, the backend uses MailingKit.from_env(). Django's
placeholder DEFAULT_FROM_EMAIL (webmaster@localhost) is ignored so
MailingKit's own default sender applies.
For templates and idempotency, call MailingKit directly from views or tasks with the blocking client:
from mailingkit import SyncMailingKit
mail = SyncMailingKit.from_env(templates="templates")from myproject.mail import mail
def approve_leave(request, request_id):
leave = LeaveRequest.objects.get(pk=request_id)
leave.approve()
mail.send_template(
"leave-approved",
{"name": leave.employee.first_name, "dates": leave.dates_display},
to=leave.employee.email,
idempotency_key=f"leave-approved:{leave.pk}",
)
...Django is never a dependency of MailingKit. The backend module is only imported when Django loads it.
Celery, RQ and other workers
Use SyncMailingKit. It runs MailingKit's async core on a private event loop in a
background thread, so it is safe from any worker thread or process, even one that has its own
event loop running.
from celery import shared_task
from mailingkit import ProviderError, SyncMailingKit
mail = SyncMailingKit.from_env(templates="templates")
@shared_task(bind=True, max_retries=5)
def send_receipt(self, order_id, email, total):
try:
mail.send_template(
"receipt",
{"order_id": order_id, "total": total},
to=email,
idempotency_key=f"receipt:{order_id}",
)
except ProviderError as error:
if error.retryable:
raise self.retry(exc=error, countdown=60)
raiseThe idempotency key makes Celery's own retries safe: if the email went out before the worker crashed, a retry in the same process returns the earlier result. Use a shared idempotency store across workers.
Scripts and CLIs
from mailingkit import SyncMailingKit
with SyncMailingKit.from_env() as mail:
mail.send(to="ops@example.com", subject="Nightly export finished", text="All 12 files uploaded.")