SMTP Server (sendmailrs)
Deploy and configure sendmailrs — the self-hosted Rust SMTP inbound server for AgentOwl.
sendmailrs is a self-hosted Rust SMTP inbound server for AgentOwl. It listens on port 25, accepts email for tenant subdomains, authenticates messages (SPF/DKIM/DMARC/ARC), and dispatches normalized payloads to the AgentOwl Worker API.
Architecture
sendmailrs processes each inbound message through a 9-module pipeline:
Internet
|
v
TCP :25 --> TLS (STARTTLS)
|
v
SMTP Session (smtp-proto)
|
v
Recipient Validation (subdomain + token routing)
|
v
Spool (disk-backed, TTL cleanup)
|
v
MIME Parse (mail-parser)
|
v
Auth (SPF / DKIM / DMARC / ARC via mail-auth)
|
v
Normalize (canonical JSON envelope)
|
v
Dispatch --> POST /ingress/smtp on AgentOwl WorkerEach module lives in its own directory under src/. The server runs on Tokio with async I/O and graceful shutdown on SIGINT/SIGTERM.
Features
- STARTTLS via tokio-rustls (Let's Encrypt certificates)
- SPF/DKIM/DMARC/ARC verification using
mail-auth - Subdomain-based tenant routing — email to
*@{tenant}.inbound.agentowl.devroutes to the correct tenant - Tokenized address routing for per-address delivery targets
- Rate limiting per source IP using
governor - Prometheus metrics exposed on a configurable endpoint (default
127.0.0.1:9090) - Structured JSON logging via
tracing+tracing-subscriber - Disk-backed spool with configurable TTL and automatic cleanup
- Retry with backoff on Worker dispatch (via
reqwest-retry, up to 3 attempts) - systemd service with security hardening (ProtectSystem, NoNewPrivileges, PrivateTmp)
- Graceful shutdown on SIGINT/SIGTERM
Prerequisites
- Ubuntu 22.04+ VPS (tested on OVHCloud)
- Port 25 open (inbound TCP) — check with your hosting provider
- DNS MX record pointing to the VPS
- TLS certificate (Let's Encrypt, automated by the bootstrap script)
- Rust toolchain (only if building on the VPS with
--build-on-vps)
Quick start / deployment
Deployment is scripted for an OVHCloud VPS. Run from your local machine.
1. Set up SSH access
./scripts/init_ssh_access.sh --server ubuntu@<vps-ip>Generates an ed25519 key (if needed), installs it on the VPS, and saves connection details to .env.scripts.
2. Create DNS records
./scripts/setup_dns.py --vps-ip <vps-ip> --dry-run # preview
./scripts/setup_dns.py --vps-ip <vps-ip> # applyRequires CF_API_TOKEN env var (Cloudflare API token). Creates A, MX, SPF, and DMARC records.
3. Bootstrap the VPS
# Option A: Build on VPS (installs Rust toolchain remotely)
./scripts/bootstrap_remote.sh --build-on-vps \
--worker-url https://api.agentowl.dev \
--internal-token <shared-secret>
# Option B: Upload a pre-built binary
cargo build --release --target x86_64-unknown-linux-gnu
./scripts/bootstrap_remote.sh \
--binary target/x86_64-unknown-linux-gnu/release/sendmailrs \
--worker-url https://api.agentowl.dev \
--internal-token <shared-secret>This creates the sendmailrs system user, installs the binary, writes config, obtains a TLS certificate via certbot, installs the systemd unit, and starts the service.
4. Verify deployment
./scripts/health_check.sh --onceChecks: service status, port 25 listening, metrics port, DNS A/MX/PTR/SPF records, and SMTP banner response.
Configuration
Config is loaded from /etc/sendmailrs/config.toml (override with SENDMAILRS_CONFIG env var). Missing file falls back to defaults.
Environment variable overrides
| Variable | Overrides |
|---|---|
SENDMAILRS_HOSTNAME | server.hostname |
SENDMAILRS_SPOOL_DIR | spool.directory |
SENDMAILRS_WORKER_API_URL | worker_api.base_url |
SENDMAILRS_WORKER_API_TOKEN | worker_api.api_token |
SENDMAILRS_LISTEN | server.listen_addrs (comma-separated) |
Secrets should go in /etc/sendmailrs/env (read by systemd EnvironmentFile).
Full config reference
[server]
listen_addrs = ["0.0.0.0:25"] # Bind addresses
hostname = "mx.agentowl.dev" # SMTP banner / Auth-Results identity
max_connections = 1000 # Concurrent connection limit
max_message_size = 26214400 # 25 MiB
[tls]
cert_path = "/etc/letsencrypt/live/mx.agentowl.dev/fullchain.pem"
key_path = "/etc/letsencrypt/live/mx.agentowl.dev/privkey.pem"
[spool]
directory = "/var/lib/sendmailrs/spool" # Must be writable by service user
ttl_secs = 86400 # 24h auto-cleanup
[dispatch]
max_attempts = 3 # Retry count before dead-lettering
http_timeout_secs = 30 # Per-request timeout
connect_timeout_secs = 10 # TCP connect timeout
[observability]
metrics_listen = "127.0.0.1:9090" # Prometheus scrape endpoint
[recipient]
allowed_domains = ["inbound.agentowl.dev"] # Accepted RCPT TO domains
base_domain = "agentowl.dev"
[worker_api]
base_url = "https://api.agentowl.dev" # Worker API for tenant resolution
api_token = "set-via-env-var" # X-Internal-Token shared secret
cache_ttl_secs = 300 # Tenant endpoint cache TTLDNS setup
Four DNS records are required. The setup_dns.py script creates the first three via Cloudflare API; the PTR must be set manually.
| # | Type | Name | Value | Notes |
|---|---|---|---|---|
| 1 | A | mx.agentowl.dev | <vps-ip> | Not proxied (grey cloud) |
| 2 | MX | inbound.agentowl.dev | mx.agentowl.dev (priority 10) | Route email to the VPS |
| 3 | TXT | inbound.agentowl.dev | v=spf1 mx -all | SPF record |
| 4 | PTR | <vps-ip> | mx.agentowl.dev | Set in OVH control panel (rDNS) |
The script also creates a DMARC record at _dmarc.inbound.agentowl.dev.
For wildcard tenant routing (e.g., *.inbound.agentowl.dev), add a wildcard MX record pointing to mx.agentowl.dev.
Monitoring
Prometheus metrics
Exposed on 127.0.0.1:9090 (configurable). Scrape with Prometheus or access directly:
# From the VPS
curl http://127.0.0.1:9090/metricsHealth check
# Single check
./scripts/health_check.sh --once
# Continuous monitoring (default: 30s interval)
./scripts/health_check.sh --interval 60Checks performed:
- systemd service active
- Port 25 listening
- Metrics port 9090 listening
- DNS A record resolves correctly
- DNS MX record points to mx host
- DNS PTR (reverse DNS) matches
- SPF record present
- SMTP banner responds with 220
Logs
journalctl -u sendmailrs -f # live structured JSON logs
journalctl -u sendmailrs --since "1h ago" # recent logsSet log level via the AGENTOWL_LOG environment variable (default: info).
Relationship to AgentOwl Worker
sendmailrs is the inbound email gateway for the AgentOwl platform:
-
sendmailrs receives email on port 25, authenticates it, and POSTs a normalized JSON payload to
POST /ingress/smtpon the AgentOwl Worker (Cloudflare Workers). -
AgentOwl Worker validates the internal token, persists the email event to D1, and enqueues it for tenant webhook delivery.
-
Inbox UI at
{tenant}.agentowl.dev/inbound/emaildisplays received email events for each tenant.
The shared secret (SENDMAILRS_WORKER_API_TOKEN / X-Internal-Token header) authenticates the sendmailrs-to-Worker connection. Tenant routing is determined by the recipient subdomain: email to anything@acme.inbound.agentowl.dev is routed to tenant acme.