Python library · v0.1.0
Transactional email, without the plumbing.
MailingKit is a drop-in mailing layer for Python applications. Your code calls one small, typed API. MailingKit renders the template, validates the message, retries the hiccups and talks to whichever provider you choose.
from mailingkit import MailingKit
mail = MailingKit.from_env()
await mail.send_template(
"welcome",
{"name": "Ada"},
to="ada@example.com",
idempotency_key="welcome:ada",
)MailingKit console provider: this email was NOT sent
From: Example App <no-reply@example.com>
To: ada@example.com
Subject: Welcome to Example, Ada
Where it sits
One layer between your application and every provider.
Your application knows MailingKit. MailingKit knows the provider. Switching from SMTP to an HTTP API is an environment variable, not a refactor.
Your application
Any Python code that needs to send email.
- FastAPI
- Django
- workers
- scripts
MailingKit
One API: send, send_template, render, validate.
- templates
- validation
- retries
- idempotency
- hooks
Provider adapter
Swappable. Write your own in one method.
- SMTP
- Resend
- console
- yours
Recipient
The right email, sent once.
Why MailingKit
The boring parts of email, handled once.
Every application ends up rebuilding the same mailing code. MailingKit is that code, written carefully, tested, and shared.
Provider-agnostic
SMTP, Resend, a console printer or your own adapter. Errors and results look the same whichever one is underneath.
Framework-independent
Plain Python with one dependency, Jinja2. Works in FastAPI, Django, Celery, CLIs and scripts, async or not.
Safe in development
Nothing is sent until you configure a provider. Write .eml previews, or redirect every email to one staging inbox.
Typed templates
Bind a template to a dataclass or Pydantic model. Missing data fails before sending, not in the customer's inbox.
Reliable delivery
Transient failures retry with backoff. Idempotency keys make sure the same event never sends the same email twice.
Secure by default
Header injection blocked, HTML escaped, templates sandboxed, TLS required for SMTP credentials, secrets kept out of logs.
Any application
Your domain stays in your code.
MailingKit has no idea what a candidate, an order or an invoice is. It only sees a template and the data you give it, which is why it fits anywhere.
SaaS
user.created
↓
welcome email with a sign-in link
Recruitment
candidate.selected
↓
invitation to the next interview round
E-commerce
order.shipped
↓
shipping email with a tracking link
Authentication
password.reset.requested
↓
a reset link that expires in 30 minutes
Fits your stack
Create one client. Use it everywhere.
The same object works in an async web app, a blocking worker or a test. Nothing framework-specific is required.
from fastapi import FastAPI
from mailingkit import MailingKit
mail = MailingKit.from_env()
app = FastAPI()
@app.post("/signup")
async def signup(email: str, name: str):
await mail.send_template("welcome", {"name": name}, to=email)
return {"ok": True}# Every send_mail(), password reset and admin error email now goes through MailingKit.
MAILERS = {
"default": {"BACKEND": "mailingkit.contrib.django.EmailBackend"},
}
# Django before 6.1:
# EMAIL_BACKEND = "mailingkit.contrib.django.EmailBackend"from mailingkit import Attachment, SyncMailingKit
mail = SyncMailingKit.from_env() # blocking API, safe from any thread
def send_invoice(invoice_id: int, email: str, pdf: bytes) -> None:
mail.send(
to=email,
subject=f"Invoice {invoice_id}",
text="Your invoice is attached.",
attachments=[Attachment(f"invoice-{invoice_id}.pdf", pdf)],
idempotency_key=f"invoice:{invoice_id}",
)from mailingkit import MailingKit
from mailingkit.providers import MemoryProvider
async def test_signup_sends_welcome_email():
outbox = MemoryProvider()
mail = MailingKit(outbox, default_sender="app@example.com", templates="templates")
await signup(mail, "ada@example.com")
assert outbox.last.subject == "Welcome to Example, Ada"Scope
Small on purpose.
MailingKit does the mailing and nothing else. That is what keeps it easy to drop in and easy to trust.
In the box
Core library, one dependency.
- Message model, validation and safety limits
- Jinja2 templates with typed data and text fallback
- Console, file, memory, SMTP and Resend providers
- One error hierarchy across every provider
- Retries, idempotency keys, staging redirect
- Hooks for metrics and tracing, Django backend
Not its job
These stay in your application.
- Users, candidates, orders or any business data
- Deciding when an email should be sent
- Storing delivery history in a database
- Marketing campaigns and newsletters
- Running a mail server
Drop MailingKit into your application.
Install it, set two environment variables, send your first email. The workshop walks you through it in about 45 minutes.