Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TeachMeMailer

CI Nightly PR Coverage

Production-grade email gateway — FastAPI + Postgres + async SMTP. API-key auth (hashed at rest), per-key daily limits enforced atomically in Postgres, full observability stack in one docker compose up: Prometheus, Grafana (pre-provisioned dashboard), Mailpit SMTP sink, admin UI.

The gateway under load — provisioned Grafana dashboard, out of the box:

Grafana dashboard

Red line in "Responses by status" = 429s from a key exhausting its daily limit — the limiter doing its job under load.

One command, whole system

cp .env.example .env
docker compose up -d      # app + postgres + redis + prometheus + grafana + mailpit
# create an API key
docker compose exec app uv run --no-sync python create_api_key.py

# send an email
curl -X POST http://localhost:8088/api/v1/send \
  -H "X-API-Key: <your_key>" -H "Content-Type: application/json" \
  -d '{"to": "user@example.com", "subject": "Hello", "text": "Hi!", "html": "<p>Hi!</p>"}'
# → {"status":"queued","remaining":49}

Every email lands in Mailpit (localhost:8025) — a real SMTP round-trip, no external provider needed:

Mailpit inbox

Architecture

flowchart LR
    C[Client] -->|X-API-Key| A[FastAPI gateway :8088]
    A --> K["auth: key hash lookup<br/>+ atomic daily counter"]
    K --> Q[background send task]
    Q -->|aiosmtplib, STARTTLS| S[SMTP<br/>Mailpit locally / any provider in prod]
    A --> DB[(PostgreSQL<br/>keys, send log, quotas)]
    A -.-> P[(Prometheus)] -.-> G[Grafana dashboard]
    ADM[Admin UI /admin<br/>BasicAuth] --> DB
Loading

What's worth stealing

Atomic daily limits. The per-key counter is a single UPDATE ... WHERE count < limit RETURNING — no race between check and increment, no Redis needed for correctness. 429 when exhausted; the remaining quota is returned on every send.

Keys are hashed at rest. The API key table stores bcrypt/PBKDF2 hashes — a leaked DB dump doesn't leak keys.

Send is backgrounded, log is not. The HTTP request commits an audit row and returns 202 queued immediately; SMTP happens in a background task that back-fills the message ID. Slow SMTP never blocks the API.

Admin UI included — key management, daily usage, send logs (BasicAuth or header-key protected):

Admin UI

Endpoints

Endpoint Description
POST /api/v1/send Send email (to, subject, text and/or html, optional headers) — 202 / 401 / 429 / 422
GET /api/v1/health Healthcheck
GET /metrics Prometheus (via prometheus-fastapi-instrumentator)
GET /admin Admin UI: API keys, usage, send logs
GET /docs Swagger UI

Local observability stack

Service Port Purpose
app 8088 the gateway
Mailpit 8025 SMTP sink UI — every sent email, viewable
Grafana 3000 pre-provisioned "Mail Gateway" dashboard (anonymous viewer enabled)
Prometheus 9090 scrapes the app every 5 s
Postgres 15432 published for local inspection

For production SMTP set SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASSWORD/SMTP_STARTTLS in .env — the Mailpit values in compose are dev defaults.

Tests

docker compose exec app uv run --no-sync pytest -q

CI runs lint + tests on every PR, nightly workflow exercises the full compose stack, coverage is tracked per-PR (see badges).

License

MIT — see LICENSE.

About

Async mail delivery service built with FastAPI. Provides API key authentication, per-key daily send limits, rate-limiting, logging of each email, and async delivery through Gmail SMTP. Includes healthcheck, metrics, CI/CD, linting, and test automation.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages