Skip to content

Fix/cross account move#680

Merged
rathlinus merged 3 commits into
bulwarkmail:mainfrom
hildebrandttk:fix/cross-account-move
Jul 24, 2026
Merged

Fix/cross account move#680
rathlinus merged 3 commits into
bulwarkmail:mainfrom
hildebrandttk:fix/cross-account-move

Conversation

@hildebrandttk

@hildebrandttk hildebrandttk commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

"Move to" a folder in another account (own ↔ delegated/shared) was a no-op — the
message stayed put. Drag-and-drop already handled this; the context menu didn't.

Changes

  • moveToMailboxCrossAware (stores/email-store.ts): detect a cross-account
    destination (own and shared mailboxes both carry accountId) and route through
    the drag-and-drop crossAccountMoveEmails pipeline; else single-account move.
  • When one client reaches both accounts (delegated/shared), move via a
    server-side JMAP Email/copy (copyEmailAcrossAccounts) instead of blob
    copy+import. The blob path stays only for separate cross-server login accounts.
  • copyEmailAcrossAccounts carries the source keywords into the copy (Email/copy
    drops them otherwise → arrived unread) and requests onSuccessDestroyOriginal
    to remove the source. Note: Stalwart currently destroys the copy's create-id
    instead of the source id, so the original is left behind until that is fixed
    upstream (reported: support.stalw.art Support #1150). Our code self-heals once fixed.

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

@hildebrandttk
hildebrandttk force-pushed the fix/cross-account-move branch from 289331c to 800fdd3 Compare July 23, 2026 17:47
Moving a message to a folder in another account (own ↔ delegated/shared) via the
"Move to" context menu was a no-op — the handlers always issued a single-account
Email/set, which can't move between JMAP accounts. Drag-and-drop already routed
these correctly; the context menu never did.

Add moveToMailboxCrossAware: it detects a cross-account destination (own and
shared mailboxes both carry accountId) and routes through the drag-and-drop
crossAccountMoveEmails pipeline, else falls back to the single-account move.

Fix the pipeline for delegated folders too: a client can't stage a blob in a
delegated account (Blob/upload → blobNotFound), so importing into a shared folder
failed. When one client reaches both accounts, use a server-side JMAP Email/copy
(+ destroy original) instead of blob copy+import; the blob path is kept only for
separate cross-server login accounts. Adds client.copyEmailAcrossAccounts.

Unit tests for the dispatch; the two 08-shared-moves specs are un-pinned. Full
docker integration suite green (37 passed).
Moving a message across the account boundary (own ↔ shared folder, or
between two owners' shared folders) left the original in the source
folder and showed the moved copy as unread. Same-account moves were fine.

Cause (verified against a live Stalwart):
- Email/copy drops keywords unless the create sets them, so the copy lost
  $seen and arrived unread.
- onSuccessDestroyOriginal is unreliable — the implicit destroy reports
  notFound and leaves the original behind (flaky), so the move duplicated.

Fix (copyEmailAcrossAccounts): read the source keywords and carry them
into the Email/copy create, then destroy the original with an explicit
Email/set on the source account instead of onSuccessDestroyOriginal.

Tests: 08-shared-moves now asserts the source is gone and the read state
survives on every cross-account case, and adds a cross-owner shared →
shared move (alice's folder → bob's folder). Confirmed red on the old
code (3 cross-account cases fail), green with the fix.
The explicit Email/set destroy workaround for the duplicate-on-move bug is
removed now that the root cause is filed upstream (support.stalw.art #1150:
onSuccessDestroyOriginal destroys the copy's create-id instead of the source
id). copyEmailAcrossAccounts keeps requesting onSuccessDestroyOriginal, so the
move self-heals once Stalwart ships the fix.

Kept: the keyword-preservation fix (carry the source keywords into Email/copy)
so the moved message keeps its read state.

Tests: 08-shared-moves still asserts delivery + read-state on every
cross-account case; the source-removal checks are re-pinned test.fail, scoped
to a nested describe, until #1150 is fixed. Suite green (5 pass, 3 expected-fail).
@hildebrandttk
hildebrandttk force-pushed the fix/cross-account-move branch from 800fdd3 to b48b6e0 Compare July 24, 2026 16:31
@rathlinus
rathlinus merged commit 83cd675 into bulwarkmail:main Jul 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