quorate/docs/spec/authoritative-email-server-spec.md

177 lines
13 KiB
Markdown

# Authoritative Email Server Specification
## 1. Scope
Stalwart is the authoritative email server for `quorate.dev`. It keeps hosted delivery inside the cluster and submits external delivery through OCI Email Delivery.
This specification owns the Stalwart deployment, configuration, routing, storage, protocol, identity, and filtering boundaries.
## 2. Topology
```mermaid
flowchart LR
Ingress["Declared ingress surfaces"]
Stalwart["Stalwart\n≥2 replicas"]
Controller["Configuration controller\nexactly one owner"]
PG["PostgreSQL\ndurable state"]
S3["VersityGW\ndurable blobs"]
Cache["DragonflyDB\nnon-durable state"]
OCI["OCI Email Delivery"]
Hosted["Hosted mailbox"]
External["External recipient"]
Ingress --> Stalwart
Controller --> Stalwart
Stalwart --> PG
Stalwart --> S3
Stalwart --> Cache
Stalwart --> Hosted
Stalwart -->|"authenticated TLS"| OCI
OCI -->|"best-effort TLS"| External
```
## 3. Configuration Ownership
```mermaid
stateDiagram-v2
[*] --> Empty
Empty --> RecoveryAccess: one Job starts
RecoveryAccess --> RecoveryAccess: retry after interruption
RecoveryAccess --> Reconciled: controller applies ordinary desired state
Reconciled --> Ownerless: Job, controller, and recovery workload removed
Ownerless --> Steady: replicas and controller start
Steady --> Steady: continuous reconciliation
```
| Owner | Authority |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Git | All readable, non-secret desired state |
| Vault | Service credentials |
| Bootstrap Job | At most one one-shot Job; owns only temporary recovery access and waits for controller readiness |
| Controller | Sole owner of declared Stalwart configuration during initialization and steady operation, plus bounded certificate-ID derivation |
| Terraform | Dependency and external-service state |
| Flux | Bootstrap or normal workload state, never both |
The bootstrap Job contains no Stalwart desired-state plan and performs no configuration mutation. It provides the recovery-authenticated management endpoint while the controller applies the ordinary desired-state plan, then exits after the controller reports successful convergence. No Stalwart `Bootstrap` object is used. The recovery credential and recovery workload are absent before the steady controller starts, where the permanent controller identity is authenticated independently.
The deployment has the following minimum identities:
| Identity | Kind | Authority or use | Identity authority | Hosted mailbox |
| ------------------------- | -------------------- | ------------------------------------------------------------------- | ----------------------------- | -------------- |
| Recovery administrator | Temporary automation | Recovery-authenticated management during bootstrap only | — | None |
| Configuration controller | Automation | Declarative configuration reconciliation | Zitadel | None |
| Alert submitter | Automation | Authenticated cluster-local SMTP submission to the operator mailbox | Zitadel | None |
| Authenticated client test | Automation | TCP/465 and TCP/993 protocol qualification as `[email protected]` | Zitadel | Required |
| Human administrator | Human | Administration and operator mailbox membership | Zitadel | — |
| `[email protected]` | Shared mailbox | Operational alerts and Android shared-folder access | Zitadel-authenticated members | Required |
| `[email protected]` | Test mailbox | Authenticated client, inbound delivery, and routing acceptance | Authenticated client test | Required |
Zitadel provides the human-administrator role. The human administrator is a member of the operator shared mailbox.
Human identity remains Zitadel-owned. Mail clients that cannot use the Zitadel access token through `OAUTHBEARER` or `XOAUTH2` use a per-user Stalwart app password as a revocable protocol credential.
The controller:
| Field | Contract |
| ------------------ | ------------------------------------------------------------------------ |
| Ownership | PostgreSQL advisory lock; a competing owner performs no reconciliation |
| Interval | 5 minutes |
| Timeout | 2 minutes per reconciliation |
| Retry | Exponential backoff from 5 seconds to 5 minutes |
| Persistent failure | Unready after 5 consecutive failures while retries continue |
| Desired input | Git state plus runtime-projected secret values |
| Derived input | Server certificate ID resolved from its declared SAN |
| Secret rotation | Vault-owned service credentials converge after rotation |
| Pruning | Exhaustive declared configuration is pruned; accounts are upserted |
| Status | Liveness, readiness, last success, persistent failure, and plan revision |
Manual API patches, generated snapshot adoption, and live `kubectl` mutation are not deployment or configuration mechanisms. Break-glass mutation is limited to failure response and teardown.
| App-password frontend | Contract |
| --------------------- | --------------------------------------------------------------- |
| Implementation | Custom UI backed by Zitadel login and Stalwart app-password API |
| Scope | Signed-in user's app passwords |
| Endpoint | `mail.quorate.dev/account/` |
| Ingress | Gateway R and direct Stalwart frontend |
| TLS | Gateway R certificate on both ingress paths |
| Root redirect | HTTPS `/` → `/account/` |
## 4. Protocol Boundary
| Surface | Required listener availability | TLS | Authentication |
| --------------------------------------------- | -------------------------------- | ----------------------------- | -------------------------------------- |
| SMTP TCP/25 | Public-ingress target | Opportunistic STARTTLS | AUTH prohibited |
| Submissions TCP/465 | Public-ingress target | Implicit TLS | Zitadel token or per-user app password |
| Submission TCP/587 | Cluster-local | Cilium transparent encryption | Vault-backed automation authentication |
| IMAPS TCP/993 | Public-ingress target | Implicit TLS | Zitadel token or per-user app password |
| JMAP and user HTTPS TCP/443 | Public-ingress and direct target | Required | Zitadel-backed human authentication |
| Administration API at `mail.quorate.internal` | Internal-ingress target | Required outside the cluster | Zitadel-backed human authentication |
## 5. Routing
```mermaid
flowchart LR
Copy["Accepted recipient copy"]
Classify{"In Stalwart's\nhosted-domain set?"}
Local["Local delivery\nPostgreSQL + VersityGW"]
Explicit{"Explicit external\nrecipient or disclosure?"}
Relay["OCI Email Delivery"]
Reject["Reject"]
Copy --> Classify
Classify -->|yes| Local
Classify -->|no| Explicit
Explicit -->|yes| Relay
Explicit -->|no| Reject
```
| Condition | Result |
| -------------------------------------- | -------------------------------------------------------------------------------------------- |
| Hosted account, alias, or mailbox | Deliver or queue entirely inside the cluster |
| Unknown hosted recipient | Reject during SMTP without a later bounce |
| External authenticated recipient | Submit through OCI Email Delivery |
| Mixed recipients | Keep the hosted copy local; submit only the external copy |
| Explicit external forwarding or report | Treat as an external disclosure and minimize quoted original content |
| OCI failure | Keep hosted delivery available and queue external delivery |
| Local-store failure | Queue locally or return `451`; never reclassify as external |
| SMTP pressure | Apply bounded session and concurrency controls; return retryable `451` where SMTP permits it |
| Routing loop | Never convert a hosted destination into external delivery |
Hosted-domain classification comes from Stalwart's authoritative domain set, not suffix matching or a duplicated routing list. Authenticated cluster-local submission to hosted recipients never contacts OCI.
## 6. Storage and Credentials
| Component | Role |
| ----------- | -------------------------------------------------- |
| PostgreSQL | Durable account, metadata, queue, and search state |
| VersityGW | Durable message bodies and attachments |
| DragonflyDB | Disposable Redis-compatible state |
| Vault | Source authority for service credentials |
Stalwart reports SMTP success only after the required PostgreSQL state and VersityGW blob are durable. Dependency failure causes local queueing or a temporary failure without changing the recipient's routing class.
Coordinated PostgreSQL and VersityGW recovery is outside this specification.
## 7. Encryption and Filtering
The server-wide application-encryption capability is enabled.
| Identity | Application-layer encryption policy |
| ---------------------------------------------------- | ------------------------------------------------ |
| Recovery, controller, and alert-submitter identities | Not applicable; none has a mailbox |
| `[email protected]` | Disabled |
| External human account | Enabled by default; explicit per-account opt-out |
Human onboarding records the OpenPGP public key or S/MIME certificate, recovery implications, supported search boundary, spam-training consequences, and append-encryption choice. Delivery and append encryption are verified independently.
Encrypted and plaintext accounts coexist without changing each other's policy. Plaintext accounts retain body search. Encrypted accounts expose only supported header search. Plaintext spam training requires recorded consent.
Stalwart's built-in spam and phishing filter is the only v1 content filter. It runs before application-layer encryption for unauthenticated inbound mail. Authenticated alert submission bypasses inbound spam classification.
Resolver and filter-update concurrency, cache, and timeouts are bounded. Resolver failure does not reject all inbound mail. Filter-update failure keeps the last valid data or enters the declared bounded degraded behavior without opening general egress.
OpenPGP and S/MIME remain available when message content must stay opaque to OCI Email Delivery and subsequent relays.
## 8. Mail Identity
SPF, OCI DKIM, DMARC, the envelope sender, bounce handling, MTA-STS, and TLS reporting align with the `quorate.dev` authority.