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/ui/mailbox/MailboxViewModel.kt:471 — high
Folder identity is the raw case-sensitive fullName everywhere (message id = "accountId:folder:uid", Room queries, unread counts), but INBOX is case-insensitive per RFC 3501: on a server whose LIST reports "Inbox", background sync (MailSyncer's literal "INBOX") and drawer selection (selectFolder(acct, "Inbox")) write and read two different folder keys for the same mailbox.
Failure scenario: Account on a server reporting the inbox as "Inbox": syncAll/notifications cache rows with id "acct:INBOX:uid" folder="INBOX"; user taps the drawer inbox entry -> selectFolder(acct, "Inbox") syncs the same messages again as "acct:Inbox:uid" folder="Inbox". Every message is duplicated in the DB; the unified inbox (queries 'INBOX') and the per-account inbox (queries 'Inbox') show divergent read/star state (marking read in one leaves the other unread); folder unread badges and drawer selection highlight (fullName == "INBOX" default) are wrong. withInbox() itself matches ignoreCase — acknowledging such servers — then passes the unnormalized fullName into selection/queries.
Verifier justification (CONFIRMED): Every link of the claimed chain is in the code with no counter-guard. ImapClient.listFolders persists the server's raw LIST name ('FetchedFolder(fullName = folder.fullName, ...)', ImapClient.kt:152; the ignore-case check at 161 only injects a missing INBOX, it never canonicalizes an existing 'Inbox'). FolderDrawer.kt:113 passes 'folder.fullName' into selectFolder -> mailSyncer.syncFolder(accountId, "Inbox"), while syncAll/syncAccount (MailSyncer.kt:63/82) use the literal "INBOX". The folder string is the cache identity: Mappers.kt:137 'id = "$accountId:$folder:$uid"', and all MessageDao queries use case-sensitive 'WHERE folder = :folder' / 'GROUP BY accountId, folder' (no COLLATE NOCASE anywhere in the schema). JavaMail opens the same physical mailbox for both spellings (RFC 3501 INBOX case-insensitivity), so on a server whose LIST reports 'Inbox' (e.g. on-prem Exchange IMAP) the same messages are cached twice under two keys: duplicate DB rows, divergent read/star state between the unified inbox (queries 'INBOX') and the per-account inbox (queries 'Inbox'), wrong unread badges, and the drawer highlight (FolderDrawer.kt:95 'folder.fullName == selectedFolder' vs default 'INBOX') never matches. No normalization exists anywhere between LIST and the Room key, and this contradicts none of the known-intentional behaviors.
Defective line:private fun withInbox(accountId: String, folders: List<Folder>): List<Folder> = if (folders.any { it.fullName.equals(INBOX, ignoreCase = true) }) { folders } else { ... } // vs Mappers.kt:137: id = "$accountId:$folder:$uid" // vs MailSyncer.kt:63: syncFolderHeaders(account, INBOX, notify = true) // INBOX = "INBOX" // vs FolderDrawer.kt:113: onSelectFolder(drawerAccount.id, folder.fullName)
Fix hint: Canonicalize the inbox name to "INBOX" at the boundary: in ImapClient.listFolders map any fullName.equals("INBOX", ignoreCase = true) folder to fullName = "INBOX" before returning (and likewise displayName), so the persisted FolderEntity, drawer selection, and every downstream cache key agree with MailSyncer's literal; add a one-time Room migration/cleanup that rewrites existing mixed-case inbox rows (messages.folder, message ids, folders.fullName) to the canonical key.
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/ui/mailbox/MailboxViewModel.kt:471` — high
Folder identity is the raw case-sensitive fullName everywhere (message id = "accountId:folder:uid", Room queries, unread counts), but INBOX is case-insensitive per RFC 3501: on a server whose LIST reports "Inbox", background sync (MailSyncer's literal "INBOX") and drawer selection (selectFolder(acct, "Inbox")) write and read two different folder keys for the same mailbox.
**Failure scenario:** Account on a server reporting the inbox as "Inbox": syncAll/notifications cache rows with id "acct:INBOX:uid" folder="INBOX"; user taps the drawer inbox entry -> selectFolder(acct, "Inbox") syncs the same messages again as "acct:Inbox:uid" folder="Inbox". Every message is duplicated in the DB; the unified inbox (queries 'INBOX') and the per-account inbox (queries 'Inbox') show divergent read/star state (marking read in one leaves the other unread); folder unread badges and drawer selection highlight (fullName == "INBOX" default) are wrong. withInbox() itself matches ignoreCase — acknowledging such servers — then passes the unnormalized fullName into selection/queries.
**Verifier justification (CONFIRMED):** Every link of the claimed chain is in the code with no counter-guard. ImapClient.listFolders persists the server's raw LIST name ('FetchedFolder(fullName = folder.fullName, ...)', ImapClient.kt:152; the ignore-case check at 161 only injects a missing INBOX, it never canonicalizes an existing 'Inbox'). FolderDrawer.kt:113 passes 'folder.fullName' into selectFolder -> mailSyncer.syncFolder(accountId, "Inbox"), while syncAll/syncAccount (MailSyncer.kt:63/82) use the literal "INBOX". The folder string is the cache identity: Mappers.kt:137 'id = "$accountId:$folder:$uid"', and all MessageDao queries use case-sensitive 'WHERE folder = :folder' / 'GROUP BY accountId, folder' (no COLLATE NOCASE anywhere in the schema). JavaMail opens the same physical mailbox for both spellings (RFC 3501 INBOX case-insensitivity), so on a server whose LIST reports 'Inbox' (e.g. on-prem Exchange IMAP) the same messages are cached twice under two keys: duplicate DB rows, divergent read/star state between the unified inbox (queries 'INBOX') and the per-account inbox (queries 'Inbox'), wrong unread badges, and the drawer highlight (FolderDrawer.kt:95 'folder.fullName == selectedFolder' vs default 'INBOX') never matches. No normalization exists anywhere between LIST and the Room key, and this contradicts none of the known-intentional behaviors.
**Defective line:** `private fun withInbox(accountId: String, folders: List<Folder>): List<Folder> =
if (folders.any { it.fullName.equals(INBOX, ignoreCase = true) }) {
folders
} else { ... }
// vs Mappers.kt:137: id = "$accountId:$folder:$uid"
// vs MailSyncer.kt:63: syncFolderHeaders(account, INBOX, notify = true) // INBOX = "INBOX"
// vs FolderDrawer.kt:113: onSelectFolder(drawerAccount.id, folder.fullName)`
**Fix hint:** Canonicalize the inbox name to "INBOX" at the boundary: in ImapClient.listFolders map any fullName.equals("INBOX", ignoreCase = true) folder to fullName = "INBOX" before returning (and likewise displayName), so the persisted FolderEntity, drawer selection, and every downstream cache key agree with MailSyncer's literal; add a one-time Room migration/cleanup that rewrites existing mixed-case inbox rows (messages.folder, message ids, folders.fullName) to the canonical key.
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/ui/mailbox/MailboxViewModel.kt:471— highFolder identity is the raw case-sensitive fullName everywhere (message id = "accountId:folder:uid", Room queries, unread counts), but INBOX is case-insensitive per RFC 3501: on a server whose LIST reports "Inbox", background sync (MailSyncer's literal "INBOX") and drawer selection (selectFolder(acct, "Inbox")) write and read two different folder keys for the same mailbox.
Failure scenario: Account on a server reporting the inbox as "Inbox": syncAll/notifications cache rows with id "acct:INBOX:uid" folder="INBOX"; user taps the drawer inbox entry -> selectFolder(acct, "Inbox") syncs the same messages again as "acct:Inbox:uid" folder="Inbox". Every message is duplicated in the DB; the unified inbox (queries 'INBOX') and the per-account inbox (queries 'Inbox') show divergent read/star state (marking read in one leaves the other unread); folder unread badges and drawer selection highlight (fullName == "INBOX" default) are wrong. withInbox() itself matches ignoreCase — acknowledging such servers — then passes the unnormalized fullName into selection/queries.
Verifier justification (CONFIRMED): Every link of the claimed chain is in the code with no counter-guard. ImapClient.listFolders persists the server's raw LIST name ('FetchedFolder(fullName = folder.fullName, ...)', ImapClient.kt:152; the ignore-case check at 161 only injects a missing INBOX, it never canonicalizes an existing 'Inbox'). FolderDrawer.kt:113 passes 'folder.fullName' into selectFolder -> mailSyncer.syncFolder(accountId, "Inbox"), while syncAll/syncAccount (MailSyncer.kt:63/82) use the literal "INBOX". The folder string is the cache identity: Mappers.kt:137 'id = "$accountId:$folder:$uid"', and all MessageDao queries use case-sensitive 'WHERE folder = :folder' / 'GROUP BY accountId, folder' (no COLLATE NOCASE anywhere in the schema). JavaMail opens the same physical mailbox for both spellings (RFC 3501 INBOX case-insensitivity), so on a server whose LIST reports 'Inbox' (e.g. on-prem Exchange IMAP) the same messages are cached twice under two keys: duplicate DB rows, divergent read/star state between the unified inbox (queries 'INBOX') and the per-account inbox (queries 'Inbox'), wrong unread badges, and the drawer highlight (FolderDrawer.kt:95 'folder.fullName == selectedFolder' vs default 'INBOX') never matches. No normalization exists anywhere between LIST and the Room key, and this contradicts none of the known-intentional behaviors.
Defective line:
private fun withInbox(accountId: String, folders: List<Folder>): List<Folder> = if (folders.any { it.fullName.equals(INBOX, ignoreCase = true) }) { folders } else { ... } // vs Mappers.kt:137: id = "$accountId:$folder:$uid" // vs MailSyncer.kt:63: syncFolderHeaders(account, INBOX, notify = true) // INBOX = "INBOX" // vs FolderDrawer.kt:113: onSelectFolder(drawerAccount.id, folder.fullName)Fix hint: Canonicalize the inbox name to "INBOX" at the boundary: in ImapClient.listFolders map any fullName.equals("INBOX", ignoreCase = true) folder to fullName = "INBOX" before returning (and likewise displayName), so the persisted FolderEntity, drawer selection, and every downstream cache key agree with MailSyncer's literal; add a one-time Room migration/cleanup that rewrites existing mixed-case inbox rows (messages.folder, message ids, folders.fullName) to the canonical key.