Skip to content

Built-in SMTP server

Lots of self-hosted apps (Synology DSM, Vaultwarden, Forgejo, Pi-hole, Home Assistant) can only send notifications by email. atreoAGENT ships with a small SMTP server that turns those emails into encrypted push notifications. Point an app at it, send mail to a member’s atreoLINK email address, and the agent forwards it as a notification.

The server is off by default and designed for LAN-only use.

In config.yaml:

smtp:
enabled: true

Or with environment variables:

Terminal window
SMTP_ENABLED=true

That’s all that’s required. The gateway listens on 0.0.0.0:2525 by default; it stays LAN-only because the source-IP allowlist rejects anything outside your local networks. Binding to a specific LAN IP (listen: "192.168.1.10:2525" / SMTP_LISTEN=192.168.1.10:2525) is optional defense-in-depth, not a substitute for the allowlist — host-networked containers can’t always rely on a host loopback bind.

Then restart the agent: docker compose restart atreoagent. You’ll see smtp: listening on 0.0.0.0:2525 (full-email routing, AUTH PLAIN/LOGIN required, password = notify API key, STARTTLS disabled (plaintext only)) in the logs.

The gateway only accepts connections from a configurable list of CIDRs. A connection from a source IP outside the list is rejected before AUTH — the password is never even solicited. This, not the bind address, is what keeps the gateway LAN-only even though it listens on 0.0.0.0:2525 by default.

When smtp.trusted_networks is unset, the allowlist defaults to loopback, the private (RFC 1918) ranges, link-local, IPv6 ULA, and the WireGuard tunnel subnet:

127.0.0.0/8 ::1/128
10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
169.254.0.0/16 fe80::/10 fc00::/7
100.64.0.0/24 (the WireGuard tunnel subnet)

To override the list, in config.yaml:

smtp:
enabled: true
trusted_networks:
- "127.0.0.0/8"
- "192.168.50.0/24"

Or with an environment variable (comma-separated):

Terminal window
SMTP_TRUSTED_NETWORKS=127.0.0.0/8,192.168.50.0/24

Setting smtp.trusted_networks replaces the default list, so include loopback and any ranges you still need. If you widen the list to include public IP space, the agent logs a warning at startup — the gateway is designed for LAN use only.

The gateway requires SMTP AUTH. Two mechanisms are advertised: PLAIN and LOGIN (used by older clients).

FieldValue
Usernameanything (the agent ignores it)
Passwordyour notification API key (same key the HTTP notify API uses)

There’s a single shared secret across both the HTTP API and the SMTP gateway. Rotating the notification API key from the dashboard rotates it for both surfaces; there’s nothing extra to update SMTP-side.

Read or rotate the key from app.atreolink.com → your server’s Settings → Notification API key, or from the host:

Terminal window
docker exec atreoagent cat /var/lib/atreoagent/notify_api_key

Some apps (notably Grafana) refuse to send AUTH over a plaintext SMTP connection. For those, enable STARTTLS so the AUTH exchange is encrypted in transit:

smtp:
enabled: true
listen: "192.168.1.10:2525"
tls_enabled: true

Or with an environment variable:

Terminal window
SMTP_TLS_ENABLED=true

On first start the agent generates a self-signed certificate and persists it under the data dir as smtp_tls_cert.pem + smtp_tls_key.pem. Subsequent starts reuse the same cert. The cert isn’t pinned to a hostname — the LAN bind varies per deployment — so calling apps must skip certificate verification (Grafana: skip_verify = true; most other SMTP clients have an equivalent toggle).

The STARTTLS listener requires TLS 1.3, so the calling app must support TLS 1.3.

This is opportunistic encryption, not server authentication. AUTH (password = notify API key) is what proves you’re talking to the right server.

The agent matches the SMTP RCPT TO address against members’ atreoLINK account emails. To send a notification to a particular member, use their atreoLINK email as the recipient:

