Skip to content

fix: stop unified-mailbox from mutating client-returned email objects#487

Merged
rathlinus merged 1 commit into
bulwarkmail:mainfrom
hildebrandttk:fix/unified-mailbox-no-mutation
Jun 24, 2026
Merged

fix: stop unified-mailbox from mutating client-returned email objects#487
rathlinus merged 1 commit into
bulwarkmail:mainfrom
hildebrandttk:fix/unified-mailbox-no-mutation

Conversation

@hildebrandttk

Copy link
Copy Markdown
Contributor

Summary

The cross-account fan-out helpers in lib/unified-mailbox.ts stamp the source
account info (accountId, accountLabel, sourceClientAccountId, sourceAccountId,
sourceFolder) onto every email before merging. main does this by MUTATING each
email object in place:

  email.accountId = account.accountId;
  email.accountLabel = account.accountLabel;
  ... (3 more fields)

Those objects are shared references handed back by the per-account JMAP client.
Mutating them in place could surprise any caller that retained them and corrupt
an account-state snapshot. The cross-account "All accounts" feature (a29c33b)
widened the bug: it carried the original two mutation sites forward and added a
third in the shared/group fan-out, so there are now three sites:

  • lib/unified-mailbox.ts fetchUnifiedEmails
  • lib/unified-mailbox.ts fanOutUnifiedQuery
  • lib/unified-mailbox.ts fanOutCrossQuery (new, from the cross-account feature)

Changes

  • lib/unified-mailbox.ts: at all THREE fan-out sites, decorate shallow copies
    instead of mutating the client's 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);
    

    All five stamped fields are preserved; the merged result is identical, only the
    client's original objects are now left untouched.

  • lib/tests/unified-mailbox.test.ts: flips the existing CHARACTERISATION test
    ("mutates the source email objects in place") into "does NOT mutate the source
    email objects (decorates copies)" - asserts the returned email carries the
    account info while the input object has neither accountId nor accountLabel.

Related Issues

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactor / code quality improvement
  • Chore / dependency update / CI change

Checklist

  • I have read the Contributing Guide
  • My code follows the project's code style and conventions
  • I have run npm run typecheck && npm run lint and there are no errors
  • The build passes (npm run build)
  • I have tested my changes locally
  • [] I have added or updated documentation if needed
  • I have updated translations (locales/) if my changes affect user-facing text
  • I have included screenshots or a screen recording for UI changes

Screenshots / Demo

Notes for Reviewers

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.
@rathlinus rathlinus merged commit 70aaf0a into bulwarkmail:main Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants