@remit/mailbox-service 0.0.111 → 0.0.112

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.
Files changed (123) hide show
  1. package/package.json +21 -6
  2. package/src/body-part-mapper.ts +51 -1
  3. package/src/body-sync-queue.test.ts +39 -99
  4. package/src/body-sync-terminal.test.ts +115 -277
  5. package/src/body-sync-terminal.ts +10 -12
  6. package/src/body-sync.test.ts +925 -0
  7. package/src/body-sync.ts +156 -235
  8. package/src/classification-backfill.test.ts +197 -527
  9. package/src/classify-message.test.ts +11 -74
  10. package/src/external-move.test.ts +102 -209
  11. package/src/external-move.ts +13 -0
  12. package/src/filter-operations.test.ts +319 -0
  13. package/src/filter-operations.ts +377 -0
  14. package/src/filter-update.test.ts +313 -0
  15. package/src/filters/filter-config.test.ts +55 -197
  16. package/src/filters/filter-config.ts +4 -39
  17. package/src/filters/match.test.ts +1 -38
  18. package/src/filters/match.ts +3 -52
  19. package/src/filters/pipeline.test.ts +158 -289
  20. package/src/filters/pipeline.ts +56 -170
  21. package/src/flag-push-terminal.ts +9 -11
  22. package/src/flag-push.test.ts +76 -372
  23. package/src/flag-queue.test.ts +197 -0
  24. package/src/folder-operations.test.ts +273 -0
  25. package/src/folder-operations.ts +217 -0
  26. package/src/forwarded-message.test.ts +122 -0
  27. package/src/imapflow-connect-retry.test.ts +76 -113
  28. package/src/imapflow-connection.test.ts +34 -0
  29. package/src/imapflow-connection.ts +5 -4
  30. package/src/index.ts +59 -12
  31. package/src/list-id-backfill.test.ts +133 -499
  32. package/src/mailbox-cursor.test.ts +81 -189
  33. package/src/mailbox-delete-intent.test.ts +78 -205
  34. package/src/mailbox-intent.ts +5 -3
  35. package/src/mailbox-management.test.ts +107 -220
  36. package/src/mailbox-management.ts +1 -2
  37. package/src/mailbox-presence.test.ts +36 -34
  38. package/src/mailbox-queue.ts +2 -20
  39. package/src/mailbox-rename-intent.test.ts +156 -325
  40. package/src/mailbox-rename-subtree.test.ts +205 -582
  41. package/src/mailbox-sync.test.ts +182 -744
  42. package/src/message-move.test.ts +726 -0
  43. package/src/message-move.ts +227 -57
  44. package/src/message-read.ts +87 -0
  45. package/src/message-save-retry.test.ts +90 -0
  46. package/src/message-sync-flags.test.ts +10 -1
  47. package/src/message-sync.test.ts +762 -0
  48. package/src/message-sync.ts +409 -239
  49. package/src/mime-walker.ts +16 -15
  50. package/src/organize-job.ts +93 -0
  51. package/src/outbox-attachment.test.ts +97 -209
  52. package/src/outbox-failed-recovery.test.ts +198 -269
  53. package/src/outbox-queue.test.ts +24 -0
  54. package/src/outbox-queue.ts +25 -8
  55. package/src/ownership.ts +140 -0
  56. package/src/placement-move-terminal.ts +9 -12
  57. package/src/placement-move.ts +33 -6
  58. package/src/placement-settled.ts +28 -4
  59. package/src/placement.test.ts +404 -0
  60. package/src/quarantine.test.ts +90 -148
  61. package/src/quarantine.ts +3 -2
  62. package/src/refusal.ts +29 -0
  63. package/src/restore-source-placement.ts +0 -4
  64. package/src/spam-report.test.ts +238 -710
  65. package/src/sync-warning.test.ts +213 -0
  66. package/src/sync-warning.ts +56 -0
  67. package/src/test-helpers/fake-imap.ts +690 -0
  68. package/src/test-helpers/test-queue.ts +56 -0
  69. package/src/test-helpers/test-store.ts +299 -0
  70. package/src/thread-root.test.ts +29 -0
  71. package/src/thread-root.ts +51 -0
  72. package/src/types/mailparser-augment.d.ts +8 -0
  73. package/src/adversarial-mime.e2e.test.ts +0 -307
  74. package/src/body-sync-auto-read-writeonce.test.ts +0 -219
  75. package/src/body-sync-calendar-suggestion.test.ts +0 -433
  76. package/src/body-sync-category-denormalize.test.ts +0 -352
  77. package/src/body-sync-category-override.test.ts +0 -501
  78. package/src/body-sync-category-writeonce.test.ts +0 -234
  79. package/src/body-sync-filter.test.ts +0 -237
  80. package/src/body-sync-message-gone.test.ts +0 -139
  81. package/src/body-sync-placement-flags.test.ts +0 -347
  82. package/src/body-sync-placement-writeonce.test.ts +0 -337
  83. package/src/body-sync-quarantine.test.ts +0 -276
  84. package/src/body-sync-resync.test.ts +0 -605
  85. package/src/body-sync-unclassifiable-once.test.ts +0 -260
  86. package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
  87. package/src/filters/anchor-drift.ts +0 -55
  88. package/src/filters/message-embedder.test.ts +0 -39
  89. package/src/filters/message-embedder.ts +0 -33
  90. package/src/flag-push-terminal.test.ts +0 -209
  91. package/src/flag-queue-stars.test.ts +0 -171
  92. package/src/imapflow-connection.e2e.test.ts +0 -335
  93. package/src/imapflow-connection.integ.test.ts +0 -316
  94. package/src/message-delete-junk-reconcile.test.ts +0 -117
  95. package/src/message-delete-no-trash.test.ts +0 -245
  96. package/src/message-delete-unsettled-placement.test.ts +0 -396
  97. package/src/message-move-copy.test.ts +0 -562
  98. package/src/message-move-empty-trash.test.ts +0 -326
  99. package/src/message-move-same-mailbox.test.ts +0 -139
  100. package/src/message-move-unsettled-placement.test.ts +0 -378
  101. package/src/message-sync-address-spoof.test.ts +0 -293
  102. package/src/message-sync-category.test.ts +0 -142
  103. package/src/message-sync-changedsince.test.ts +0 -840
  104. package/src/message-sync-junk-harvest.test.ts +0 -244
  105. package/src/message-sync-quarantine.test.ts +0 -416
  106. package/src/message-sync-repoint.test.ts +0 -419
  107. package/src/outbox-queue-settled-row.test.ts +0 -140
  108. package/src/pass-through-unit-of-work.ts +0 -17
  109. package/src/placement-move-terminal.test.ts +0 -300
  110. package/src/placement-move-unsettled.test.ts +0 -275
  111. package/src/placement-transition-race.test.ts +0 -482
  112. package/src/stale-message-reconcile.test.ts +0 -82
  113. package/src/stale-message-reconcile.ts +0 -55
  114. package/src/test-helpers/isolated-mailbox.ts +0 -44
  115. package/src/test-helpers/mailfuzz-connection.ts +0 -27
  116. package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
  117. package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
  118. package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
  119. package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
  120. package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
  121. package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
  122. package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
  123. package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
