fix(persistence): drafts and outbox live in the wipeable cache DB — user-authored mail lost on key-invalidation wipe #486

Open
opened 2026-07-10 19:14:28 +00:00 by JMR-dev · 0 comments
JMR-dev commented 2026-07-10 19:14:28 +00:00 (Migrated from github.com)

Verified finding(s) from the 2026-07-09 whole-repo multi-agent review (independent finder, then adversarial verifier; verdict CONFIRMED).

Triage: above the cut — fix dispatched immediately; this issue tracks the fix to Done.

app/src/main/kotlin/org/libremail/data/local/LibreMailDatabase.kt:33 — high

Drafts and queued outbox mail — user-authored data that exists nowhere else — live in the cache database that is wiped on encryption-key invalidation, contradicting the file's own 'everything here is re-derivable from the server' invariant.

Failure scenario: User enables app-lock + encrypted cache, composes a draft (or queues a send while offline), then re-enrolls a fingerprint or removes the screen lock. DatabaseKeyStore flags the auth-bound key invalid; on next launch DatabaseProvisioner.runStartupSequence calls DatabaseFiles.clear(context), which deletes libremail.db including the drafts and outbox tables. The design (issue #111) deliberately keeps the user signed in via AccountDatabase, so the wipe looks like a clean re-sync — but the unsent draft and any queued outgoing message are silently and permanently destroyed. Neither drafts nor outbox are ever synced to the server (DraftDao/OutboxDao are Room-only), so nothing can restore them.

Verifier justification (CONFIRMED): The mechanism is fully present in the code. LibreMailDatabase declares OutboxEntity (line 33) and DraftEntity as entities of libremail.db, under a doc comment claiming everything in it is re-derivable from the server. DatabaseProvisioner.runStartupSequence (lines 101-105) wipes exactly that file on key invalidation: if (keyStore.isClearPending()) { DatabaseFiles.clear(context); ... }, and DatabaseFiles.clear does context.deleteDatabase(NAME) with NAME = "libremail.db". Drafts are persisted only via draftDao.upsert (MailRepositoryImpl.kt:551) — a repo-wide grep finds no IMAP APPEND, no Graph createDraft, and no draft/outbox sync anywhere in data/sync, so wiped drafts and still-queued outbox rows are unrecoverable. The issue #111 design comments only guarantee the user stays signed in (AccountDatabase is preserved); they never address drafts/outbox, and the file's own "re-derivable" invariant shows the case was overlooked rather than accepted. Trigger: user with opt-in encrypted cache composes a draft or queues an offline send, then re-enrolls a fingerprint or removes the screen lock — next launch silently deletes the user-authored content. Rated high rather than critical because it requires the opt-in encryption feature plus a screen-lock change while unsent content exists, and scope is limited to unsent drafts/outbox (no corruption of synced mail, no security breach).

Defective line: ` OutboxEntity::class,
DraftEntity::class,
...

  • The offline mail cache. Everything here is re-derivable from the server on a fresh sync
    ...
    if (keyStore.isClearPending()) {
    DatabaseFiles.clear(context)`

Fix hint: Move DraftEntity/OutboxEntity (and their attachment references) out of libremail.db into the never-wiped AccountDatabase (they are user-authored config-like data, matching that file's contract), with a Room migration plus a copy step in AccountDataMigrator; alternatively, before DatabaseFiles.clear in DatabaseProvisioner, export surviving draft/outbox rows and re-insert them after the fresh cache is created. Either way, update LibreMailDatabase's "re-derivable from the server" doc comment to match reality.

Verified finding(s) from the 2026-07-09 whole-repo multi-agent review (independent finder, then adversarial verifier; verdict **CONFIRMED**). **Triage: above the cut — fix dispatched immediately; this issue tracks the fix to Done.** ## `app/src/main/kotlin/org/libremail/data/local/LibreMailDatabase.kt:33` — high Drafts and queued outbox mail — user-authored data that exists nowhere else — live in the cache database that is wiped on encryption-key invalidation, contradicting the file's own 'everything here is re-derivable from the server' invariant. **Failure scenario:** User enables app-lock + encrypted cache, composes a draft (or queues a send while offline), then re-enrolls a fingerprint or removes the screen lock. DatabaseKeyStore flags the auth-bound key invalid; on next launch DatabaseProvisioner.runStartupSequence calls DatabaseFiles.clear(context), which deletes libremail.db including the drafts and outbox tables. The design (issue #111) deliberately keeps the user signed in via AccountDatabase, so the wipe looks like a clean re-sync — but the unsent draft and any queued outgoing message are silently and permanently destroyed. Neither drafts nor outbox are ever synced to the server (DraftDao/OutboxDao are Room-only), so nothing can restore them. **Verifier justification (CONFIRMED):** The mechanism is fully present in the code. LibreMailDatabase declares OutboxEntity (line 33) and DraftEntity as entities of libremail.db, under a doc comment claiming everything in it is re-derivable from the server. DatabaseProvisioner.runStartupSequence (lines 101-105) wipes exactly that file on key invalidation: `if (keyStore.isClearPending()) { DatabaseFiles.clear(context); ... }`, and DatabaseFiles.clear does `context.deleteDatabase(NAME)` with NAME = "libremail.db". Drafts are persisted only via `draftDao.upsert` (MailRepositoryImpl.kt:551) — a repo-wide grep finds no IMAP APPEND, no Graph createDraft, and no draft/outbox sync anywhere in data/sync, so wiped drafts and still-queued outbox rows are unrecoverable. The issue #111 design comments only guarantee the user stays signed in (AccountDatabase is preserved); they never address drafts/outbox, and the file's own "re-derivable" invariant shows the case was overlooked rather than accepted. Trigger: user with opt-in encrypted cache composes a draft or queues an offline send, then re-enrolls a fingerprint or removes the screen lock — next launch silently deletes the user-authored content. Rated high rather than critical because it requires the opt-in encryption feature plus a screen-lock change while unsent content exists, and scope is limited to unsent drafts/outbox (no corruption of synced mail, no security breach). **Defective line:** ` OutboxEntity::class, DraftEntity::class, ... * The offline mail cache. Everything here is re-derivable from the server on a fresh sync ... if (keyStore.isClearPending()) { DatabaseFiles.clear(context)` **Fix hint:** Move DraftEntity/OutboxEntity (and their attachment references) out of libremail.db into the never-wiped AccountDatabase (they are user-authored config-like data, matching that file's contract), with a Room migration plus a copy step in AccountDataMigrator; alternatively, before DatabaseFiles.clear in DatabaseProvisioner, export surviving draft/outbox rows and re-insert them after the fresh cache is created. Either way, update LibreMailDatabase's "re-derivable from the server" doc comment to match reality.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: JMR-dev/LibreMail#486