authmail-relay
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
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, 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-relay
import authmail_relay
Migration 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.
Install
# 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"
Quickstart — HTTP service mode
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: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}
$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.
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 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.
Quickstart — library mode
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.
Security — read before deploying
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.
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 → authmail-relay → 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 | 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.
Development
git clone https://github.com/hwan96-ai/authmail-relay.git
cd authmail-relay
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.
Release files for authmail-relay 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| authmail_relay-0.5.0.tar.gz | 60.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| authmail_relay-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 96.0 kB
Release files / authmail_relay-0.5.0.tar.gz
| Download URL | authmail_relay-0.5.0.tar.gz |
|---|---|
| Size | 60.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9500650ff2c1d7a17553eda6c6c83114c957315343fc3e1308edc855619d1d60
|
|
BLAKE2b-256 checksum How to use checksums |
1f85cca4e2161f4328a962d2801c9cb698e0b008174d6e2db293ea2ff46c3ffd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.12.9
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 22, 2026.
Transparency logRelease files / authmail_relay-0.5.0-py3-none-any.whl
| Download URL | authmail_relay-0.5.0-py3-none-any.whl |
|---|---|
| Size | 36.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ec26134bc2731112429bfc059a0335515370948d664a8dc6131a2573e9cbf349
|
|
BLAKE2b-256 checksum How to use checksums |
1092e014f0b84b342750188e38ad1f6981ea17c3ddc9af4487b129201609a377
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.12.9
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 22, 2026.
Transparency log