From be97c8bf08d96cfac25d027aa218825d24e600b6 Mon Sep 17 00:00:00 2001 From: zisisvakras Date: Sun, 16 Aug 2026 20:26:49 +0300 Subject: [PATCH] fix(jmap): remove keywords with null rather than false in Email/set markAsRead, batchMarkAsRead and toggleStar cleared a keyword by patching it to false, sending keywords/$seen: false or keywords/$flagged: false. RFC 8620 section 5.3 removes a property from a PatchObject by setting it to null, and RFC 8621 section 4.1.1 defines the keywords map as holding true for every key, so false is not a valid way to clear one. A server that enforces this rejects the update with invalidProperties. The client does not inspect notUpdated, so marking a message unread or removing a star looked like it had worked, the sidebar counts moved, and the next refresh restored the old state from the server. Stalwart accepts both forms, which is why this went unnoticed. Send null instead, matching removeKeyword and the integration test helpers, which already patch keywords this way. --- lib/jmap/client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index 83c87b166..246ab4f74 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -1705,7 +1705,7 @@ export class JMAPClient implements IJMAPClient { accountId: targetAccountId, update: { [emailId]: { - "keywords/$seen": read, + "keywords/$seen": read ? true : null, }, }, }, "0"], @@ -1716,7 +1716,7 @@ export class JMAPClient implements IJMAPClient { if (emailIds.length === 0) return; for (const batch of batched(emailIds, this.getMaxObjectsInSet())) { - const updates = Object.fromEntries(batch.map(id => [id, { "keywords/$seen": read }])); + const updates = Object.fromEntries(batch.map(id => [id, { "keywords/$seen": read ? true : null }])); await this.request([ ["Email/set", { accountId: accountId || this.accountId, update: updates }, "0"], ]); @@ -1729,7 +1729,7 @@ export class JMAPClient implements IJMAPClient { accountId: accountId || this.accountId, update: { [emailId]: { - "keywords/$flagged": starred, + "keywords/$flagged": starred ? true : null, }, }, }, "0"],