StateSet Mail
StateSet Mail both sends and receives transactional email and drives automated marketing campaigns — profiles, lists, segments, templates, flows, opens, clicks, list-unsubscribe — on top of any open-source mail server. The included Docker stack uses Stalwart Mail Server for a fully-OSS pipeline. Agents call a small JSON API; StateSet Mail handles the SMTP/IMAP plumbing, persistence, retries, bounce correlation, webhook delivery, template rendering, audience resolution, send throttling, and tracking so the agent doesn’t have to.What it covers
Transactional email
Persistent SQLite-backed send queue with exponential-backoff retries, IMAP receive, bounce
correlation, idempotency keys, and HMAC-signed webhooks.
Marketing automation
Profiles, static lists, saved-rule segments, Handlebars and MJML templates, campaigns with
A/B variants, and event-triggered flows.
Agent inbox
A conversation layer with triage state — status, assignee, tags, read flags — plus FTS5
full-text search and attachment retrieval.
MCP server
Tools, resources, and prompts over stdio or Streamable HTTP. Every marketing and inbox
operation is agent-callable.
Transactional core
- Send via a persistent SQLite-backed queue with exponential-backoff retries — process restarts don’t lose mail.
- Receive via an IMAP polling worker. New messages land in
/v1/inbox/messages. - Bounce correlation — DSN replies are parsed, matched against the outbound
Message-IDwe assigned, and the original send is markedbounced. Hard (5.x) bounces are auto-added to the suppression list. - Webhooks for
message.sent,message.failed,message.bounced, andinbox.received, with HMAC-SHA256 signatures and durable retries. - Tool schemas at
/v1/toolsin both OpenAI and Anthropic formats. - Idempotency keys so a retried tool call doesn’t double-send.
Marketing automation
- Profiles — contacts with arbitrary JSON properties, consent state, and timezone.
- Lists (static) and Segments (saved JSON rule trees over properties and the event
log). Property rules compile down to a single SQL
WHEREfor O(matching) evaluation. - Templates — Handlebars merge tags for
subject,html_body,text_body, andpreheader. Validated at create time, rendered per recipient at send time. Templates also acceptmjml_sourceand auto-compile to inline-styled responsive HTML. - Campaigns — one-shot broadcast to a list or segment, with a per-domain token bucket to avoid hammering a single MX. Live stats: sent, failed, suppressed, opens, unique opens, clicks, unique clicks, unsubscribes, bounces.
- A/B testing — an optional
variantsarray of{label, template_id, weight, subject_override?}. Recipients are hash-bucketed deterministically by(campaign_id, profile_id), so re-runs see the same assignment. Stats split per variant. - Flows — event-triggered automations (
signup→ send welcome → wait 24h → send tips). Multi-step, durable, and they survive restarts. - Open / click tracking — a 1×1 pixel plus a link rewriter that proxies through the service and writes to a tracking event log.
- One-click unsubscribe — RFC 8058
List-UnsubscribeandList-Unsubscribe-Postheaders plus a footer link. Honoring it suppresses the address and flips the profile’s consent tounsubscribed. - Suppression list — auto-populated from hard bounces, unsubscribes, and complaints. Checked at enqueue and at send time.
- Hosted signup forms — public
GET /f/:idrenders an HTML form,POST /f/:idcollects subscribers, with optional double opt-in. Admin CRUD under/v1/forms. - Engagement-based segments — an
engagementleaf rule supportingopened | clicked | sent | bounced | unsubscribed×occurred | did_not_occur | count, optionally scoped to a specific campaign. - Time-series analytics —
GET /v1/campaigns/:id/timeseries?metric=opens|clicks|sent|bounced|unsubscribed&bucket=hour|day.
Production hardening
- HMAC-signed tracking URLs — every
/t/o/,/t/c/,/t/u/URL carries?s=<16-byte hmac>. Unsigned or mismatched requests get a 404 with no DB writes, closing the open-redirect risk on click tracking. - Per-IP tracking rate limit — a token bucket (
TRACKING_IP_PER_MIN) blocks pixel-fetch amplification.TRUST_PROXY_HOPStells the tracking routes how far back to walkX-Forwarded-For; it defaults to 0 (socket peer only). - Transactional worker claims — the campaign and flow workers take SQLite’s RESERVED write lock before flipping status, so workers never double-fire a step.
- Crash recovery — rows a worker claimed but never completed are reclaimed. The outbound
worker reaps stale
sendingrows back topending, mirroring the flow worker’s orphan sweep. Nothing is silently lost on restart. - Graceful shutdown — on SIGINT/SIGTERM the HTTP server drains, then every background worker is signalled and joined before exit.
- SSRF-guarded webhooks — outbound webhook targets are validated at create time and re-resolved before each send. Loopback, private, link-local, and metadata addresses are refused, and redirects are disabled.
- Send-time windows —
send_window_start_hour,send_window_end_hour, andsend_timezoneon campaigns, plustimezoneon profiles. Messages outside the window defer to the next local window start with no attempt recorded. - Activity log — an append-only table capturing
(actor fingerprint, action, resource_type, resource_id, payload)on every mutating handler. The actor issha256(api_key)[:8]— enough to attribute, not enough to recover the key. - Observability —
GET /v1/admin/queuesfor per-queue status counters andGET /metricsfor Prometheus text-format gauges.
This is a single-node design. The transactional worker claims prevent double-firing within
one process, not across replicas.
Inbound webhook adapters
Each adapter verifies the source’s signature, then translates the payload into an event that drives flows and segments:Web dashboard
/dashboard is a three-pane Gmail-like UI — sidebar, list, detail — powered by HTMX, so
clicking a message swaps the detail pane without a full page reload. Tailwind via CDN, no JS
build step.
It covers the Inbox (bounces get a red badge; opening a message marks it seen), Sent
(the full outbound queue with status badges, attempt counts, SMTP response codes, and the
rendered HTML in a sandboxed iframe), Compose, and an Overview home with live
counters and a recent-activity feed.
Auth is an HMAC-signed session cookie: POST /dashboard/login validates the API key against
STATESET_MAIL_API_KEYS and sets a 7-day HttpOnly cookie (Secure when PUBLIC_BASE_URL is
HTTPS). The actor fingerprint matches the activity log, so dashboard-initiated actions show up
attributed.
Next steps
Quickstart
Bring up the stack and round-trip a message.
Conversations
The agent inbox: triage, threading, and search.
MCP server
Tools, resources, and prompts for agents.
Configuration
Every environment variable.