# M6.5 Email Features — Operator Runbook

**Status:** Shipped 2026-04-23 (Steps 1-7 merged to `main` at `cc0449b`).
**ADRs:** 0051 (DB-as-truth, M6 pattern continued), 0052 (disclaimer sieve;
Spike A passed — text/plain + text/html both covered via extracttext + replace).

## What this adds

Six new tabs on the user Mail page at `/jabali-panel/mail`:

| Tab | Stalwart primitive | Per |
|---|---|---|
| Mailboxes | (existing M6) | domain |
| Forwarders | `x:UserAccount.aliases` (alias) + `x:SieveUserScript` (external) | mailbox |
| Autoresponders | `VacationResponse` singleton (RFC 8621 §8) | mailbox |
| Catch-All | `x:Domain.catchAllAddress` | domain |
| Disclaimer | `x:SieveSystemScript` | domain |
| Shared Folders | `Mailbox.shareWith` map | mailbox |
| Logs | `x:Trace/query` + `x:Trace/get` (pass-through) | user-scoped |

## Enabling on a host

```bash
# Run on panel host. Fetches latest main and migrates:
jabali update -f
```

The migrations 000060-000063 land automatically. New columns/tables:

- `email_forwarders` (rows are `alias` or `external`)
- `email_autoresponders` (one row per mailbox)
- `mailbox_shares` (owner → target, JSON Rights)
- `domains.catchall_target`, `domains.disclaimer_enabled`, `domains.disclaimer_text`

## Reconciliation loop

Every 60s (existing reconciler interval) the phase registry runs:

- `catchall` (domain-scoped): pushes `catchall_target` → `x:Domain.catchAllAddress`.
- `disclaimer` (domain-scoped): pushes `disclaimer_enabled + disclaimer_text` →
  `x:SieveSystemScript` named `jabali-disclaimer-<domain>`.
- `autoresponder` (mailbox-scoped): pushes `email_autoresponders` row →
  `VacationResponse/set`.
- `forwarders` (mailbox-scoped): pushes all enabled rows for the mailbox →
  alias list on `x:UserAccount.aliases` + concatenated sieve script
  `jabali-fwds`.
- `mailbox_share` (mailbox-scoped): pushes `mailbox_shares` rows owned by the
  mailbox → `Mailbox.shareWith` patch on the mailbox's INBOX.

Operator-side edits via Stalwart admin console are considered drift and
overwritten on the next tick.

## Manual verification per feature

### Catch-All

1. UI: Mail → Catch-All → Set catch-all → pick domain + target email → Save.
2. On Stalwart: `stalwart-cli get x:Domain <id>` should show
   `catchAllAddress: <target>`.
3. Send a message to a nonexistent local-part on the domain; verify it lands
   in the target mailbox.

### Autoresponders

1. UI: Mail → Autoresponders → Set autoresponder → pick mailbox, set date range,
   enter subject + body.
2. On Stalwart: `stalwart-cli get VacationResponse <account-id> singleton` →
   shows `isEnabled: true` + body.
3. Send from an external account; verify auto-reply.

### Forwarders (alias)

1. UI: Mail → Forwarders → Add → Type=Alias, pick mailbox, local-part = `sales`.
2. On Stalwart: the mailbox's `x:Account/User/aliases` list includes
   `{name: sales, domainId: <domain>}`.
3. Send to `sales@<domain>`; arrives in the target mailbox.

### Forwarders (external)

1. UI: Mail → Forwarders → Add → Type=External, source mailbox, target
   `external@gmail.com`.
2. On Stalwart: `jabali-fwds` sieve script on the account contains
   `redirect :copy "external@gmail.com";`.
3. Send to the source mailbox; arrives at external + source (because `:copy`).

### Disclaimer

**Spike A passed (2026-04-23, ADR-0052 ACCEPTED):** sieve now covers both
`text/plain` AND `text/html` body parts on Stalwart v1.0.0. The rendered
script uses `extracttext` + `variables` + `replace` per RFC 5703. Operator
text is HTML-escaped before being injected into the HTML branch.

1. UI: Mail → Disclaimer → click row → Enable + enter text → Save.
2. On Stalwart: query `x:SieveSystemScript/query { filter: { name: "jabali-disclaimer-<domain>" } }`
   and `x:SieveSystemScript/get` by returned id — shows an active script
   with the rendered template.
3. Send from a mailbox in the domain to an external address; verify disclaimer
   appears appended to both text and HTML alternatives.

### Shared Folders

1. UI: Mail → Shared Folders → Share folder → pick source + target mailbox
   + tick rights (at minimum `mayRead`) → Save.
2. On Stalwart: the source mailbox's INBOX object has `shareWith` map with the
   target principal id → rights object.
3. Log into the target mailbox's webmail (Bulwark via `/mail/sso/<id>`); the
   shared folder should appear with the granted rights.

### Logs

1. UI: Mail → Logs → set date range → page through entries.
2. Scoping check: user A's logs should not include messages to/from user B's
   domains. Verify by signing in as user A and confirming only A's domains
   appear in results.
3. Pagination: `?limit=50&offset=50` → page 2.

## Rollback

Per step. Each step's commit is a revertible unit. To unship:

```bash
# Revert the feature's commit range (example: forwarders)
git revert <step5-commit>..<head>
jabali update -f
```

Migrations 000060-000063 are reversible via their `.down.sql`.

## Troubleshooting

- **Reconciler logs** — `journalctl -u jabali-panel | grep -iE 'phase|autoresponder|catchall|disclaimer|forwarder|share'` shows every tick's
  per-phase run. Errors include the feature name and domain/mailbox id.
- **Stalwart JMAP timeout** — raise via `panel-api` config; phases use 10-15s
  per-call timeout.
- **Empty Logs tab on a user with no domains** — expected; UI paginates empty.
- **"mailbox not yet registered"** from autoresponder/forwarder agent — the
  mailbox account hasn't authed to Stalwart yet (ADR-0045 lazy registry).
  The reconciler retries next tick; first successful auth populates the
  registry.
- **Disclaimer not applied** — check Stalwart supports the sieve extensions
  listed in the script's `require` line (`envelope`, `variables`, `mime`,
  `foreverypart`, `extracttext`, `replace`). Verify by creating a probe
  script with the same `require` list; a compile error ID-tags which
  extension is missing.

## Monitoring

Add a scraper rule (future) on:

- Reconciler tick duration
- Stalwart JMAP failure rate per method
- Disclaimer script apply failure count (operator-visible drift signal)

## Follow-up work

- Playwright E2E specs per feature. Dispatched as a separate PR.
- Admin-shell mirrors of these tabs (ADR-0051 defers admin views to M6.6).
