fix(email): preserve read state and remove source on cross-account move
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.
This commit is contained in:
+21
-2
@@ -6400,12 +6400,18 @@ export class JMAPClient implements IJMAPClient {
|
||||
toAccountId: string,
|
||||
destMailboxId: string,
|
||||
): Promise<string> {
|
||||
// Email/copy drops keywords unless the create sets them, so carry the
|
||||
// source's over — otherwise the moved message shows up as unread.
|
||||
const srcResp = await this.request([
|
||||
["Email/get", { accountId: fromAccountId, ids: [emailId], properties: ["keywords"] }, "0"],
|
||||
]);
|
||||
const keywords = srcResp.methodResponses?.[0]?.[1]?.list?.[0]?.keywords ?? {};
|
||||
|
||||
const response = await this.request([
|
||||
["Email/copy", {
|
||||
fromAccountId,
|
||||
accountId: toAccountId,
|
||||
create: { c: { id: emailId, mailboxIds: { [destMailboxId]: true } } },
|
||||
onSuccessDestroyOriginal: true,
|
||||
create: { c: { id: emailId, mailboxIds: { [destMailboxId]: true }, keywords } },
|
||||
}, "0"],
|
||||
]);
|
||||
const res = response.methodResponses?.[0]?.[1];
|
||||
@@ -6417,6 +6423,19 @@ export class JMAPClient implements IJMAPClient {
|
||||
if (!id) {
|
||||
throw new Error("Email/copy succeeded but no ID returned");
|
||||
}
|
||||
|
||||
// onSuccessDestroyOriginal is unreliable on Stalwart (implicit destroy reports
|
||||
// notFound and leaves the original behind), so remove the source explicitly.
|
||||
const delResp = await this.request([
|
||||
["Email/set", { accountId: fromAccountId, destroy: [emailId] }, "0"],
|
||||
]);
|
||||
const notDestroyed = delResp.methodResponses?.[0]?.[1]?.notDestroyed?.[emailId];
|
||||
if (notDestroyed) {
|
||||
throw new Error(
|
||||
notDestroyed.description || notDestroyed.type ||
|
||||
"Copied email but failed to remove the original from the source folder",
|
||||
);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user