From 5c2ad65c6a8683d0167d248a191f20205db37cc3 Mon Sep 17 00:00:00 2001 From: Konstantinos Togias Date: Fri, 3 Jul 2026 14:58:24 +0300 Subject: [PATCH 1/2] fix(unified-inbox): don't discard every account's envelopes if one account fails fetchEnvelopes() and fetchNextEnvelopes() combine every constituent account's envelope fetch into the unified/priority inbox via Promise.all(), which rejects as soon as any single promise rejects. A single account that's temporarily unreachable (throttled provider, network hiccup, etc.) was therefore enough to make the whole unified mailbox render nothing at all, discarding every other account's already-successful envelopes too. Catches each individual account's fetch before it reaches Promise.all(), so a failing account contributes an empty list instead of failing the whole batch. Same reasoning applied to the pagination path in fetchNextEnvelopes(). Fixes #9072 Signed-off-by: Konstantinos Togias --- src/store/mainStore/actions.js | 15 +++++++ src/tests/unit/store/actions.spec.js | 67 ++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/src/store/mainStore/actions.js b/src/store/mainStore/actions.js index 825551d174..f967d2882d 100644 --- a/src/store/mainStore/actions.js +++ b/src/store/mainStore/actions.js @@ -722,6 +722,12 @@ export default function mainStoreActions() { const mailbox = this.getMailbox(mailboxId) if (mailbox.isUnified) { + // One account's fetch rejecting must not discard every + // other account's already-successful envelopes. Promise.all() + // rejects as soon as any single promise rejects, so a single + // slow/unreachable account used to make the whole unified + // mailbox render nothing at all instead of everything except + // that one account. See #9072. const fetchIndividualLists = pipe( map((mb) => this.fetchEnvelopes({ mailboxId: mb.databaseId, @@ -729,6 +735,9 @@ export default function mainStoreActions() { addToUnifiedMailboxes: false, sort: this.getPreference('sort-order'), view: this.getPreference('layout-message-view'), + }).catch((error) => { + logger.error(`Failed to fetch envelopes for unified constituent mailbox ${mb.databaseId}: ${error}`, { error }) + return [] })), Promise.all.bind(Promise), andThen(map(sliceToPage)), @@ -825,12 +834,18 @@ export default function mainStoreActions() { logger.debug('not enough local envelopes for the next unified page. ' + mbs.length + ' fetches required', { mailboxes: mbs.map((mb) => mb.databaseId), }) + // Same reasoning as fetchEnvelopes() above: one account + // failing must not fail pagination for every other + // account sharing this unified mailbox. return pipe( map((mb) => this.fetchNextEnvelopes({ mailboxId: mb.databaseId, query, quantity, addToUnifiedMailboxes: false, + }).catch((error) => { + logger.error(`Failed to fetch next envelopes for unified constituent mailbox ${mb.databaseId}: ${error}`, { error }) + return [] })), Promise.all.bind(Promise), andThen(() => this.fetchNextEnvelopes({ diff --git a/src/tests/unit/store/actions.spec.js b/src/tests/unit/store/actions.spec.js index 1b57709a23..3ea96a3f75 100644 --- a/src/tests/unit/store/actions.spec.js +++ b/src/tests/unit/store/actions.spec.js @@ -197,6 +197,73 @@ describe('Vuex store actions', () => { }) }) + it('creates a unified page from the accounts that succeed even if another account fails', async () => { + const account13 = { + id: 13, + personalNamespace: '', + mailboxes: [], + } + const account14 = { + id: 14, + personalNamespace: '', + mailboxes: [], + } + + store.addAccountMutation(account13) + store.addAccountMutation(account14) + store.addMailboxMutation({ + account: account13, + mailbox: { + id: 'INBOX', + name: 'INBOX', + databaseId: 21, + accountId: 13, + specialRole: 'inbox', + }, + }) + store.addMailboxMutation({ + account: account14, + mailbox: { + id: 'INBOX', + name: 'INBOX', + databaseId: 31, + accountId: 14, + specialRole: 'inbox', + }, + }) + + store.addEnvelopesMutation = vi.fn() + + MessageService.fetchEnvelopes.mockImplementation(async (accountId) => { + if (accountId === 14) { + throw new Error('account 14 is temporarily unavailable') + } + + return [{ + databaseId: 123, + mailboxId: 21, + uid: 321, + subject: 'msg1', + }] + }) + + const envelopes = await store.fetchEnvelopes({ + mailboxId: UNIFIED_INBOX_ID, + }) + + // The unreachable account (14) contributes nothing, but the + // reachable one (13) still renders instead of the whole unified + // fetch coming back empty. + expect(envelopes).toEqual([ + { + databaseId: 123, + mailboxId: 21, + uid: 321, + subject: 'msg1', + }, + ]) + }) + it('fetches the next individual page', async () => { const msgs1 = reverse(range(30, 40)) const page1 = reverse(range(10, 30)) From 3402fcccbd341067751add5f8c6476332e42dc19 Mon Sep 17 00:00:00 2001 From: Konstantinos Togias Date: Sat, 4 Jul 2026 02:31:46 +0300 Subject: [PATCH 2/2] amend! fix(unified-inbox): don't discard every account's envelopes if one account fails fix(unified-inbox): don't discard every account's envelopes if one account fails Fixes #9072 Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Konstantinos Togias