fix: route unread-counter update to the email's own account in aggregate views

In a cross-account view, marking a second account's email read/unread updated
the *active* account's folder counter instead of the email's. Two causes: the
optimistic counter update only touched `state.mailboxes` (the active account),
and JMAP mailbox ids can collide across accounts so the id match hit the wrong
folder.

Add applyMailboxCounterUpdate(): route the counter delta to the list that holds
the email's folders — the active account's `mailboxes` (incl. its shared
folders) for active-account/shared emails, otherwise that account's
`accountMailboxes[sourceClientAccountId]` entry. Use it in markAsRead.

Regression test: a 2nd-account email with a colliding inbox id decrements that
account's counter and leaves the active account's untouched.
This commit is contained in:
Stefan Hildebrandt
2026-06-23 19:16:22 +02:00
parent a29c33b50a
commit befee332d2
2 changed files with 63 additions and 16 deletions
@@ -68,6 +68,7 @@ describe('unified-view single-email action routing (#281)', () => {
// account-a is the active login; account-b is a second direct login; the
// active login (account-a) also delegates access to the shared owner 'owner-x'.
useAuthStore.setState({
activeAccountId: 'account-a',
getClientForAccount: (id: string) =>
(id === 'account-b' ? accountBClient : id === 'account-a' ? activeClient : undefined) as never,
} as never);
@@ -124,6 +125,27 @@ describe('unified-view single-email action routing (#281)', () => {
expect(activeClient.moveEmail).not.toHaveBeenCalled();
});
it('updates the unread counter on the emails own account, not the active one (id collision)', async () => {
// Real per-account JMAP ids can collide; here both inboxes use the same id.
useEmailStore.setState({
mailboxes: [makeMailbox({ id: 'inbox', role: 'inbox', unreadEmails: 5 })],
accountMailboxes: {
'account-a': [makeMailbox({ id: 'inbox', role: 'inbox', unreadEmails: 5 })],
'account-b': [makeMailbox({ id: 'inbox', role: 'inbox', unreadEmails: 3 })],
},
emails: [
makeEmail({ id: 'b1', sourceClientAccountId: 'account-b', sourceAccountId: 'account-b', keywords: {}, mailboxIds: { inbox: true } }),
],
});
await useEmailStore.getState().markAsRead(activeClient, 'b1', true);
const s = useEmailStore.getState();
expect(s.accountMailboxes['account-b'][0].unreadEmails).toBe(2); // account-b decremented
expect(s.mailboxes[0].unreadEmails).toBe(5); // active account untouched
expect(s.accountMailboxes['account-a'][0].unreadEmails).toBe(5); // active list untouched
});
it('routes a shared/group email through the delegating login client + owner accountId', async () => {
await useEmailStore.getState().markAsRead(activeClient, 'email-shared', true);
// Reached via account-a's client (the active one), targeting the owner account.