Troubleshooting
What the statuses mean
Section titled “What the statuses mean”| Status | Meaning |
|---|---|
| Received | Accepted and queued; one or more deliveries may still be pending or in progress. |
| Delivered | Every matching rule’s webhook returned 2xx. |
| Dropped | The email was accepted, but no rule matched it. Nothing was sent. |
| Partial failure | Some matching webhooks succeeded and some didn’t. |
| Failed | Every matching webhook failed. Open the email to see each attempt’s status code, error and response body. |
| Rejected | mailrelay refused the email at the SMTP level because an auth check set to enforce failed. The sender got a 550 error. |
Mail to an address not in smtp.allowed_recipients is also refused, but it never shows up in the UI. Look for rejected recipient in the logs, or the mailrelay_smtp_recipients_rejected_total metric.
The email never arrived
Section titled “The email never arrived”Work through these in order:
-
DNS.
dig +short MX your-domainshould return your mailrelay host, anddig +short Aon that host should return your server’s IP. -
Port 25 is reachable. From a machine outside your network:
nc -vz mx.example.com 25. If it times out, check your firewall, your provider’s port 25 policy and any port-forwarding. See Before you start. -
The recipient is allowed. Check the logs for
rejected recipient. Patterns insmtp.allowed_recipientsare globs matched against the whole address, such as*@alerts.example.com. -
The sender gave up or is retrying. Most senders send you a bounce message saying why. Mail servers retry for hours, so a fix may still let a delayed email through.
-
Send one yourself, bypassing DNS, to check mailrelay on its own:
Terminal window swaks --server mx.example.com:25 --to you@alerts.example.com --from test@example.comFor a batch of varied test emails (attachments, HTML, CC, forwarded mail), run
scripts/test-emails.shwithSMTP_HOSTandSMTP_PORTset.
The email shows as Dropped
Section titled “The email shows as Dropped”No rule matched. Open the email and compare its fields with your rule’s conditions:
- Header From/To and the SMTP envelope (
MAIL FROM/RCPT TO) are separate fields. For forwarded mail they’re often different. Match the one that has the address you expect. - Globs ignore case and must match the whole value:
invoiceonly matches a subject that is exactly “invoice”, while*invoice*matches it anywhere. Regexes (/…/) are case-sensitive unless you add(?i). - Disabled rules don’t match.
Fix the rule, then click Replay to try again without waiting for another email.
The webhook failed
Section titled “The webhook failed”Open the email to see each attempt:
- “private or internal” error: the URL points at localhost, your LAN, a Docker network or Tailscale. Set
webhook.allow_private_networks: trueif that’s intended. - 4xx: the receiver didn’t like the request: a wrong URL, a missing auth header, or a payload it can’t parse. 4xx responses aren’t retried. Check the response body.
- 5xx or timeout: the receiver is down or slow. These are retried; raise
retry.timeoutif the receiver needs longer. - Template errors are flagged in the rule editor as you type. Remember that variables go in without quotes.
Retries after a restart
Section titled “Retries after a restart”Webhook deliveries and their next attempt time are stored in the SQLite database (webui.db_path). Keep that database on a persistent volume. On startup, pending deliveries resume when due, and requests left in progress are retried. Already completed deliveries are not resent. A crash after the receiver accepts a request can cause a duplicate, so receivers should handle duplicate messages safely. Run only one mailrelay process per database.
An email stays Received until all queued deliveries finish. Delivery records show pending or in_progress and the completed attempt count. Retries use the payload, target and effective retry policy captured when queued, including max_retries, initial_wait, max_wait, timeout and retry_on_timeout. Settings changes affect new deliveries. Replay queues another batch with the current rules and preserves earlier delivery records; its HTTP response does not wait for webhook delivery.
webhook.workers (default 4) bounds concurrent requests. Due work is checked every 100ms, so a saturated queue can run later than its scheduled time. On shutdown, new claims stop and requests can finish for webhook.shutdown_timeout (default 30s). Requests canceled at the deadline return to pending without consuming a retry. These two settings require a restart and can be set with MAILRELAY_WEBHOOK__WORKERS and MAILRELAY_WEBHOOK__SHUTDOWN_TIMEOUT.
Retention skips emails and raw .eml files with unfinished deliveries, even beyond webui.retention_days. They become eligible after delivery finishes. Manually deleting an email also deletes its delivery queue; an HTTP request already in flight may still reach the receiver. If SMTP cannot save an email or queue entry, it replies 451 rather than acknowledging work that could be lost.
Auth checks and forwarded mail
Section titled “Auth checks and forwarded mail”When a provider forwards mail, the forwarding server isn’t one the original sender’s SPF lists, so SPF fails, and DMARC can fail with it. mailrelay verifies ARC, which lets forwarders such as Gmail and Fastmail vouch for the original results. Not every forwarder adds ARC, though.
Before setting any check to enforce:
- Leave all three on
log(the default) for a while. - Look at the auth results on real emails in the UI, or at
mailrelay_auth_checks_totalin Prometheus. - Enforce only the checks that pass on the mail you care about.
You can change the auth modes on the Settings page without restarting.
Settings I changed in the config file don’t apply
Section titled “Settings I changed in the config file don’t apply”Once you click Save on the Settings page, those values (log level, auth checks, retry and retention) are stored in the database and override the config file and env vars on every start. Change them on the Settings page instead.
In Docker, http.addr, webui.db_path and webui.raw_email_dir are set by the image’s env vars, which beat the config file. Set the matching MAILRELAY_ variable instead.
More detail in the logs
Section titled “More detail in the logs”Set log_level: debug (or change it on the Settings page) for per-connection and per-rule logs. With Docker: docker compose logs -f mailrelay. With systemd: journalctl -u mailrelay -f.
