# Durable synthetic SMTP handoff rehearsal

Phase 3 milestone 13 connects real application approval records to the test-only SMTP adapter **inside disposable databases**. It adds no running application route, worker, production table or sending switch. The public website and preview databases remain untouched.

## Run locally

From `platform`:

```sh
apps/api/.venv/bin/python scripts/check-smtp-handoff.py
```

The command accepts no database, provider, recipient or credential arguments. A clean isolated child creates and migrates a temporary SQLite database, installs two laboratory-only tables, creates synthetic staff and signs them in through the actual API. It prepares replies and obtains approval from a different administrator. Only fixed synthetic addresses and the fake Unix-socket peer are permitted. Inherited private settings and proxies are discarded; external socket/DNS attempts fail the run even when caught. Temporary databases, credentials, certificates and receipts are removed afterwards.

Owner-only reports are saved under `.local/smtp-handoff/`. They contain fixed checkpoint labels, pass/fail status, database kind and schema revision, not session tokens, bodies or credential values. Exit code 0 means every checkpoint passed; 1 means failure; unsupported arguments return 2. Report timestamps/filenames are UTC.

The 14 checkpoints cover four protocol outcomes in each of STARTTLS and implicit TLS, expired untouched claims, missing completion, administrator uncertainty review and three abrupt subprocess exits. The final subprocess exits after the fake peer accepts, writes separate synthetic acceptance evidence, and deliberately skips the ledger completion. Recovery then proves that the durable state prevents replay. This is an application-process interruption rehearsal, not a power-loss or hosted-infrastructure drill.

## Attempt lifecycle

Two tables, `smtp_drill_attempts` and `smtp_drill_events`, exist only in the disposable database. They are separate SQLAlchemy metadata, without a production migration. Installation refuses a populated application database. The prototype is imported only by laboratory scripts/tests; the staff-facing network-free [simulation ledger](EMAIL-SIMULATION.md) remains independent.

1. **Claim:** recheck the authenticated writer, current source/context/staff versions and exact separate-admin approval. Freeze the approved snapshot, approval identity, request hash and a unique claim token. One attempt per approval and actor/retry-key uniqueness prevent duplicate claims. Changed retry parameters are rejected.
2. **Handoff:** commit `handing_off` before contacting the fake peer. A second dispatcher sees the durable state and stops. A failed commit cannot contact the peer.
3. **Final body gate:** after the peer requests DATA, recheck the token, state, deadline, credentials and approved content. Keep the existing conversation/account database locks during the bounded synthetic body exchange. A worker resumed after recovery, logout or a draft edit transmits no body. This laboratory strategy has not been accepted as a production lock/latency design.
4. **Completion:** retain the protocol outcome and frozen bytes. A later context/access change sets `context_changed`; it cannot erase an acceptance already observed. The token/state/deadline fence rejects late completions after recovery.
5. **Recovery:** an expired untouched claim becomes `blocked`; an expired handoff becomes `uncertain`. Neither is replayed. The latter is deliberately conservative, even when a process died before actual network I/O.
6. **Administrator review:** only an active administrator can resolve uncertainty; the event retains reviewer identity. Resolution preserves the outcome and cannot reuse the same approval. A newly prepared and approved revision is required for any further synthetic attempt.

An unresolved claim, handoff or uncertain result also blocks a newer approval in the same conversation. There is no automatic retry. Fake-server acceptance is not real delivery. Recovery races after a body exchange may leave uncertainty, which must not be silently promoted to accepted by a late worker.

The prototype retains a full immutable snapshot in its attempt row and compares the approval history when claiming. This older prototype uses preview identifiers/dates solely as synthetic test content. Final identities are now sealed by approval in milestone 15, and the separate milestone 16 application rehearsal uses those final bytes. The laboratory's admin resolution is a fixed synthetic acknowledgement, not a shipping operator review flow with notes and confirmation of worker shutdown.

## Evidence and limits

The final combined SQLite/local selection passed 104 tests: 34 handoff cases plus 70 protocol-adapter cases. The handoff selection is also run against isolated PostgreSQL schemas, covering concurrent claims/dispatch, stale authority, write failures before/after acceptance, retained evidence, uncertainty holds, recovery fencing and pre-body races. CLI isolation/process-exit cases remain SQLite subprocess checks. See [VALIDATION.md](VALIDATION.md) for exact final PostgreSQL counts and report location.

The existing API approval/inbox routes are exercised in process against synthetic data. No browser/mobile interface or contract changed, so those builds were not rerun. This does not establish live SMTP, physical-device, production latency, delivery, deployment or power-loss behavior.

[Staff-facing outbound attempt history and administrator review](EMAIL-ATTEMPTS.md) are now built as milestone 14. Provider dispatch remains disabled. The [application handoff rehearsal](EMAIL-HANDOFF.md) now uses final sealed identities and actual attempt/recovery records. Production integration still needs recipient policies, bounded transport/lock behavior, private provider activation and authorized live testing.
