Self-hosted SMTP relay for auth emails.
한국어 README · HTML Usage Guide · 한국어 사용 가이드
HTML guides render through GitHub Pages.
authmail-relay 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
▼
authmail-relay ← SMTP credentials live here
│
▼
SMTP provider ──► User inbox
- 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.
- 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.
The repo, the PyPI distribution, and the Python import package now share the same project name.
| Name | |
|---|---|
| Repository / service | authmail-relay |
| PyPI distribution | authmail-relay |
| Python import | authmail_relay |
pip install authmail-relayimport authmail_relayMigration note. This project was previously published on PyPI as
hwan-email-servicewith the import packageemail_service, under the repo nameemail-service. A thinemail_servicecompatibility shim is shipped in this release so existingimport email_service/from email_service import …code keeps working and emits aDeprecationWarning. The shim will be removed in a future major release — update imports toauthmail_relaywhen convenient. See CHANGELOG.md for the rename entry.
# 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 straight from git:
pip install "authmail-relay[http] @ git+https://github.com/hwan96-ai/authmail-relay.git"Run authmail-relay 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 "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:8000In 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}
$API_KEYonly exists in the shell where it was exported. Ifcurlruns in a second terminal, re-exportAPI_KEYthere or load it from.envfirst.
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.
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 authmail_relay 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.
Import authmail_relay directly inside one Python/FastAPI app. Useful when you
don't need a separate internal HTTP gateway.
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")Full library API (SmtpSender, MagicLinkNotifier, OTPNotifier,
TemplateNotifier, custom notifiers, retries): docs/api.md.
For the HTTP client SDK (EmailServiceClient), see docs/api.md#library-mode.
authmail-relay 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.
If you front authmail-relay with Supabase Auth or another auth provider, the provider — not authmail-relay — generates and verifies the token. See docs/supabase-auth.md.
For the full production checklist, see docs/deployment.md. Vulnerability reporting: SECURITY.md.
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:8025Required 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.
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.
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 → authmail-relay → SMTP send logs.- SMTP retries with bounded exponential backoff (library mode,
max_retries=N).
Full operations guide: docs/operations.md.
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.
| 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 | authmail-relay |
A longer comparison, including self-hosted email platforms, lives in docs/alternatives.md.
Using authmail-relay alongside Supabase Auth? See
Supabase Auth integration notes — authmail-relay
delivers the email, Supabase Auth still owns tokens, sessions, and
auth.uid() identity. Per-provider notes index:
docs/providers.md.
git clone https://github.com/hwan96-ai/authmail-relay.git
cd authmail-relay
pip install -e ".[dev,http]"
python -m pytest tests/ -vTests do not connect to a real SMTP server (smtplib.SMTP is mocked).
MIT.