Home · Language: English · 한국어

How to use authmail-relay

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.

What authmail-relay does

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.

When to use it

If you need managed deliverability, bounce processing, suppression lists, or domain reputation tooling, use a managed provider (Resend, Postmark, SendGrid, Mailgun, Amazon SES). authmail-relay is not a replacement for those.

Choose a mode

ModeUse when
HTTP serviceMultiple apps share one internal email service. SMTP credentials live only in this service.
Python libraryA single Python/FastAPI app sends mail directly. No separate gateway needed.
SMTP smoke test CLIQuickly verify SMTP credentials from the command line.

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 directly from git:

pip install "authmail-relay[http] @ git+https://github.com/hwan96-ai/authmail-relay.git"

Package naming

The repo name, PyPI distribution name, and Python import name differ. Use each in the right place.

ContextName
Repository / serviceauthmail-relay
PyPI distribution (used by pip install)authmail-relay
Python importauthmail_relay

Run the HTTP service

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.

30-second SMTP smoke test

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.

Calling the endpoints with curl

Generic transactional mail — /send

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>"
  }'

One-time password — /send/otp

curl -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"
  }'

Magic link — /send/magic-link

curl -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.

Second-terminal trap. $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.

Library examples

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")

Docker and Mailpit local dev

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

Production security checklist

Read before exposing this service. A self-hosted auth email service can be abused if exposed incorrectly. Treat the following as hard requirements.

Webhooks and observability

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:

What it is not