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

# Mail Configuration

> Complete environment-variable reference for StateSet Mail.

# Mail Configuration

All configuration is by environment variable. See `.env.example` for the full set.

## Core

| Variable                 | Default                 | Notes                                                                                                   |
| ------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------- |
| `STATESET_MAIL_API_KEYS` | *(required)*            | Comma-separated bearer tokens                                                                           |
| `DEFAULT_FROM`           | *(required)*            | Used when a send omits `from`                                                                           |
| `DATABASE_URL`           | `sqlite://data/mail.db` | SQLite file; WAL mode is enabled                                                                        |
| `MAX_BODY_BYTES`         | `26214400` (25 MB)      | HTTP request size cap                                                                                   |
| `PUBLIC_BASE_URL`        | unset                   | Public origin for tracking and unsubscribe URLs. **Required** for opens, clicks, and `List-Unsubscribe` |

## SMTP and IMAP

| Variable                          | Default        | Notes                            |
| --------------------------------- | -------------- | -------------------------------- |
| `SMTP_HOST` / `SMTP_PORT`         | `stalwart:587` | The SMTP server to relay through |
| `SMTP_USERNAME` / `SMTP_PASSWORD` | unset          | SMTP auth                        |
| `SMTP_ENCRYPTION`                 | `starttls`     | `starttls` \| `tls` \| `none`    |
| `IMAP_HOST` / `IMAP_PORT`         | unset / `993`  | Leave blank to disable receive   |
| `IMAP_USERNAME` / `IMAP_PASSWORD` | unset          | IMAP auth                        |
| `IMAP_FOLDER`                     | `INBOX`        |                                  |
| `IMAP_POLL_INTERVAL_SECS`         | `15`           |                                  |

## Sending

| Variable                    | Default | Notes                                        |
| --------------------------- | ------- | -------------------------------------------- |
| `SEND_MAX_ATTEMPTS`         | `8`     | Backoff is jittered exponential              |
| `SEND_INITIAL_BACKOFF_SECS` | `30`    |                                              |
| `DOMAIN_THROTTLE_PER_MIN`   | `120`   | Per-recipient-domain send cap (token bucket) |

## Security and tracking

| Variable              | Default                          | Notes                                                                                              |
| --------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------- |
| `TRACKING_SECRET`     | *first API key*                  | HMAC key signing every `/t/*` URL. Rotate carefully — it invalidates in-flight links               |
| `SESSION_SECRET`      | *derived from `TRACKING_SECRET`* | HMAC key signing dashboard session cookies. Kept distinct so the tracking key can't forge sessions |
| `TRUST_PROXY_HOPS`    | `0`                              | Proxy hops to trust when parsing `X-Forwarded-For` on tracking routes                              |
| `TRACKING_IP_PER_MIN` | `60`                             | Per-IP request cap on `/t/*`                                                                       |

<Warning>
  Leave `TRUST_PROXY_HOPS` at `0` unless the service genuinely sits behind that many proxies —
  trusting more hops than exist lets a client spoof its IP past the tracking rate limit.
</Warning>

## Webhooks

Multiple subscriptions live in a `webhook_subscriptions` table, each with its own URL, HMAC
secret, and event filters supporting wildcards (`"*"`, `"flow.*"`, or exact names).
Subscriptions can be scoped to a tenant. The legacy environment variables below seed a default
subscription on first start.

| Variable                         | Default | Notes                                                               |
| -------------------------------- | ------- | ------------------------------------------------------------------- |
| `WEBHOOK_URL` / `WEBHOOK_SECRET` | unset   | Optional outbound event sink (legacy; seeds a default subscription) |

Events emitted:

* **Transactional** — `message.sent`, `message.failed`, `message.bounced`, `inbox.received`
* **Marketing lifecycle** — `campaign.started`, `campaign.finished`, `profile.subscribed`,
  `profile.unsubscribed`, `message.opened`, `message.clicked`, `flow.run.started`,
  `flow.run.completed`

Payloads are enriched with context: `inbox.received` carries the conversation, mailbox, domain,
and tenant plus attachment metadata; bounce events carry the recipient; engagement events carry
recipient and campaign.

<Note>
  Webhook targets are SSRF-guarded — validated at create time, re-resolved before each send, with
  loopback, private, link-local, and metadata addresses refused and redirects disabled.
</Note>

## Inbound adapters

| Variable                 | Notes                                       |
| ------------------------ | ------------------------------------------- |
| `SHOPIFY_WEBHOOK_SECRET` | Required to use `POST /v1/inbound/shopify`  |
| `STRIPE_WEBHOOK_SECRET`  | Required to use `POST /v1/inbound/stripe`   |
| `INBOUND_GENERIC_SECRET` | Bearer token for `POST /v1/inbound/generic` |

## Managed domains and conversations

| Variable                                      | Default                    | Notes                                                                                 |
| --------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------- |
| `MAIL_DOMAIN`                                 | unset                      | Managed mailbox domain; when set, sends from this domain must be a registered mailbox |
| `MAIL_MX_HOST`                                | `mx.<message-id domain>`   | Host customers point their MX record at                                               |
| `MAIL_SPF_INCLUDE`                            | `_spf.<message-id domain>` | SPF include customers add to their domain                                             |
| `MAIL_DMARC_RUA`                              | unset                      | Optional DMARC aggregate-report address surfaced in the suggested record              |
| `DNS_VERIFY_INTERVAL_SECS`                    | `60`                       | How often the worker re-checks unsettled managed domains                              |
| `STALWART_ADMIN_URL` / `STALWART_ADMIN_TOKEN` | unset                      | MTA admin API for auto-provisioning verified domains. Unset is a logged no-op         |
| `ATTACHMENT_MAX_BYTES`                        | `10485760` (10 MB)         | Max size of an inbound attachment retained for `fetch_attachment`                     |
| `CONVERSATION_MATCH_WINDOW_DAYS`              | `30`                       | Fallback subject + participant threading window when RFC headers are absent           |

## Observability

| Endpoint                 | Purpose                                                                      |
| ------------------------ | ---------------------------------------------------------------------------- |
| `GET /v1/admin/queues`   | Status counters per queue                                                    |
| `GET /metrics`           | Prometheus text-format gauges                                                |
| `GET /v1/admin/activity` | Append-only activity log, filterable — e.g. `?action=campaign.sent&limit=50` |

## Bulk import limits

`POST /v1/profiles/bulk` (JSON array), `POST /v1/profiles/import` (CSV body), and
`POST /v1/lists/:id/members/bulk` are each capped at **1000 records per request**.
