AgentOwl
SMTP Server

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 Worker

Each 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.dev routes 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>              # apply

Requires 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 --once

Checks: 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

VariableOverrides
SENDMAILRS_HOSTNAMEserver.hostname
SENDMAILRS_SPOOL_DIRspool.directory
SENDMAILRS_WORKER_API_URLworker_api.base_url
SENDMAILRS_WORKER_API_TOKENworker_api.api_token
SENDMAILRS_LISTENserver.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 TTL

DNS setup

Four DNS records are required. The setup_dns.py script creates the first three via Cloudflare API; the PTR must be set manually.

#TypeNameValueNotes
1Amx.agentowl.dev<vps-ip>Not proxied (grey cloud)
2MXinbound.agentowl.devmx.agentowl.dev (priority 10)Route email to the VPS
3TXTinbound.agentowl.devv=spf1 mx -allSPF record
4PTR<vps-ip>mx.agentowl.devSet 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/metrics

Health check

# Single check
./scripts/health_check.sh --once

# Continuous monitoring (default: 30s interval)
./scripts/health_check.sh --interval 60

Checks performed:

  1. systemd service active
  2. Port 25 listening
  3. Metrics port 9090 listening
  4. DNS A record resolves correctly
  5. DNS MX record points to mx host
  6. DNS PTR (reverse DNS) matches
  7. SPF record present
  8. SMTP banner responds with 220

Logs

journalctl -u sendmailrs -f              # live structured JSON logs
journalctl -u sendmailrs --since "1h ago" # recent logs

Set log level via the AGENTOWL_LOG environment variable (default: info).

Relationship to AgentOwl Worker

sendmailrs is the inbound email gateway for the AgentOwl platform:

  1. sendmailrs receives email on port 25, authenticates it, and POSTs a normalized JSON payload to POST /ingress/smtp on the AgentOwl Worker (Cloudflare Workers).

  2. AgentOwl Worker validates the internal token, persists the email event to D1, and enqueues it for tenant webhook delivery.

  3. Inbox UI at {tenant}.agentowl.dev/inbound/email displays 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.

On this page