Introduction
Turn incoming email into webhooks. A small, self-hosted SMTP server that checks each email against your rules and sends the matches to your webhooks, with a web UI to see what arrived and how each webhook responded.
Point an MX record (or a forwarding rule) at mailrelay and wire email into the things that only speak HTTP:
- Bank and card alerts → your budgeting app (Firefly III, Actual)
- Receipts and invoices → Paperless-ngx or your accounting tool
- Monitoring alerts → ntfy, Slack, Discord, PagerDuty
- A support address → your helpdesk (Zammad, Linear, a custom API)
- Shipping notifications → Home Assistant
- Anything else → n8n, Zapier-style automations, your own service
Quick start
Section titled “Quick start”All you need is Docker. This starts mailrelay plus a small echo server that serves as a test webhook:
curl -fsSLO https://raw.githubusercontent.com/gowthamgts/mailrelay/main/docker-compose.quickstart.yamldocker compose -f docker-compose.quickstart.yaml up -dSend it an email (plain curl speaks SMTP):
curl smtp://localhost:2525 --mail-from me@example.com --mail-rcpt hello@example.com --upload-file - <<'EOF'From: me@example.comTo: hello@example.comSubject: Hello from mailrelay
It works!EOFThen:
- Open http://localhost:2623. The email is there, marked Dropped because no rule matched yet.
- Go to Rules → New rule, leave the conditions empty (matches everything) and set the webhook URL to
http://echo:8080/. - Open the email and click Replay. It is now Delivered, and the response body shows exactly what your webhook received.
Clean up with docker compose -f docker-compose.quickstart.yaml down -v.
Deploy
Section titled “Deploy”Docker. For a real server, start from docker-compose.example.yaml, or:
docker run -d --name mailrelay \ -p 25:25 -p 127.0.0.1:2623:2623 \ -v mailrelay_data:/data \ -e MAILRELAY_SMTP__DOMAIN=mx.example.com \ ghcr.io/gowthamgts/mailrelay:latestAdd -e MAILRELAY_WEBHOOK__ALLOW_PRIVATE_NETWORKS=true if your webhooks point at services on your network. The image keeps everything under /data (SQLite database, raw .eml files, and an optional config.yaml).
Binary. Download a release from the releases page, or build it with go build ./cmd/mailrelay. Then run ./mailrelay -config config.yaml. The database is created as mailrelay.db in the working directory unless you set webui.db_path.
The self-hosting guide walks through DNS, port 25, systemd, backups and upgrades. See also reverse proxy and login, webhook recipes and troubleshooting.
How it works
Section titled “How it works”SMTP ──▶ SPF / DKIM / DMARC ──▶ rule matching ──▶ webhooks (async, with retries) (off | log | enforce) every matching rule firesmailrelay persists the email and its webhook queue entries in SQLite before replying 250 OK, without waiting for webhook requests. If persistence fails, SMTP returns a temporary 451 so the sender can retry. A bounded worker pool sends webhooks in the background with exponential backoff. Pending retries survive restarts; requests interrupted by a crash are retried. Delivery is at least once: a receiver may see duplicates if mailrelay stops after sending a request but before recording its response.
Each queued delivery keeps its payload, webhook configuration and effective retry settings, including per-webhook overrides. Changes to rules or retry settings apply to new deliveries; UI replay queues a new batch using the current rules. Each attempt updates the existing delivery record, and the email reaches a final status after all its deliveries finish (the latest replay batch determines the result).
webhook.workers limits concurrent deliveries (default 4), with due work polled every 100ms. Shutdown stops new work and allows in-flight requests up to webhook.shutdown_timeout (default 30s) to finish; remaining work stays persisted. Retention keeps emails and raw messages with pending or in-progress deliveries until they finish. Manual deletion removes their queued deliveries too. Run only one mailrelay process per database.
Rules are created in the web UI and take effect immediately. Each rule has conditions and a webhook.
- Fields: From, To, CC, Subject, Body, any header, the SMTP envelope (
MAIL FROM/RCPT TO), and the domain of each address. - Patterns: globs by default (
alerts@*,*.example.com); wrap a pattern in slashes for a regex (/^\[URGENT\]/, or/(?i)invoice/to ignore case). - Match all or any of the conditions. A rule with no conditions matches every email.
- Every matching rule fires, not just the first one.
- Each webhook can override the timeout and retry settings, and the Test button checks that the URL is reachable.
- Webhooks can’t reach private or internal addresses (localhost, your LAN, Docker networks, Tailscale, cloud metadata) unless you set
webhook.allow_private_networks: true. Turn it on if your webhooks point at self-hosted services.
Payload
Section titled “Payload”With no template, the webhook receives the whole parsed email:
{ "from": "sender@example.com", "to": ["you@example.com"], "subject": "Hello", "text_body": "…", "html_body": "…", "attachments": [{ "filename": "a.pdf", "content_type": "application/pdf", "content": "base64…" }], "auth_result": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "arc": "none" }, "envelope_from": "bounce@example.com", "envelope_to": ["you@example.com"]}Or write a Go template. Every variable is already JSON-encoded, so don’t put quotes around it:
{"text": {{ .Subject }}, "from": {{ .From }}, "body": {{ .TextBody | default .HTMLBody }}}| Variables | Functions |
|---|---|
.From .To .CC .Subject .TextBody .HTMLBody .Headers .Attachments .AuthResult .EnvelopeFrom .EnvelopeTo |
first, last, join ", ", default <fallback>. Chain them: {{ .CC | default .To | first }} |
Missing fields render as null, so the output is always valid JSON. The editor in the UI autocompletes variables and flags mistakes as you type.
Configuration
Section titled “Configuration”Settings are layered: built-in defaults, then an optional config file (YAML, JSON or TOML), then MAILRELAY_ environment variables, with __ for nesting (MAILRELAY_SMTP__DOMAIN). Lists such as smtp.allowed_recipients and auth providers must go in the file. config.example.yaml documents every option.
| Setting | Default |
|---|---|
smtp.addr |
0.0.0.0:25 |
smtp.allowed_recipients |
empty (accept any recipient) |
http.addr |
127.0.0.1:2623 (0.0.0.0:2623 in Docker) |
webui.enabled |
true (rules are stored in its database, so turning it off drops all mail) |
webui.retention_days |
7 |
auth.spf / dkim / dmarc |
log (off, log or enforce; enforce rejects with SMTP 550) |
retry.max_retries / retry.timeout |
3 / 30s |
webhook.workers / webhook.shutdown_timeout |
4 / 30s |
webhook.allow_private_networks |
false (blocks localhost, LAN, Docker, Tailscale and metadata addresses) |
Retry, auth mode, log level and retention can also be changed at runtime on the Settings page.
Web UI login. Set http.auth.enabled: true and add an OpenID Connect provider (Google, Okta, Auth0, Keycloak, Authentik, …) under http.auth.providers with its issuer_url, client_id and client_secret; endpoints are discovered from the issuer. Register https://<your-host>/auth/callback as the redirect URI (or pin it with http.auth.redirect_url behind a proxy). ID tokens are verified (signature, issuer, audience, expiry, nonce). allowed_email_domains limits access to verified emails on those domains; leave it empty and any account at the provider can sign in. Set session_secret so sessions survive restarts. GitHub login is not supported, since GitHub does not speak OIDC.
Forwarded mail. When a provider such as Gmail or Fastmail forwards to mailrelay, the envelope addresses differ from the headers. The UI shows the original header From/To and lists the envelope separately, and rules can match either one. ARC is verified so the original SPF/DKIM results survive forwarding.
Operations
Section titled “Operations”| Endpoint | |
|---|---|
/healthz |
Liveness probe, returns ok |
/metrics |
Prometheus metrics (mailrelay_*). Public even with login on, unless http.protect_metrics: true. See docs/prometheus.md and the Grafana dashboard. |
mailrelay -version prints the build version.
Development
Section titled “Development”just dev # run with config.dev.yaml (SMTP on :2525, UI on :2623)just dev-test # send a batch of sample emails with swaksjust testjust docs # preview the docs site (README.md + docs/) on :4321The docs site at gowthamgts.github.io/mailrelay is built from README.md and docs/ by the Starlight project in site/, and deployed to GitHub Pages on every push to main. Edit the Markdown, not the generated files under site/src/content/.
