Self-hosted SMTP relay for auth emails.
Project description
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.
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 authmail_relay-0.5.0.tar.gz.
File metadata
- Download URL: authmail_relay-0.5.0.tar.gz
- Upload date:
- Size: 60.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9500650ff2c1d7a17553eda6c6c83114c957315343fc3e1308edc855619d1d60
|
|
| MD5 |
6ecdfad07cb631c5e25f2fb4ea13408f
|
|
| BLAKE2b-256 |
1f85cca4e2161f4328a962d2801c9cb698e0b008174d6e2db293ea2ff46c3ffd
|
Provenance
The following attestation bundles were made for authmail_relay-0.5.0.tar.gz:
Publisher:
release.yml on hwan96-ai/authmail-relay
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
authmail_relay-0.5.0.tar.gz -
Subject digest:
9500650ff2c1d7a17553eda6c6c83114c957315343fc3e1308edc855619d1d60 - Sigstore transparency entry: 1601646259
- Sigstore integration time:
-
Permalink:
hwan96-ai/authmail-relay@6b1971906b6c166ad6a7f0ce7d4f26e3bb3c45d0 -
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@6b1971906b6c166ad6a7f0ce7d4f26e3bb3c45d0 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file authmail_relay-0.5.0-py3-none-any.whl.
File metadata
- Download URL: authmail_relay-0.5.0-py3-none-any.whl
- Upload date:
- Size: 36.0 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 |
ec26134bc2731112429bfc059a0335515370948d664a8dc6131a2573e9cbf349
|
|
| MD5 |
6d6d3f01693e53f683dc50a344d9bf4e
|
|
| BLAKE2b-256 |
1092e014f0b84b342750188e38ad1f6981ea17c3ddc9af4487b129201609a377
|
Provenance
The following attestation bundles were made for authmail_relay-0.5.0-py3-none-any.whl:
Publisher:
release.yml on hwan96-ai/authmail-relay
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
authmail_relay-0.5.0-py3-none-any.whl -
Subject digest:
ec26134bc2731112429bfc059a0335515370948d664a8dc6131a2573e9cbf349 - Sigstore transparency entry: 1601646350
- Sigstore integration time:
-
Permalink:
hwan96-ai/authmail-relay@6b1971906b6c166ad6a7f0ce7d4f26e3bb3c45d0 -
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@6b1971906b6c166ad6a7f0ce7d4f26e3bb3c45d0 -
Trigger Event:
workflow_dispatch
-
Statement type: