A practical guide for sending auth emails (magic link, OTP, password reset) through your own SMTP, using authmail-relay as an HTTP gateway or Python library.
authmail-relay is a small self-hosted service that sends transactional auth emails through your own SMTP account. It keeps SMTP credentials and email-template logic out of every app that needs to send auth mail. Other apps call one internal HTTP endpoint with a Bearer API key, or import it as a Python library.
It ships built-in flows for magic links, OTP codes, and password reset, plus an arbitrary HTML/text /send endpoint for any transactional mail.
authmail-relay is not a replacement for those.| Mode | Use when |
|---|---|
| HTTP service | Multiple apps share one internal email service. SMTP credentials live only in this service. |
| Python library | A single Python/FastAPI app sends mail directly. No separate gateway needed. |
| SMTP smoke test CLI | Quickly verify SMTP credentials from the command line. |
# Library mode (no extra deps)
pip install authmail-relay
# HTTP service mode (FastAPI + uvicorn)
pip install "authmail-relay[http]"
Requirements: Python 3.10+.
Install the latest unreleased commit directly from git:
pip install "authmail-relay[http] @ git+https://github.com/hwan96-ai/authmail-relay.git"
The repo name, PyPI distribution name, and Python import name differ. Use each in the right place.
| Context | Name |
|---|---|
| Repository / service | authmail-relay |
PyPI distribution (used by pip install) | authmail-relay |
| Python import | authmail_relay |
pip install "authmail-relay[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 authmail_relay
# → Uvicorn running on http://127.0.0.1:8000
OpenAPI docs are served at http://127.0.0.1:8000/docs. Required environment variables are SMTP_HOST and API_KEY; the service fails fast at startup if they are missing.
export SMTP_HOST=smtp.gmail.com
export SMTP_USER=sender@gmail.com
export SMTP_PASSWORD=app-password
python -m authmail_relay test --to me@example.com
Exits 0 on success, 1 on failure with an error_code.
/sendcurl -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>"
}'
/send/otpcurl -X POST http://127.0.0.1:8000/send/otp \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"user_name": "User Name",
"code": "482901"
}'
/send/magic-linkcurl -X POST http://127.0.0.1:8000/send/magic-link \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"user_name": "User Name",
"token": "$TOKEN",
"base_url": "https://myapp.com"
}'
The caller is responsible for generating, storing, expiring, and single-use enforcing the token. authmail-relay only sends the mail. Generate $TOKEN in your app — e.g. python -c "import secrets; print(secrets.token_urlsafe(32))" — or pass through the token issued by your auth provider.
$API_KEY only exists in the shell where it was exported. If curl runs in a second terminal, re-export API_KEY there or load it from .env first.from authmail_relay import SmtpSender, MagicLinkNotifier, OTPNotifier
from authmail_relay.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
# The caller owns token generation. For custom auth, generate a high-entropy
# opaque token; if you use an auth provider (e.g. Supabase), use the token it issues.
import secrets
token = secrets.token_urlsafe(32)
MagicLinkNotifier(sender, base_url="https://myapp.com").send(
"user@example.com", "User Name", token,
)
# OTP
OTPNotifier(sender).send("user@example.com", "User Name", "482901")
cp .env.example .env
# Edit .env: SMTP_HOST / SMTP_USER / SMTP_PASSWORD / API_KEY
docker compose up -d --build
curl http://127.0.0.1:8000/health
The docker-compose.yml publishes 8000:8000 on the host for convenience. Do not expose that port to the public internet.
For local dev without a real SMTP provider, use Mailpit:
docker compose -f docker-compose.dev.yml up -d --build
# Mailpit UI: http://127.0.0.1:8025
/docs and /metrics — disable them at the edge or require auth. Set METRICS_REQUIRE_AUTH=true.API_KEY, WEBHOOK_SECRET, and SMTP credentials in environment variables or a secret manager. Generate API_KEY with openssl rand -hex 32.authmail-relay sends auth emails. It does not generate, store, verify, or expire login tokens. The caller owns token entropy (≥ secrets.token_urlsafe(32)), expiration, single-use enforcement, replay protection, and account-state checks.token_hash values, full confirmation_url values, or action_link values in production. They are bearer secrets. Use dry-run or .eml capture mode for non-production debugging.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 timestamp-bound V2 header; new receivers should validate V2. See webhooks.md.
Observability is opt-in and off by default:
/metrics (METRICS_ENABLED=true, METRICS_REQUIRE_AUTH=true recommended).EMAIL_SERVICE_LOG_FORMAT=json). Recipient addresses are hashed (SHA-256, first 8 chars) and never logged in plaintext.X-Request-ID propagation end-to-end.max_retries=N).