Skip to content

hwan96-ai/authmail-relay

Repository files navigation

authmail-relay

PyPI Python License: MIT

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-service with the import package email_service, under the repo name email-service. A thin email_service compatibility shim is shipped in this release so existing import email_service / from email_service import … code keeps working and emits a DeprecationWarning. The shim will be removed in a future major release — update imports to authmail_relay when 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_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.

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 /docs and /metrics — either disable at the edge or require auth. Set METRICS_REQUIRE_AUTH=true for /metrics.
  • Store API_KEY, WEBHOOK_SECRET, and SMTP credentials in environment variables or a secret manager. Generate API_KEY with openssl 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=true recommended).
  • Structured JSON logs (EMAIL_SERVICE_LOG_FORMAT=json). Recipient addresses are hashed (SHA-256, first 8 chars) — never logged in plaintext.
  • X-Request-ID propagation 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:


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.

About

Self-hosted auth email service for Python/FastAPI teams using their own SMTP. Magic-link, OTP, password-reset over your own SMTP server, with idempotency, rate limiting, and webhook delivery.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors