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/repository/AccountRepositoryImpl.kt:97 — high
deleteAccount never removes the account's queued outbox rows, staged attachment files, or their URI grants, and because account IDs are deterministic (normalized email, e.g. "imap:" / "outlook:"), re-adding the same account later resurrects the stale queued message and the next outbox drain silently sends it.
Failure scenario: User queues a message that fails to send (offline), then deletes the account: the outbox row (full recipients, subject, body, bodyHtml) stays in the DB, cacheDir/outbox// files stay on disk, and the row remains visible in the Outbox UI. deleteAccount deletes drafts (draftDao.deleteByAccount) but there is no OutboxDao.deleteByAccount call and no sendScheduler trigger. If the user later re-adds the same address, Account id is identical, so SendWorker.sendQueued's accountDao.getById(entity.accountId) resolves and the months-old message is sent without the user's knowledge (SendWorker only drops the row when the account is still missing at drain time).
Verifier justification (CONFIRMED): deleteAccount (AccountRepositoryImpl.kt:97-124) purges messages, folders, backfill progress, and drafts but never touches the outbox: OutboxDao has no deleteByAccount and OutboxEntity is declared with no ForeignKey, so no cascade fires when the account row is deleted; the queued row, its cacheDir/outbox// staged files, and its URI grants all survive. Account IDs are deterministic by design (Account.outlook: id = "outlook:${normalizeEmailForAccountId(email, lowercaseLocalPart = true)}", commented as intentional per issue #305), so re-adding the same address recreates the exact accountId the orphaned row references. SendWorker.sendQueued only drops orphans when accountDao.getById(entity.accountId) is null at drain time, and SendScheduler.sendNow gates the drain on NetworkType.CONNECTED with exponential backoff — so the concrete trigger is: queue a send while offline (drain never runs), delete the account, re-add the same address, regain network: the first drain resolves the account and silently sends the stale message. Severity high (not critical): the mail goes to the originally-addressed recipients, but is sent without the user's knowledge after they deleted the account — user-visible malfunction with privacy impact.
Defective line:accountDao.deleteById(id) ... messageDao.deleteByAccount(id) folderDao.deleteForAccount(id) backfillProgressDao.deleteForAccount(id) draftDao.deleteByAccount(id) // <- no OutboxDao.deleteByAccount call anywhere in deleteAccount; OutboxEntity has no FK to accounts
Fix hint: Add OutboxDao.getByAccount/deleteByAccount ("DELETE FROM outbox WHERE accountId = :accountId") and, in AccountRepositoryImpl.deleteAccount, enumerate the account's outbox rows before deletion, delete the rows, recursively delete each row's cacheDir/outbox// staging dir, and include their attachment URIs in the attachmentUriGrants.releaseUnreferenced pass alongside the draft URIs.
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/repository/AccountRepositoryImpl.kt:97` — high
deleteAccount never removes the account's queued outbox rows, staged attachment files, or their URI grants, and because account IDs are deterministic (normalized email, e.g. "imap:<email>" / "outlook:<email>"), re-adding the same account later resurrects the stale queued message and the next outbox drain silently sends it.
**Failure scenario:** User queues a message that fails to send (offline), then deletes the account: the outbox row (full recipients, subject, body, bodyHtml) stays in the DB, cacheDir/outbox/<id>/ files stay on disk, and the row remains visible in the Outbox UI. deleteAccount deletes drafts (draftDao.deleteByAccount) but there is no OutboxDao.deleteByAccount call and no sendScheduler trigger. If the user later re-adds the same address, Account id is identical, so SendWorker.sendQueued's accountDao.getById(entity.accountId) resolves and the months-old message is sent without the user's knowledge (SendWorker only drops the row when the account is still missing at drain time).
**Verifier justification (CONFIRMED):** deleteAccount (AccountRepositoryImpl.kt:97-124) purges messages, folders, backfill progress, and drafts but never touches the outbox: OutboxDao has no deleteByAccount and OutboxEntity is declared with no ForeignKey, so no cascade fires when the account row is deleted; the queued row, its cacheDir/outbox/<id>/ staged files, and its URI grants all survive. Account IDs are deterministic by design (Account.outlook: id = "outlook:${normalizeEmailForAccountId(email, lowercaseLocalPart = true)}", commented as intentional per issue #305), so re-adding the same address recreates the exact accountId the orphaned row references. SendWorker.sendQueued only drops orphans when accountDao.getById(entity.accountId) is null at drain time, and SendScheduler.sendNow gates the drain on NetworkType.CONNECTED with exponential backoff — so the concrete trigger is: queue a send while offline (drain never runs), delete the account, re-add the same address, regain network: the first drain resolves the account and silently sends the stale message. Severity high (not critical): the mail goes to the originally-addressed recipients, but is sent without the user's knowledge after they deleted the account — user-visible malfunction with privacy impact.
**Defective line:** `accountDao.deleteById(id)
...
messageDao.deleteByAccount(id)
folderDao.deleteForAccount(id)
backfillProgressDao.deleteForAccount(id)
draftDao.deleteByAccount(id) // <- no OutboxDao.deleteByAccount call anywhere in deleteAccount; OutboxEntity has no FK to accounts`
**Fix hint:** Add OutboxDao.getByAccount/deleteByAccount ("DELETE FROM outbox WHERE accountId = :accountId") and, in AccountRepositoryImpl.deleteAccount, enumerate the account's outbox rows before deletion, delete the rows, recursively delete each row's cacheDir/outbox/<id>/ staging dir, and include their attachment URIs in the attachmentUriGrants.releaseUnreferenced pass alongside the draft URIs.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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/repository/AccountRepositoryImpl.kt:97— highdeleteAccount never removes the account's queued outbox rows, staged attachment files, or their URI grants, and because account IDs are deterministic (normalized email, e.g. "imap:" / "outlook:"), re-adding the same account later resurrects the stale queued message and the next outbox drain silently sends it.
Failure scenario: User queues a message that fails to send (offline), then deletes the account: the outbox row (full recipients, subject, body, bodyHtml) stays in the DB, cacheDir/outbox// files stay on disk, and the row remains visible in the Outbox UI. deleteAccount deletes drafts (draftDao.deleteByAccount) but there is no OutboxDao.deleteByAccount call and no sendScheduler trigger. If the user later re-adds the same address, Account id is identical, so SendWorker.sendQueued's accountDao.getById(entity.accountId) resolves and the months-old message is sent without the user's knowledge (SendWorker only drops the row when the account is still missing at drain time).
Verifier justification (CONFIRMED): deleteAccount (AccountRepositoryImpl.kt:97-124) purges messages, folders, backfill progress, and drafts but never touches the outbox: OutboxDao has no deleteByAccount and OutboxEntity is declared with no ForeignKey, so no cascade fires when the account row is deleted; the queued row, its cacheDir/outbox// staged files, and its URI grants all survive. Account IDs are deterministic by design (Account.outlook: id = "outlook:${normalizeEmailForAccountId(email, lowercaseLocalPart = true)}", commented as intentional per issue #305), so re-adding the same address recreates the exact accountId the orphaned row references. SendWorker.sendQueued only drops orphans when accountDao.getById(entity.accountId) is null at drain time, and SendScheduler.sendNow gates the drain on NetworkType.CONNECTED with exponential backoff — so the concrete trigger is: queue a send while offline (drain never runs), delete the account, re-add the same address, regain network: the first drain resolves the account and silently sends the stale message. Severity high (not critical): the mail goes to the originally-addressed recipients, but is sent without the user's knowledge after they deleted the account — user-visible malfunction with privacy impact.
Defective line:
accountDao.deleteById(id) ... messageDao.deleteByAccount(id) folderDao.deleteForAccount(id) backfillProgressDao.deleteForAccount(id) draftDao.deleteByAccount(id) // <- no OutboxDao.deleteByAccount call anywhere in deleteAccount; OutboxEntity has no FK to accountsFix hint: Add OutboxDao.getByAccount/deleteByAccount ("DELETE FROM outbox WHERE accountId = :accountId") and, in AccountRepositoryImpl.deleteAccount, enumerate the account's outbox rows before deletion, delete the rows, recursively delete each row's cacheDir/outbox// staging dir, and include their attachment URIs in the attachmentUriGrants.releaseUnreferenced pass alongside the draft URIs.