The identity migration ran ADD COLUMN, backfill, and CREATE UNIQUE INDEX as
separate autocommit statements with the backfill gated on having just added
the column. A crash between the ADD and the backfill could leave legacy rows
at remote_id='' with the column present, so the next startup skipped the
backfill and the index creation then collided on
(account, folder, uid_validity='', remote_id='') -- wedging the daemon.
Run the whole step in one transaction (a crash rolls back atomically) and make
the backfill unconditional (idempotent UPDATE ... WHERE remote_id = '') so any
committed partial state is completed before the index is built. No-op on a
fresh or already-migrated database.
Refs #281
IMAP UIDs are unique only per mailbox+UIDVALIDITY, yet messages.id was the
sole key and Email.id was "imap-uid-{uid}", so the same UID in two folders or
accounts collapsed to one row and silently overwrote (audit C1). The id shape
also leaked internal encoding on the wire.
Messages now carry an opaque surrogate id = SHA-256 over length-prefixed
(account, folder, uid_validity, remote_id), plus first-class remote_id and
uid_validity columns and a UNIQUE(account, folder, uid_validity, remote_id)
index (backfill-safe migration seeding legacy rows from the old id). The IMAP
and JMAP backends populate remote_id/uid_validity and hash the id; the outbox
executor recovers remote addressing from the columns instead of parsing the
id, still SELECT-verifying UIDVALIDITY before mutating. Re-sync recomputes the
same id, so it upserts one row and the fire-once filter guard still holds.
Message.id stays a proto string; only its value becomes opaque (no proto
change).
Refs #281