> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stateset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# StateSet Mail

> Transactional email API, campaign engine, agent inbox, and MCP server — a single Rust binary on one SQLite database.

# 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](https://stalw.art) 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.

```
   Agent ─HTTP/JSON─▶ stateset-mail ──SMTP──▶ Stalwart ──▶ Internet
                          │            ◀──IMAP──┘            │
                          │                                  │
                          └──webhook──▶ Your service ◀──bounces
```

Written with **Axum** and **Tokio**, it runs as a single Rust binary against one SQLite
database.

## What it covers

<CardGroup cols={2}>
  <Card title="Transactional email" icon="paper-plane">
    Persistent SQLite-backed send queue with exponential-backoff retries, IMAP receive, bounce
    correlation, idempotency keys, and HMAC-signed webhooks.
  </Card>

  <Card title="Marketing automation" icon="bullhorn">
    Profiles, static lists, saved-rule segments, Handlebars and MJML templates, campaigns with
    A/B variants, and event-triggered flows.
  </Card>

  <Card title="Agent inbox" icon="inbox">
    A conversation layer with triage state — status, assignee, tags, read flags — plus FTS5
    full-text search and attachment retrieval.
  </Card>

  <Card title="MCP server" icon="plug">
    Tools, resources, and prompts over stdio or Streamable HTTP. Every marketing and inbox
    operation is agent-callable.
  </Card>
</CardGroup>

## 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-ID`
  we assigned, and the original send is marked `bounced`. Hard (5.x) bounces are auto-added to
  the suppression list.
* **Webhooks** for `message.sent`, `message.failed`, `message.bounced`, and `inbox.received`,
  with HMAC-SHA256 signatures and durable retries.
* **Tool schemas** at `/v1/tools` in 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 `WHERE` for O(matching) evaluation.
* **Templates** — Handlebars merge tags for `subject`, `html_body`, `text_body`, and
  `preheader`. Validated at create time, rendered per recipient at send time. Templates also
  accept `mjml_source` and 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 `variants` array 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-Unsubscribe` and `List-Unsubscribe-Post` headers
  plus a footer link. Honoring it suppresses the address and flips the profile's consent to
  `unsubscribed`.
* **Suppression list** — auto-populated from hard bounces, unsubscribes, and complaints.
  Checked at enqueue *and* at send time.
* **Hosted signup forms** — public `GET /f/:id` renders an HTML form, `POST /f/:id` collects
  subscribers, with optional double opt-in. Admin CRUD under `/v1/forms`.
* **Engagement-based segments** — an `engagement` leaf rule supporting
  `opened | 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_HOPS` tells the tracking routes how far back to walk
  `X-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 `sending` rows back to `pending`, 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`, and
  `send_timezone` on campaigns, plus `timezone` on 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 is `sha256(api_key)[:8]` — enough to attribute, not enough to recover the key.
* **Observability** — `GET /v1/admin/queues` for per-queue status counters and `GET /metrics`
  for Prometheus text-format gauges.

<Note>
  This is a **single-node design**. The transactional worker claims prevent double-firing within
  one process, not across replicas.
</Note>

## Inbound webhook adapters

Each adapter verifies the source's signature, then translates the payload into an event that
drives flows and segments:

| Endpoint                   | Verification                 |
| -------------------------- | ---------------------------- |
| `POST /v1/inbound/shopify` | HMAC-SHA256, base64          |
| `POST /v1/inbound/stripe`  | `Stripe-Signature` v1 scheme |
| `POST /v1/inbound/generic` | Bearer auth                  |

## 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

<CardGroup cols={2}>
  <Card title="Quickstart" icon="rocket" href="/stateset-mail/quickstart">
    Bring up the stack and round-trip a message.
  </Card>

  <Card title="Conversations" icon="comments" href="/stateset-mail/conversations">
    The agent inbox: triage, threading, and search.
  </Card>

  <Card title="MCP server" icon="plug" href="/stateset-mail/mcp-server">
    Tools, resources, and prompts for agents.
  </Card>

  <Card title="Configuration" icon="gear" href="/stateset-mail/configuration">
    Every environment variable.
  </Card>
</CardGroup>
