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.

  • Python 3.11+
  • Any provider
  • Any framework
  • Typed
  • MIT
app.py
from mailingkit import MailingKit

mail = MailingKit.from_env()

await mail.send_template(
    "welcome",
    {"name": "Ada"},
    to="ada@example.com",
    idempotency_key="welcome:ada",
)
no provider configured
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.

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.

01

Provider-agnostic

SMTP, Resend, a console printer or your own adapter. Errors and results look the same whichever one is underneath.

02

Framework-independent

Plain Python with one dependency, Jinja2. Works in FastAPI, Django, Celery, CLIs and scripts, async or not.

03

Safe in development

Nothing is sent until you configure a provider. Write .eml previews, or redirect every email to one staging inbox.

04

Typed templates

Bind a template to a dataclass or Pydantic model. Missing data fails before sending, not in the customer's inbox.

05

Reliable delivery

Transient failures retry with backoff. Idempotency keys make sure the same event never sends the same email twice.

06

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.

main.py
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}

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.

Get started