@@ -1,300 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type {
4
- MessageItem,
5
- PlacementPredicate,
6
- PlacementTransitionInput,
7
- } from "@remit/data-ports";
8
- import {
9
- MessageMutation,
10
- MessageStatus,
11
- MessageSyncStatus,
12
- } from "@remit/domain-enums";
13
- import type { PlacementMoveLogger } from "./placement-move.js";
14
- import {
15
- type ResolveExhaustedPlacementMoveDeps,
16
- resolveExhaustedPlacementMoveFailure,
17
- } from "./placement-move-terminal.js";
18
- import type { IImapConnection } from "./types.js";
19
-
20
- interface LogEntry {
21
- obj: Record<string, unknown>;
22
- msg: string;
23
- }
24
-
25
- interface Transition {
26
- messageId: string;
27
- expected: PlacementPredicate;
28
- next: PlacementTransitionInput;
29
- }
30
-
31
- interface Recorder {
32
- deps: ResolveExhaustedPlacementMoveDeps;
33
- infos: LogEntry[];
34
- errors: LogEntry[];
35
- markerDeletes: string[];
36
- deletedMessages: string[];
37
- deletedThreadMessages: Array<{
38
- accountConfigId: string;
39
- threadMessageId: string;
40
- }>;
41
- transitions: Transition[];
42
- threadUpdates: string[];
43
- }
44
-
45
- const buildRecorder = (
46
- threadMessages: Array<{ accountConfigId: string; threadMessageId: string }>,
47
- transitionWins = true,
48
- ): Recorder => {
49
- const infos: LogEntry[] = [];
50
- const errors: LogEntry[] = [];
51
- const markerDeletes: string[] = [];
52
- const deletedMessages: string[] = [];
53
- const deletedThreadMessages: Array<{
54
- accountConfigId: string;
55
- threadMessageId: string;
56
- }> = [];
57
- const transitions: Transition[] = [];
58
- const threadUpdates: string[] = [];
59
-
60
- const log: PlacementMoveLogger = {
61
- info: (obj, msg) => infos.push({ obj, msg }),
62
- error: (obj, msg) => errors.push({ obj, msg }),
63
- };
64
-
65
- const deps: ResolveExhaustedPlacementMoveDeps = {
66
- markerService: {
67
- delete: async (id: string) => {
68
- markerDeletes.push(id);
69
- },
70
- },
71
- messageService: {
72
- delete: async (id: string) => {
73
- deletedMessages.push(id);
74
- },
75
- transitionPlacement: async (
76
- messageId: string,
77
- expected: PlacementPredicate,
78
- next: PlacementTransitionInput,
79
- ) => {
80
- transitions.push({ messageId, expected, next });
81
- return transitionWins ? ({ messageId } as MessageItem) : undefined;
82
- },
83
- },
84
- threadMessageService: {
85
- findAllByMessageId: async () => threadMessages as never,
86
- deleteMany: async (
87
- keys: Array<{ accountConfigId: string; threadMessageId: string }>,
88
- ) => {
89
- deletedThreadMessages.push(...keys);
90
- },
91
- update: async (_accountConfigId: string, threadMessageId: string) => {
92
- threadUpdates.push(threadMessageId);
93
- return undefined as never;
94
- },
95
- },
96
- log,
97
- };
98
-
99
- return {
100
- deps,
101
- infos,
102
- errors,
103
- markerDeletes,
104
- deletedMessages,
105
- deletedThreadMessages,
106
- transitions,
107
- threadUpdates,
108
- };
109
- };
110
-
111
- const buildConnection = (
112
- present: Set<number>,
113
- fetchDrops: Set<number> = new Set(),
114
- ): IImapConnection =>
115
- ({
116
- openBox: async () => ({}) as never,
117
- fetchMessages: async (uids: number[]) =>
118
- uids
119
- .filter((uid) => present.has(uid) && !fetchDrops.has(uid))
120
- .map((uid) => ({ uid }) as unknown as never),
121
- search: async (criteria: unknown[]) => {
122
- const [, value] = (criteria as Array<[string, string]>)[0];
123
- const uid = Number(value);
124
- return present.has(uid) ? [uid] : [];
125
- },
126
- }) as unknown as IImapConnection;
127
-
128
- describe("resolveExhaustedPlacementMoveFailure — the two terminal outcomes (mirrors #1270 for placement moves)", () => {
129
- it("RECONCILED (expected): the message is gone from its pending-move source — marker dropped, stale row reconciled, no alarm", async () => {
130
- const rec = buildRecorder([
131
- { accountConfigId: "cfg-1", threadMessageId: "tm-msg-gone" },
132
- ]);
133
-
134
- const result = await resolveExhaustedPlacementMoveFailure(rec.deps, {
135
- accountId: "acc-1",
136
- accountConfigId: "cfg-1",
137
- messageId: "msg-gone",
138
- uid: 101,
139
- sourceMailboxId: "mbx-junk",
140
- sourceMailboxPath: "Junk",
141
- getConnection: async () => buildConnection(new Set()),
142
- });
143
-
144
- assert.equal(result.outcome, "reconciled");
145
- assert.deepEqual(rec.markerDeletes, ["msg-gone"]);
146
- assert.deepEqual(rec.deletedMessages, ["msg-gone"]);
147
- assert.deepEqual(rec.deletedThreadMessages, [
148
- { accountConfigId: "cfg-1", threadMessageId: "tm-msg-gone" },
149
- ]);
150
- assert.equal(
151
- rec.errors.length,
152
- 0,
153
- "no alarm for the expected/routine outcome",
154
- );
155
- assert.ok(
156
- rec.infos.some(
157
- (e) => e.obj.metric === "placement_move_stale_row_reconciled",
158
- ),
159
- );
160
- });
161
-
162
- it("BROKEN: the message is still at the source — the row goes back to the source pair rather than being left `moving` forever", async () => {
163
- const rec = buildRecorder([
164
- { accountConfigId: "cfg-1", threadMessageId: "tm-msg-broken" },
165
- ]);
166
-
167
- const result = await resolveExhaustedPlacementMoveFailure(rec.deps, {
168
- accountId: "acc-1",
169
- accountConfigId: "cfg-1",
170
- messageId: "msg-broken",
171
- uid: 202,
172
- sourceMailboxId: "mbx-inbox",
173
- sourceMailboxPath: "INBOX",
174
- getConnection: async () => buildConnection(new Set([202])),
175
- });
176
-
177
- assert.equal(result.outcome, "broken");
178
- assert.deepEqual(rec.transitions, [
179
- {
180
- messageId: "msg-broken",
181
- expected: {
182
- status: [MessageStatus.moving, MessageStatus.deleting],
183
- },
184
- next: {
185
- mailboxId: "mbx-inbox",
186
- uid: 202,
187
- status: MessageStatus.active,
188
- syncStatus: MessageSyncStatus.synced,
189
- // Remit's own classification filing, not something the user asked
190
- // for, so no per-message give-up marker — the alert is the surface.
191
- abandonedMutation: MessageMutation.none,
192
- },
193
- },
194
- ]);
195
- assert.deepEqual(rec.threadUpdates, ["tm-msg-broken"]);
196
- // The marker predicted a move that has now been given up on; leaving it
197
- // would keep a folder-count prediction nothing will ever settle.
198
- assert.deepEqual(rec.markerDeletes, ["msg-broken"]);
199
- assert.deepEqual(rec.deletedMessages, []);
200
- assert.ok(
201
- rec.errors.some((e) => e.obj.alert === "placement_move_failed"),
202
- "the operator signal is the alert, not a stuck row",
203
- );
204
- });
205
-
206
- // The marker is the operator's evidence that this move still owed IMAP a
207
- // push. Dropping it first meant a restore that threw left the row stuck
208
- // `moving` with nothing left to say why.
209
- it("BROKEN: a restore that throws leaves the marker standing", async () => {
210
- const rec = buildRecorder([
211
- { accountConfigId: "cfg-1", threadMessageId: "tm-msg-broken" },
212
- ]);
213
- rec.deps.messageService.transitionPlacement = async () => {
214
- throw new Error("ProvisionedThroughputExceeded");
215
- };
216
-
217
- await assert.rejects(
218
- () =>
219
- resolveExhaustedPlacementMoveFailure(rec.deps, {
220
- accountId: "acc-1",
221
- accountConfigId: "cfg-1",
222
- messageId: "msg-broken",
223
- uid: 202,
224
- sourceMailboxId: "mbx-inbox",
225
- sourceMailboxPath: "INBOX",
226
- getConnection: async () => buildConnection(new Set([202])),
227
- }),
228
- /ProvisionedThroughputExceeded/,
229
- );
230
-
231
- assert.deepEqual(
232
- rec.markerDeletes,
233
- [],
234
- "the record of the owed push outlives a failed restore",
235
- );
236
- });
237
-
238
- it("BROKEN with another lane already settled: the restore loses its predicate and writes nothing", async () => {
239
- const rec = buildRecorder(
240
- [{ accountConfigId: "cfg-1", threadMessageId: "tm-msg-raced" }],
241
- false,
242
- );
243
-
244
- const result = await resolveExhaustedPlacementMoveFailure(rec.deps, {
245
- accountId: "acc-1",
246
- accountConfigId: "cfg-1",
247
- messageId: "msg-raced",
248
- uid: 404,
249
- sourceMailboxId: "mbx-inbox",
250
- sourceMailboxPath: "INBOX",
251
- getConnection: async () => buildConnection(new Set([404])),
252
- });
253
-
254
- assert.equal(result.outcome, "broken");
255
- assert.equal(rec.transitions.length, 1);
256
- assert.deepEqual(
257
- rec.threadUpdates,
258
- [],
259
- "a lost predicate leaves the listing rows to whoever won",
260
- );
261
- });
262
-
263
- it("a dropped FETCH row is not absence: the message is still at the source — local rows survive", async () => {
264
- const rec = buildRecorder([
265
- { accountConfigId: "cfg-1", threadMessageId: "tm-msg-live" },
266
- ]);
267
-
268
- const result = await resolveExhaustedPlacementMoveFailure(rec.deps, {
269
- accountId: "acc-1",
270
- accountConfigId: "cfg-1",
271
- messageId: "msg-live",
272
- uid: 303,
273
- sourceMailboxId: "mbx-inbox",
274
- sourceMailboxPath: "INBOX",
275
- getConnection: async () =>
276
- buildConnection(new Set([303]), new Set([303])),
277
- });
278
-
279
- assert.equal(result.outcome, "broken");
280
- assert.deepEqual(rec.deletedMessages, []);
281
- assert.deepEqual(rec.deletedThreadMessages, []);
282
- assert.ok(rec.errors.some((e) => e.obj.alert === "placement_move_failed"));
283
- });
284
-
285
- it("never throws — both outcomes are terminal, the caller always acks", async () => {
286
- const rec = buildRecorder([]);
287
-
288
- await assert.doesNotReject(() =>
289
- resolveExhaustedPlacementMoveFailure(rec.deps, {
290
- accountId: "acc-1",
291
- accountConfigId: "cfg-1",
292
- messageId: "msg-x",
293
- uid: 1,
294
- sourceMailboxId: "mbx-inbox",
295
- sourceMailboxPath: "INBOX",
296
- getConnection: async () => buildConnection(new Set([1])),
297
- }),
298
- );
299
- });
300
- });
@@ -1,275 +0,0 @@
1
- /**
2
- * Issue #496's rule applied to the placement move's own re-entry: a second
3
- * verdict, for a different destination, arriving while the first move is still
4
- * unconfirmed. The row then names the first destination but still carries the
5
- * uid of the folder before it, so binding a fresh marker to that pair sends the
6
- * reconciler to move the destination folder's OWN message at that uid.
7
- */
8
-
9
- import assert from "node:assert/strict";
10
- import { afterEach, beforeEach, describe, it, mock } from "node:test";
11
- import { SQSClient } from "@aws-sdk/client-sqs";
12
- import type {
13
- IAddressRepository,
14
- IMessagePlacementMoveRepository,
15
- IMessageRepository,
16
- IThreadMessageRepository,
17
- MessageItem,
18
- MessagePlacementMoveItem,
19
- PlacementPredicate,
20
- PlacementTransitionInput,
21
- PutMessagePlacementMoveInput,
22
- } from "@remit/data-ports";
23
- import { PlacementMoveService } from "./placement-move.js";
24
- import { noFolderRoles } from "./test-helpers/folder-roles.js";
25
-
26
- const ACCOUNT_ID = "acc-pm";
27
- const ACCOUNT_CONFIG_ID = "cfg-pm";
28
- const MESSAGE_ID = "msg-pm";
29
- const INBOX_ID = "mbx-inbox";
30
- const ARCHIVE_ID = "mbx-archive";
31
- const JUNK_ID = "mbx-junk";
32
- const INBOX_UID = 42;
33
-
34
- interface Harness {
35
- service: PlacementMoveService;
36
- row: MessageItem;
37
- puts: PutMessagePlacementMoveInput[];
38
- marker: MessagePlacementMoveItem | null;
39
- reconciled: string[];
40
- }
41
-
42
- const buildHarness = (): Harness => {
43
- const row = {
44
- messageId: MESSAGE_ID,
45
- accountConfigId: ACCOUNT_CONFIG_ID,
46
- mailboxId: INBOX_ID,
47
- uid: INBOX_UID,
48
- status: "active",
49
- syncStatus: "synced",
50
- } as unknown as MessageItem;
51
-
52
- const puts: PutMessagePlacementMoveInput[] = [];
53
- const reconciled: string[] = [];
54
- const harness = { row, puts, marker: null, reconciled } as Harness;
55
-
56
- const addressService = {
57
- reconcileJunkOnlyForMessage: async (messageId: string) => {
58
- reconciled.push(`${messageId}@${row.mailboxId}`);
59
- },
60
- } as unknown as IAddressRepository;
61
-
62
- const messageService = {
63
- get: async () => row,
64
- transitionPlacement: async (
65
- _messageId: string,
66
- expected: PlacementPredicate,
67
- next: PlacementTransitionInput,
68
- ) => {
69
- if (expected.status !== undefined && expected.status !== row.status) {
70
- return undefined;
71
- }
72
- if (expected.uid !== undefined && expected.uid !== row.uid) {
73
- return undefined;
74
- }
75
- if (
76
- expected.mailboxId !== undefined &&
77
- expected.mailboxId !== row.mailboxId
78
- ) {
79
- return undefined;
80
- }
81
- Object.assign(row, next);
82
- return row;
83
- },
84
- } as unknown as IMessageRepository;
85
-
86
- const threadMessageService = {
87
- getByMessageId: async () => ({
88
- accountConfigId: ACCOUNT_CONFIG_ID,
89
- threadMessageId: "tm-pm",
90
- sentDate: 1,
91
- mailboxId: row.mailboxId,
92
- isRead: false,
93
- isDeleted: false,
94
- hasStars: false,
95
- hasAttachment: false,
96
- }),
97
- update: async () => {},
98
- } as unknown as IThreadMessageRepository;
99
-
100
- const markerService = {
101
- put: async (input: PutMessagePlacementMoveInput) => {
102
- puts.push(input);
103
- harness.marker = {
104
- ...input,
105
- state: "pending",
106
- createdAt: Date.now(),
107
- } as unknown as MessagePlacementMoveItem;
108
- return harness.marker;
109
- },
110
- find: async () => harness.marker,
111
- updateState: async (
112
- _messageId: string,
113
- state: MessagePlacementMoveItem["state"],
114
- ) => {
115
- if (!harness.marker) throw new Error("no marker");
116
- harness.marker.state = state;
117
- return harness.marker;
118
- },
119
- delete: async () => {
120
- harness.marker = null;
121
- },
122
- } as unknown as IMessagePlacementMoveRepository;
123
-
124
- harness.service = new PlacementMoveService({
125
- messageService,
126
- threadMessageService,
127
- markerService,
128
- addressService,
129
- mailboxSpecialUseService: noFolderRoles,
130
- sqsQueueUrl: "https://sqs.eu-west-1.amazonaws.com/000/message-mgmt",
131
- moveSettleTimeoutMs: 200,
132
- moveSettlePollMs: 10,
133
- });
134
-
135
- return harness;
136
- };
137
-
138
- describe("PlacementMoveService — a second destination while the first move is unsettled (#496)", () => {
139
- let harness: Harness;
140
-
141
- beforeEach(() => {
142
- mock.method(SQSClient.prototype, "send", async () => ({}));
143
- harness = buildHarness();
144
- });
145
-
146
- afterEach(() => mock.restoreAll());
147
-
148
- it("binds to the settled row once the earlier move confirms", async () => {
149
- await harness.service.moveMessage(
150
- ACCOUNT_CONFIG_ID,
151
- MESSAGE_ID,
152
- ARCHIVE_ID,
153
- ACCOUNT_ID,
154
- );
155
- assert.equal(harness.row.status, "moving");
156
- assert.equal(harness.row.uid, INBOX_UID);
157
-
158
- // What the reconciler writes when the IMAP move lands: Archive's own
159
- // COPYUID, and the status back to active.
160
- setTimeout(() => {
161
- Object.assign(harness.row, { uid: 907, status: "active" });
162
- harness.marker = null;
163
- }, 30);
164
-
165
- await harness.service.moveMessage(
166
- ACCOUNT_CONFIG_ID,
167
- MESSAGE_ID,
168
- JUNK_ID,
169
- ACCOUNT_ID,
170
- );
171
-
172
- assert.equal(harness.puts.length, 2);
173
- assert.equal(
174
- harness.puts[1]?.sourceMailboxId,
175
- ARCHIVE_ID,
176
- "the source is where the confirmed move left the message",
177
- );
178
- assert.equal(harness.row.originalUid, 907, "and its confirmed uid");
179
- });
180
-
181
- it("does not apply the move when the earlier one never settles", async () => {
182
- await harness.service.moveMessage(
183
- ACCOUNT_CONFIG_ID,
184
- MESSAGE_ID,
185
- ARCHIVE_ID,
186
- ACCOUNT_ID,
187
- );
188
-
189
- await assert.rejects(
190
- () =>
191
- harness.service.moveMessage(
192
- ACCOUNT_CONFIG_ID,
193
- MESSAGE_ID,
194
- JUNK_ID,
195
- ACCOUNT_ID,
196
- ),
197
- /has not settled/,
198
- );
199
-
200
- assert.equal(harness.puts.length, 1, "no second marker was written");
201
- assert.equal(
202
- harness.puts[0]?.sourceMailboxId,
203
- INBOX_ID,
204
- "the surviving marker still names the folder the message is actually in",
205
- );
206
- assert.equal(harness.marker?.destinationMailboxId, ARCHIVE_ID);
207
- });
208
-
209
- it("drives a surviving pending marker for the same destination forward", async () => {
210
- await harness.service.moveMessage(
211
- ACCOUNT_CONFIG_ID,
212
- MESSAGE_ID,
213
- ARCHIVE_ID,
214
- ACCOUNT_ID,
215
- );
216
- // The enqueue failed on the earlier call, so the marker never advanced.
217
- if (harness.marker) harness.marker.state = "pending";
218
-
219
- await harness.service.moveMessage(
220
- ACCOUNT_CONFIG_ID,
221
- MESSAGE_ID,
222
- ARCHIVE_ID,
223
- ACCOUNT_ID,
224
- );
225
-
226
- assert.equal(harness.puts.length, 1, "the local move is never re-derived");
227
- assert.equal(harness.marker?.state, "queued");
228
- });
229
-
230
- it("moves a settled row normally", async () => {
231
- await harness.service.moveMessage(
232
- ACCOUNT_CONFIG_ID,
233
- MESSAGE_ID,
234
- ARCHIVE_ID,
235
- ACCOUNT_ID,
236
- );
237
- Object.assign(harness.row, { uid: 907, status: "active" });
238
- harness.marker = null;
239
-
240
- await harness.service.moveMessage(
241
- ACCOUNT_CONFIG_ID,
242
- MESSAGE_ID,
243
- JUNK_ID,
244
- ACCOUNT_ID,
245
- );
246
-
247
- assert.equal(harness.puts.length, 2);
248
- assert.equal(harness.puts[1]?.sourceMailboxId, ARCHIVE_ID);
249
- assert.equal(harness.puts[1]?.destinationMailboxId, JUNK_ID);
250
- });
251
-
252
- it("marks the sender when the classifier demotes the message", async () => {
253
- await harness.service.moveMessage(
254
- ACCOUNT_CONFIG_ID,
255
- MESSAGE_ID,
256
- JUNK_ID,
257
- ACCOUNT_ID,
258
- );
259
-
260
- assert.deepEqual(harness.reconciled, [`${MESSAGE_ID}@${JUNK_ID}`]);
261
- });
262
-
263
- it("asks nothing of a destination the message is already in", async () => {
264
- Object.assign(harness.row, { mailboxId: JUNK_ID });
265
-
266
- await harness.service.moveMessage(
267
- ACCOUNT_CONFIG_ID,
268
- MESSAGE_ID,
269
- JUNK_ID,
270
- ACCOUNT_ID,
271
- );
272
-
273
- assert.deepEqual(harness.reconciled, []);
274
- });
275
- });