From: sonarr@home.example.com
To: alice@example.com <- Alice's atreoLINK account email
Subject: Episode downloaded

If no member matches, the SMTP server returns 550 unknown recipient and drops the message. Matching is case-insensitive and whitespace-trimmed.

By default, mail addressed to anything that isn’t a member’s email is rejected at RCPT TO. Set smtp.catch_all to a fallback recipient and that mail is delivered to them instead of bouncing:

smtp:
enabled: true
catch_all: "alerts@example.com" # an existing member's atreoLINK email

Or with an environment variable:

Terminal window
SMTP_CATCH_ALL=alerts@example.com

The catch-all address must belong to an existing member. A push notification is end-to-end encrypted (sealed-box) to a member’s identity key, so a member is the only address the agent can actually deliver to. If smtp.catch_all is set to an address that isn’t a member, unmatched mail is still rejected with 550 and the agent logs a warning — a misconfigured catch-all is surfaced, not silently dropped.

When a message is delivered through the catch-all, the address the sender originally used is surfaced in the notification’s From line, rendered as original-sender → original-recipient, so the receiving member can tell which mailbox the mail actually targeted:

From: grafana@home.example.com → oncall@example.com

Catch-all matching is case-insensitive and whitespace-trimmed, the same as normal recipient routing.

LimitDefaultOverride
Max message size1 MiBSMTP_MAX_MESSAGE_BYTES
Rate per source IP50 / minuteSMTP_RATE_PER_MINUTE
Recipients per message1not configurable
Attachmentsrejectednot configurable

The SMTP gateway also shares the per-recipient notification cap with the HTTP API: 60 notifications per minute per member, summed across every server and every paired device. Mail that would push a recipient over that line is rejected at delivery time. See Rate limits.

Multipart messages (text/html + text/plain) are forwarded as-is.

Most self-hosted apps with an SMTP setting need:

FieldValue
Hostthe agent server’s LAN IP (e.g. 192.168.1.10)
Port2525
EncryptionNone — or STARTTLS if you’ve enabled it
AuthenticationAUTH PLAIN or AUTH LOGIN
Usernameanything (e.g. atreoagent)
Passwordyour notification API key
Fromanything (not validated)
Tothe member’s atreoLINK account email

In Control Panel → Notification → Email:

Outgoing email service Custom SMTP server
SMTP server 192.168.1.10
SMTP port 2525
Authentication required on
Username atreoagent
Password <your notification API key>
Send email as dsm@home.example.com

Enter the member’s atreoLINK email under Recipient’s email in the System tab.

Email fieldNotification field
SubjectTitle
Body (HTML if present, plaintext otherwise)Body
HTML partHTML view in the inbox
Plaintext partPlaintext view in the inbox
From: headerSender metadata, shown in the inbox
Subject: headerSubject metadata, shown in the inbox

Notifications are encrypted to the recipient’s atreoLINK identity key before atreoAGENT relays them. The relay never sees plaintext.

535 5.7.8 authentication failed. The password didn’t match. Check the notification API key in the dashboard; if you’ve rotated it recently, update the SMTP configuration on the calling app.

530 5.7.0 authentication required. The app sent MAIL FROM without authenticating first. Enable AUTH in the app’s SMTP settings.

550 unknown recipient. The recipient address doesn’t match any member’s atreoLINK email. Check the email address (not the display name). If you’ve set a catch-all and still see this, the smtp.catch_all value itself isn’t a member’s email — the agent logs a warning naming the offending address.

452 too many messages. You hit the per-source-IP rate limit. Raise SMTP_RATE_PER_MINUTE or batch fewer messages.

554 attachments not supported. Strip attachments at the source. The bridge is for notifications, not for delivering files.

Mail accepted, no notification. Check the agent logs for dispatch failed. The most common cause is that the recipient hasn’t paired a phone or browser yet, so there’s no destination for the notification.

WireGuard is a registered trademark of Jason A. Donenfeld.