Skip to content

fix(email-store): route shared-folder batch actions to the owner account#657

Merged
rathlinus merged 1 commit into
bulwarkmail:mainfrom
dev-hive-kazniisa:fix/shared-folder-batch-action-routing
Jul 21, 2026
Merged

fix(email-store): route shared-folder batch actions to the owner account#657
rathlinus merged 1 commit into
bulwarkmail:mainfrom
dev-hive-kazniisa:fix/shared-folder-batch-action-routing

Conversation

@dev-hive-kazniisa

Copy link
Copy Markdown
Contributor

What

Batch actions performed while viewing a shared/group mailbox directly (the "Shared" sidebar section, not the unified inbox) were dispatched to the logged-in user's own account instead of the shared owner account. This affects:

  • batchDelete (multi-select delete / move-to-trash)
  • batchMoveToMailbox (multi-select move)
  • batchArchive (multi-select archive)
  • batchMarkAsRead (multi-select mark read/unread)

Symptom: the selected messages disappear from the list optimistically, then reappear after a page reload — the server change never persisted. No error is shown.

Why it happens

In the directly-viewed shared folder, the listed emails are undecorated — they carry no sourceAccountId (that field is only set for unified/cross-account views). They are fetched from the owner account through the active login client, and only their mailboxIds are namespaced.

Three of these grouped emails by email.sourceAccountId || '__default__'; undecorated emails all fell into the '__default__' bucket, which routes to the active client with accountId: undefined (i.e. the user's own account) and looks up the trash/destination in the user's own folder list. batchArchive had the same effect for a different reason: it picked the archive folder from the merged mailbox list with no account scoping, and the user's own archive is listed first.

Stalwart then receives an Email/set on the wrong account. Because the ids belong to the shared account, it returns them under updated as null with an unchanged state (a silent no-op, not notUpdated), so the client's notUpdated guard never fires and the UI drops the rows optimistically. On reload they are still there.

This only appears to work when the user's own trash/destination id happens to collide with the shared account's (folder ids are per-account and often line up after folder canonicalization), which masks the bug on some accounts.

The fix

Added resolveViewAccountId(): the owner JMAP accountId of the shared folder currently being viewed (derived from the selected namespaced mailbox), or undefined for a normal own-account view. This mirrors what fetchEmails and the single-email action path (resolveEmailActionContext) already do.

The four batch actions now route undecorated emails to that owner account (reached via the active client) instead of '__default__' → own account. batchMoveToMailbox additionally resolves the destination to its bare originalId (shared folders use namespaced store ids), and batchArchive scopes the archive-folder lookup to that account.

Own-account and unified/cross-account views are unchanged: resolveViewAccountId() returns undefined when the selected folder isn't shared, and the existing per-sourceAccountId grouping still applies in unified view.

Tests

New stores/__tests__/email-store-shared-folder-actions.test.ts covers all four batch actions in the non-unified shared view (asserting they carry the owner accountId + the shared destination), plus an own-account regression guard (accountId stays undefined). Each case fails against the pre-fix code.

npm run typecheck and npm run lint pass.

Scope / not included

This PR does not fix #387 ("can't empty the 'Deleted Items' folder for shared folders"). That report's symptom traces to a separate bug in the single-email delete path: handleDelete in app/(main)/[locale]/page.tsx resolves the trash folder with !m.isShared in the non-unified view, so a single delete from a shared folder targets the user's own Trash instead of the shared account's (it only appears to work when the trash ids collide). Because deleted mail then never lands in the shared account's real Trash, "empty Trash" on that folder has nothing to remove. That fix lives in the UI layer and is better handled as its own change; happy to follow up with it (and file a dedicated issue) separately.

Batch actions (delete, move, archive, mark-as-read) performed while
viewing a shared/group mailbox directly from the "Shared" sidebar section
were dispatched to the user's OWN account instead of the shared owner
account. Emails in that view are undecorated (no sourceAccountId, that is
only set in unified/cross-account views) and are reached through the
active client, so they fell into the '__default__' bucket / non-unified
else-branch, which defaults the JMAP accountId to the active account.
batchArchive independently picked the archive folder from the merged
mailbox list, where the user's own archive is listed first.

Stalwart then applies Email/set to the wrong account: because the ids
belong to the shared account it returns them as `updated: null` with an
unchanged state (a silent no-op, not `notUpdated`), so the UI drops the
rows optimistically and they reappear on the next reload. It only appears
to work when the own and shared folder ids happen to collide.

Add resolveViewAccountId() — the owner accountId of the directly-viewed
shared folder (from the selected namespaced mailbox), undefined for a
normal own-account view, mirroring fetchEmails and the single-email path.
Route the four batch actions to that owner account (via the active
client); batchMoveToMailbox also resolves the destination to its bare
originalId, and batchArchive scopes the archive folder to that account.
Own-account and unified/cross-account views are unchanged.

Adds email-store-shared-folder-actions.test.ts covering all four batch
actions in the non-unified shared view plus an own-account regression.
@rathlinus
rathlinus merged commit 88b07a1 into bulwarkmail:main Jul 21, 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.

[Bug]: I can't empty the 'Deleted Items' folder for shared folders (group account).

2 participants