fix: stop unified-mailbox from mutating client-returned email objects
fetchUnifiedEmails, fanOutUnifiedQuery and the cross-account fanOutCrossQuery stamped accountId/accountLabel/source* directly onto each email object returned by the per-account client. Those objects are shared references; mutating them in place could surprise any caller that retained them (and corrupt an account-state snapshot). Decorate shallow copies instead, at all three fan-out sites. The original fix/unified-mailbox-no-mutation branch predated the cross-account "All accounts" feature and only covered two sites; this re-applies the fix to main's current code, including the third (shared/group) fan-out site, and preserves all five stamped fields. Flips the characterisation test to assert the client's object is left untouched.
This commit is contained in:
committed by
Linus Rath
parent
de56229ef2
commit
70aaf0aac1
+32
-25
@@ -119,16 +119,19 @@ export async function fetchUnifiedEmails(
|
||||
|
||||
const { account, result } = outcome.value;
|
||||
|
||||
// Decorate each email with the source account info.
|
||||
for (const email of result.emails) {
|
||||
email.accountId = account.accountId;
|
||||
email.accountLabel = account.accountLabel;
|
||||
email.sourceClientAccountId = account.clientAccountId;
|
||||
email.sourceAccountId = account.jmapAccountId;
|
||||
email.sourceFolder = resolveSourceFolderName(email, account.mailboxes);
|
||||
}
|
||||
// Decorate each email with the source account info. The per-account client
|
||||
// returns shared object references; decorate shallow copies instead of
|
||||
// mutating them in place so retained callers/snapshots aren't corrupted.
|
||||
const decorated = result.emails.map((email) => ({
|
||||
...email,
|
||||
accountId: account.accountId,
|
||||
accountLabel: account.accountLabel,
|
||||
sourceClientAccountId: account.clientAccountId,
|
||||
sourceAccountId: account.jmapAccountId,
|
||||
sourceFolder: resolveSourceFolderName(email, account.mailboxes),
|
||||
}));
|
||||
|
||||
mergedEmails = mergedEmails.concat(result.emails);
|
||||
mergedEmails = mergedEmails.concat(decorated);
|
||||
totalSum += result.total;
|
||||
if (result.hasMore) {
|
||||
anyHasMore = true;
|
||||
@@ -247,14 +250,16 @@ async function fanOutUnifiedQuery(
|
||||
for (const outcome of results) {
|
||||
if (outcome.status !== 'fulfilled' || outcome.value === null) continue;
|
||||
const { account, result } = outcome.value;
|
||||
for (const email of result.emails) {
|
||||
email.accountId = account.accountId;
|
||||
email.accountLabel = account.accountLabel;
|
||||
email.sourceClientAccountId = account.clientAccountId;
|
||||
email.sourceAccountId = account.jmapAccountId;
|
||||
email.sourceFolder = resolveSourceFolderName(email, account.mailboxes);
|
||||
}
|
||||
mergedEmails = mergedEmails.concat(result.emails);
|
||||
// Decorate shallow copies, not the shared client-returned objects.
|
||||
const decorated = result.emails.map((email) => ({
|
||||
...email,
|
||||
accountId: account.accountId,
|
||||
accountLabel: account.accountLabel,
|
||||
sourceClientAccountId: account.clientAccountId,
|
||||
sourceAccountId: account.jmapAccountId,
|
||||
sourceFolder: resolveSourceFolderName(email, account.mailboxes),
|
||||
}));
|
||||
mergedEmails = mergedEmails.concat(decorated);
|
||||
totalSum += result.total;
|
||||
if (result.hasMore) anyHasMore = true;
|
||||
}
|
||||
@@ -383,14 +388,16 @@ async function fanOutCrossQuery(
|
||||
for (const outcome of results) {
|
||||
if (outcome.status !== 'fulfilled' || outcome.value === null) continue;
|
||||
const { account, result } = outcome.value;
|
||||
for (const email of result.emails) {
|
||||
email.accountId = account.accountId;
|
||||
email.accountLabel = account.accountLabel;
|
||||
email.sourceClientAccountId = account.clientAccountId;
|
||||
email.sourceAccountId = account.jmapAccountId;
|
||||
email.sourceFolder = resolveSourceFolderName(email, account.mailboxes);
|
||||
}
|
||||
mergedEmails = mergedEmails.concat(result.emails);
|
||||
// Decorate shallow copies, not the shared client-returned objects.
|
||||
const decorated = result.emails.map((email) => ({
|
||||
...email,
|
||||
accountId: account.accountId,
|
||||
accountLabel: account.accountLabel,
|
||||
sourceClientAccountId: account.clientAccountId,
|
||||
sourceAccountId: account.jmapAccountId,
|
||||
sourceFolder: resolveSourceFolderName(email, account.mailboxes),
|
||||
}));
|
||||
mergedEmails = mergedEmails.concat(decorated);
|
||||
totalSum += result.total;
|
||||
if (result.hasMore) anyHasMore = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user