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
Add -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) and enables the web UI.
Binary. Download a release from the releases page, or build it with go build ./cmd/mailrelay. Then run ./mailrelay -config config.yaml.
[!IMPORTANT]
The web UI has no login unless you configure http.auth (OIDC). Keep port 2623 on localhost, or put it behind a reverse proxy that handles auth. Also set smtp.allowed_recipients so only your addresses are accepted.
mailrelay replies 250 OK as soon as it accepts an email. Webhooks are sent in the background with exponential backoff, so a slow or failing webhook never bounces mail. Every attempt is recorded, and any email can be replayed from the UI.
Rules
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
With no template, the webhook receives the whole parsed email:
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
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
false (true in Docker)
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.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.