@remit/mailbox-service 0.0.91 → 0.0.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -239,7 +239,19 @@ export class SpamReportService {
239
239
  // originalMailboxId, so a stale value left by some earlier, unrelated
240
240
  // move would otherwise survive and send a later notSpam to the wrong
241
241
  // folder. This report-spam action established no move of its own.
242
- await this.messageService.clearOriginalMailboxId(messageId);
242
+ //
243
+ // A transition off the row this call read (imap-mutations R3): the pair
244
+ // being cleared belongs to a placement, and a lane that has claimed the
245
+ // row since keeps its own — its settle manages the pair.
246
+ await this.messageService.transitionPlacement(
247
+ messageId,
248
+ {
249
+ status: before.status,
250
+ mailboxId: before.mailboxId,
251
+ uid: before.uid,
252
+ },
253
+ { originalMailboxId: null, originalUid: null },
254
+ );
243
255
  }
244
256
 
245
257
  await this.flagPushService.flip({
@@ -299,7 +311,21 @@ export class SpamReportService {
299
311
  messageId,
300
312
  accountId,
301
313
  );
302
- await this.messageService.clearOriginalMailboxId(messageId);
314
+
315
+ // Re-read, then clear against what was read. The restore just wrote a
316
+ // placement, so the row here is not the one above; predicating on it is
317
+ // what keeps this from erasing a pair some other lane is relying on
318
+ // (imap-mutations R3).
319
+ const restored = await this.messageService.get(messageId);
320
+ await this.messageService.transitionPlacement(
321
+ messageId,
322
+ {
323
+ status: restored.status,
324
+ mailboxId: restored.mailboxId,
325
+ uid: restored.uid,
326
+ },
327
+ { originalMailboxId: null, originalUid: null },
328
+ );
303
329
  }
304
330
 
305
331
  const current = await this.messageService.get(messageId);
@@ -0,0 +1,77 @@
1
+ import type { ThreadMessageItem } from "@remit/data-ports";
2
+
3
+ /**
4
+ * Build the `set` and `composites` payload for the ThreadMessage update on a
5
+ * MESSAGE_DELETE move-to-trash.
6
+ *
7
+ * The CURRENT row state goes in `composites`; the NEW values go in `set`.
8
+ * ElectroDB uses `composites` to run the conditional check on the existing row
9
+ * AND to compute the previous sort-key values needed to recompute the new ones.
10
+ * Passing the NEW values in `composites` makes the conditional check fail with
11
+ * ConditionalCheckFailedException, which ElectroDB wraps as NotFoundError, and
12
+ * the caller silently drops the update. Same root cause as PR #186 fixed for
13
+ * `flag-queue.ts`.
14
+ */
15
+ export type ThreadMessageRowState = Pick<
16
+ ThreadMessageItem,
17
+ | "sentDate"
18
+ | "mailboxId"
19
+ | "isRead"
20
+ | "isDeleted"
21
+ | "hasStars"
22
+ | "hasAttachment"
23
+ >;
24
+
25
+ const currentComposites = (threadMessage: ThreadMessageRowState) => ({
26
+ sentDate: threadMessage.sentDate,
27
+ mailboxId: threadMessage.mailboxId,
28
+ isRead: threadMessage.isRead,
29
+ isDeleted: threadMessage.isDeleted,
30
+ hasStars: threadMessage.hasStars,
31
+ hasAttachment: threadMessage.hasAttachment,
32
+ });
33
+
34
+ export const buildThreadMessageTrashUpdate = (
35
+ threadMessage: ThreadMessageRowState,
36
+ newUid: number,
37
+ destinationMailboxId: string,
38
+ ) => ({
39
+ set: {
40
+ uid: newUid,
41
+ mailboxId: destinationMailboxId,
42
+ isDeleted: true,
43
+ },
44
+ composites: currentComposites(threadMessage),
45
+ });
46
+
47
+ /**
48
+ * The inverse payload: put the thread row back where the message still is on
49
+ * the server. The delete was recorded optimistically, so a delete that gives up
50
+ * with the message still at its source has to hand the row back rather than
51
+ * leave it claiming Trash — an invisible `failed` on a row the user cannot see
52
+ * is the shape of the incident this whole change is about.
53
+ */
54
+ export const buildThreadMessageMoveRevert = (
55
+ threadMessage: ThreadMessageRowState,
56
+ sourceUid: number,
57
+ sourceMailboxId: string,
58
+ ) => ({
59
+ set: {
60
+ uid: sourceUid,
61
+ mailboxId: sourceMailboxId,
62
+ isDeleted: false,
63
+ },
64
+ composites: currentComposites(threadMessage),
65
+ });
66
+
67
+ /**
68
+ * Hand a row back after an abandoned expunge. The mail never left Trash, so
69
+ * only the deletion mark reverts — the uid and mailbox on the row are still
70
+ * where the server has it.
71
+ */
72
+ export const buildThreadMessageUndelete = (
73
+ threadMessage: ThreadMessageRowState,
74
+ ) => ({
75
+ set: { isDeleted: false },
76
+ composites: currentComposites(threadMessage),
77
+ });