diff --git a/stores/__tests__/email-store-unified-actions.test.ts b/stores/__tests__/email-store-unified-actions.test.ts index b93e3ea9..38309b59 100644 --- a/stores/__tests__/email-store-unified-actions.test.ts +++ b/stores/__tests__/email-store-unified-actions.test.ts @@ -54,6 +54,8 @@ function makeClient() { toggleStar: vi.fn().mockResolvedValue(undefined), moveEmail: vi.fn().mockResolvedValue(undefined), batchMarkAsRead: vi.fn().mockResolvedValue(undefined), + batchDeleteEmails: vi.fn().mockResolvedValue(undefined), + batchMoveEmails: vi.fn().mockResolvedValue(undefined), } as unknown as IJMAPClient; } @@ -180,6 +182,40 @@ describe('unified-view single-email action routing (#281)', () => { expect(activeClient.toggleStar).toHaveBeenCalledWith('email-shared', true, 'owner-x'); }); + it('decrements a shared/group folder counter when deleting from the unified view', async () => { + // Real app: the active account's `mailboxes` includes its delegated shared + // folders (namespaced id + originalId + owner accountId). Unified-fetched + // shared emails carry the owner's BARE mailboxIds and sourceAccountId=owner. + // Regression: emailInMailbox missed these, so the shared folder's badge + // stayed at its old value after deleting in All mail / All unread. + useEmailStore.setState({ + mailboxes: [ + makeMailbox({ id: 'a-inbox', role: 'inbox', unreadEmails: 2, totalEmails: 5 }), + makeMailbox({ + id: 'owner-x:x-inbox', originalId: 'x-inbox', name: 'Shared Inbox', + role: 'inbox', isShared: true, accountId: 'owner-x', + unreadEmails: 4, totalEmails: 10, + }), + ], + emails: [ + makeEmail({ + id: 'email-shared', accountId: 'owner-x', + sourceClientAccountId: 'account-a', sourceAccountId: 'owner-x', + keywords: {}, // unread + mailboxIds: { 'x-inbox': true }, // BARE owner id (not namespaced) + }), + ], + selectedEmailIds: new Set(['email-shared']), + }); + + await useEmailStore.getState().batchDelete(activeClient, true); + + expect(activeClient.batchDeleteEmails).toHaveBeenCalledWith(['email-shared'], 'owner-x'); + const shared = useEmailStore.getState().mailboxes.find(m => m.id === 'owner-x:x-inbox')!; + expect(shared.unreadEmails).toBe(3); // was 4 + expect(shared.totalEmails).toBe(9); // was 10 + }); + it('still uses the active/passed client outside unified view', async () => { useEmailStore.setState({ isUnifiedView: false, diff --git a/stores/email-store.ts b/stores/email-store.ts index 9d80022a..8f3c9f7d 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -507,19 +507,32 @@ async function refreshMailboxesForViewingAccount(fallbackClient: IJMAPClient): P } // Whether an email belongs to a given mailbox, for local counter math. -// Shared/group-account emails carry NAMESPACED mailboxIds (`${ownerId}:${origId}`, -// which equals the shared mailbox's `id`), while own-account emails carry bare ids -// (equal to both `id` and `originalId`). Matching `mailbox.id` covers both; the -// `originalId` fallback is restricted to non-shared mailboxes so a bare own-account -// id can't collide with another account's shared folder. (#281) +// Own-account emails carry bare ids (equal to both `id` and `originalId`). +// Shared/group emails fetched for the unified/cross views are decorated by +// lib/unified-mailbox.ts WITHOUT namespacing their `mailboxIds`, so they carry +// the owner's BARE JMAP ids while the shared mailbox is stored with a namespaced +// `id` (`${ownerId}:${origId}`), `isShared: true` and `originalId`/`accountId` +// (the owner). Matching only `mailbox.id` therefore missed every shared email, +// so a shared folder's counter never moved when mail was deleted/moved/read from +// the unified views. We match shared mailboxes via `originalId` too, but scope it +// to the owning account (`sourceAccountId === mailbox.accountId`) so a bare owner +// id can't collide with another account's folder. (#281, shared-counter fix) function emailInMailbox( - email: { mailboxIds?: Record }, + email: { mailboxIds?: Record; sourceAccountId?: string }, mailbox: Mailbox, ): boolean { const ids = email.mailboxIds; if (!ids) return false; if (ids[mailbox.id]) return true; if (!mailbox.isShared && mailbox.originalId) return !!ids[mailbox.originalId]; + if ( + mailbox.isShared && + mailbox.originalId && + email.sourceAccountId && + email.sourceAccountId === mailbox.accountId + ) { + return !!ids[mailbox.originalId]; + } return false; }