Self-hosted auth email service for Python/FastAPI teams using their own SMTP
Project description
email-service
Small self-hosted auth-email service for Python/FastAPI teams using their own SMTP.
email-service is a small, self-hosted service that sends magic-link, OTP,
and password-reset emails through your own SMTP account. It keeps SMTP
credentials and email-template logic out of every app that needs to send auth
mail — your apps call one internal HTTP endpoint with a Bearer API key, or
import it as a Python library.
App / Auth server
│ Bearer API key
▼
email-service ← SMTP credentials live here
│
▼
SMTP provider ──► User inbox
What it is
- A small internal auth-email gateway for teams that already have SMTP.
- Sends transactional auth emails: magic links, OTP codes, password resets, plus arbitrary templated mail.
- Built for Python/FastAPI teams, but the HTTP API is language-agnostic.
What it is not
- Not a mail server — it talks to your existing SMTP provider (Gmail, SES SMTP, an internal relay, etc.). It does not accept inbound mail or handle MX.
- Not a full auth platform — it sends auth emails; it does not generate, store, verify, or expire login tokens, manage sessions, or store users.
- Not a marketing/bulk-email platform — no bounce processing, suppression lists, analytics dashboards, or deliverability tooling.
- Not a managed-email replacement for Resend, Postmark, SendGrid, Mailgun, or SES — those bring deliverability, reputation, and SLAs that a small self-hosted gateway cannot match. See alternatives.
Package names
The repo name, PyPI distribution name, and Python import package differ.
| Name | |
|---|---|
| Repository / service | email-service |
| PyPI distribution | hwan-email-service |
| Python import | email_service |
pip install hwan-email-service
import email_service
Install
# Library mode (no extra deps)
pip install hwan-email-service
# HTTP service mode (FastAPI + uvicorn)
pip install "hwan-email-service[http]"
Requirements: Python 3.10+.
Install the latest unreleased commit straight from git:
pip install "hwan-email-service[http] @ git+https://github.com/hwan96-ai/email-service.git"
Quickstart — HTTP service mode
Run email-service as a standalone service. Other apps call it over HTTP with
a Bearer API key. SMTP credentials live in this service's environment only.
pip install "hwan-email-service[http]"
export SMTP_HOST=smtp.gmail.com
export SMTP_USER=sender@gmail.com
export SMTP_PASSWORD=app-password
export API_KEY=$(openssl rand -hex 32)
python -m email_service
# → Uvicorn running on http://127.0.0.1:8000
In another terminal:
curl -X POST http://127.0.0.1:8000/send \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"user@example.com","subject":"Hi","html_body":"<p>Hello</p>"}'
# → {"sent":true}
OpenAPI docs: http://127.0.0.1:8000/docs.
Full HTTP endpoint reference, dry-run mode, idempotency, and the Python client SDK: docs/api.md.
30-second SMTP smoke test
If you just want to verify SMTP credentials, skip the HTTP server entirely:
export SMTP_HOST=smtp.gmail.com
export SMTP_USER=sender@gmail.com
export SMTP_PASSWORD=app-password
python -m email_service test --to me@example.com
# → SendResult(sent=True, error_code=None, ..., message_id='<...@host>')
Exits 0 on success, 1 on failure with error_code printed.
Quickstart — library mode
Import email_service directly inside one Python/FastAPI app. Useful when you
don't need a separate internal HTTP gateway.
from email_service import SmtpSender, MagicLinkNotifier, OTPNotifier
from email_service.sender import SmtpConfig
sender = SmtpSender(SmtpConfig(
host="smtp.gmail.com",
user="sender@gmail.com",
password="app-password",
))
# One-off HTML mail
sender.send("user@example.com", "Hi", "<p>Hello</p>")
# Magic link
MagicLinkNotifier(sender, base_url="https://myapp.com").send(
"user@example.com", "User Name", "abc123token",
)
# OTP
OTPNotifier(sender).send("user@example.com", "User Name", "482901")
Full library API (SmtpSender, MagicLinkNotifier, OTPNotifier,
TemplateNotifier, custom notifiers, retries): docs/api.md.
Security — read before deploying
email-service is designed as an internal service. A self-hosted auth
email service can be abused if exposed incorrectly. Treat the following as
hard requirements before any production deploy:
- Do not expose directly to the public internet. Put it behind a reverse proxy or API gateway on a private network / VPC.
- Terminate TLS at the edge (nginx, Traefik, your gateway).
- Rate-limit failed auth attempts at the edge. The app's built-in per-bearer rate limit applies to authenticated requests; it does not protect against blind Bearer-token guessing.
- Protect
/docsand/metrics— either disable at the edge or require auth. SetMETRICS_REQUIRE_AUTH=truefor/metrics. - Store
API_KEY,WEBHOOK_SECRET, and SMTP credentials in environment variables or a secret manager. GenerateAPI_KEYwithopenssl rand -hex 32. Never commit them.
Trust boundary: this service sends auth emails. It does not generate,
store, verify, or expire login tokens. The caller is responsible for token
entropy (at least secrets.token_urlsafe(32)), expiration, single-use
enforcement, replay protection, and account-state checks.
For the full production checklist, see docs/deployment.md. Vulnerability reporting: SECURITY.md.
Docker
cp .env.example .env
# Edit .env: set SMTP_HOST / SMTP_USER / SMTP_PASSWORD / API_KEY
# API_KEY=$(openssl rand -hex 32)
docker compose up -d --build
curl http://127.0.0.1:8000/health # → {"status":"ok"}
The provided docker-compose.yml publishes 8000:8000 on the host for
convenience. Do not expose this port to the public internet — see the
deployment guide for production hardening.
Local development with Mailpit (no real SMTP needed):
docker compose -f docker-compose.dev.yml up -d --build
# Mailpit UI: http://127.0.0.1:8025
Configuration
Required env vars: SMTP_HOST, API_KEY.
The service fails fast at startup if required vars are missing.
Full env-var reference (rate limits, idempotency, webhook SSRF allowlist, metrics auth, structured logs, retry tuning): docs/configuration.md.
A working .env.example is included in the repo root.
Webhooks (async send)
Pass webhook_url in a /send* request body to receive the delivery result
asynchronously. The service signs the payload with both a legacy V1 header and
a V2 timestamp-bound header; new receivers should validate V2.
Webhook payload format, signature verification, the V1 → V2 migration, and
local testing with docker-compose.dev.yml: docs/webhooks.md.
Observability
Opt-in features, all off by default:
- Prometheus metrics at
/metrics(METRICS_ENABLED=true,METRICS_REQUIRE_AUTH=truerecommended). - Structured JSON logs (
EMAIL_SERVICE_LOG_FORMAT=json). Recipient addresses are hashed (SHA-256, first 8 chars) — never logged in plaintext. X-Request-IDpropagation end-to-end from gateway → email-service → SMTP send logs.- SMTP retries with bounded exponential backoff (library mode,
max_retries=N).
Full operations guide: docs/operations.md.
Examples
End-to-end integration snippets for common Python frameworks:
- examples/fastapi_integration.py
- examples/django_integration.py
- examples/flask_integration.py
- examples/integration_test_with_capture.py
—
.emlcapture mode for integration tests without a real SMTP server.
When to use what
| If you need… | Use |
|---|---|
| Managed deliverability, bounces, SLA, dashboards | Resend / Postmark / SendGrid / Mailgun / Amazon SES |
| Full user/session/RBAC/password flows | Supabase Auth, Ory Kratos, Keycloak, Authentik, Appwrite |
| A mail library inside one FastAPI app | fastapi-mail |
| An internal HTTP gateway that keeps your existing SMTP credentials out of every app | email-service |
A longer comparison, including self-hosted email platforms, lives in docs/alternatives.md.
Development
git clone https://github.com/hwan96-ai/email-service.git
cd email-service
pip install -e ".[dev,http]"
python -m pytest tests/ -v
Tests do not connect to a real SMTP server (smtplib.SMTP is mocked).
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hwan_email_service-0.4.2.tar.gz.
File metadata
- Download URL: hwan_email_service-0.4.2.tar.gz
- Upload date:
- Size: 57.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be6102286328b768cad24a7aaa04beb10b1a7b1068023dfb4c598d599a623196
|
|
| MD5 |
00bd807a0ef039e0601dd12919cff593
|
|
| BLAKE2b-256 |
f741df1da8da6ef63385f42b3f70615e49a7dd1f0ade3e9d820d101efaedf7b8
|
Provenance
The following attestation bundles were made for hwan_email_service-0.4.2.tar.gz:
Publisher:
release.yml on hwan96-ai/email-service
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hwan_email_service-0.4.2.tar.gz -
Subject digest:
be6102286328b768cad24a7aaa04beb10b1a7b1068023dfb4c598d599a623196 - Sigstore transparency entry: 1597886572
- Sigstore integration time:
-
Permalink:
hwan96-ai/email-service@972ccf8649e2994a5535875d33f3db9370405228 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/hwan96-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@972ccf8649e2994a5535875d33f3db9370405228 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file hwan_email_service-0.4.2-py3-none-any.whl.
File metadata
- Download URL: hwan_email_service-0.4.2-py3-none-any.whl
- Upload date:
- Size: 34.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6665779955b3ec627688468154e1e66f16731f8b559b3351daad17362b20175c
|
|
| MD5 |
9ca5b01f9d8c698b70a8d128c84a5659
|
|
| BLAKE2b-256 |
f1ac2c31dd614feff97fa4f8b4407cc605b61e82bbccde0a88594c9402de6d01
|
Provenance
The following attestation bundles were made for hwan_email_service-0.4.2-py3-none-any.whl:
Publisher:
release.yml on hwan96-ai/email-service
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hwan_email_service-0.4.2-py3-none-any.whl -
Subject digest:
6665779955b3ec627688468154e1e66f16731f8b559b3351daad17362b20175c - Sigstore transparency entry: 1597886656
- Sigstore integration time:
-
Permalink:
hwan96-ai/email-service@972ccf8649e2994a5535875d33f3db9370405228 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/hwan96-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@972ccf8649e2994a5535875d33f3db9370405228 -
Trigger Event:
workflow_dispatch
-
Statement type: