@remit/mailbox-service 0.0.110 → 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.
- package/package.json +21 -6
- package/src/body-part-mapper.ts +51 -1
- package/src/body-sync-queue.test.ts +39 -99
- package/src/body-sync-terminal.test.ts +115 -277
- package/src/body-sync-terminal.ts +10 -12
- package/src/body-sync.test.ts +925 -0
- package/src/body-sync.ts +226 -200
- package/src/classification-backfill.test.ts +197 -527
- package/src/classify-message.test.ts +11 -74
- package/src/external-move.test.ts +102 -209
- package/src/external-move.ts +13 -0
- package/src/filter-operations.test.ts +319 -0
- package/src/filter-operations.ts +377 -0
- package/src/filter-update.test.ts +313 -0
- package/src/filters/filter-config.test.ts +55 -197
- package/src/filters/filter-config.ts +4 -39
- package/src/filters/match.test.ts +1 -38
- package/src/filters/match.ts +3 -52
- package/src/filters/pipeline.test.ts +158 -289
- package/src/filters/pipeline.ts +56 -170
- package/src/flag-push-terminal.ts +9 -11
- package/src/flag-push.test.ts +76 -372
- package/src/flag-queue.test.ts +197 -0
- package/src/folder-operations.test.ts +273 -0
- package/src/folder-operations.ts +217 -0
- package/src/forwarded-message.test.ts +122 -0
- package/src/imapflow-connect-retry.test.ts +76 -113
- package/src/imapflow-connection.test.ts +34 -0
- package/src/imapflow-connection.ts +19 -15
- package/src/index.ts +59 -12
- package/src/list-id-backfill.test.ts +133 -499
- package/src/mailbox-cursor.test.ts +81 -189
- package/src/mailbox-delete-intent.test.ts +78 -205
- package/src/mailbox-intent.ts +5 -3
- package/src/mailbox-management.test.ts +107 -220
- package/src/mailbox-management.ts +1 -2
- package/src/mailbox-presence.test.ts +36 -34
- package/src/mailbox-queue.ts +2 -20
- package/src/mailbox-rename-intent.test.ts +156 -325
- package/src/mailbox-rename-subtree.test.ts +205 -582
- package/src/mailbox-sync.test.ts +182 -744
- package/src/message-move.test.ts +726 -0
- package/src/message-move.ts +227 -57
- package/src/message-read.ts +87 -0
- package/src/message-save-retry.test.ts +90 -0
- package/src/message-sync-flags.test.ts +10 -1
- package/src/message-sync.test.ts +762 -0
- package/src/message-sync.ts +409 -239
- package/src/mime-walker.ts +16 -15
- package/src/organize-job.ts +93 -0
- package/src/outbox-attachment.test.ts +97 -209
- package/src/outbox-failed-recovery.test.ts +198 -269
- package/src/outbox-queue.test.ts +24 -0
- package/src/outbox-queue.ts +25 -8
- package/src/ownership.ts +140 -0
- package/src/placement-move-terminal.ts +9 -12
- package/src/placement-move.ts +33 -6
- package/src/placement-settled.ts +28 -4
- package/src/placement.test.ts +404 -0
- package/src/quarantine.test.ts +90 -148
- package/src/quarantine.ts +3 -2
- package/src/refusal.ts +29 -0
- package/src/restore-source-placement.ts +0 -4
- package/src/spam-report.test.ts +238 -710
- package/src/sync-warning.test.ts +213 -0
- package/src/sync-warning.ts +56 -0
- package/src/test-helpers/fake-imap.ts +690 -0
- package/src/test-helpers/test-queue.ts +56 -0
- package/src/test-helpers/test-store.ts +299 -0
- package/src/thread-root.test.ts +29 -0
- package/src/thread-root.ts +51 -0
- package/src/types/mailparser-augment.d.ts +8 -0
- package/src/types.ts +1 -1
- package/src/adversarial-mime.e2e.test.ts +0 -307
- package/src/body-sync-auto-read-writeonce.test.ts +0 -219
- package/src/body-sync-calendar-suggestion.test.ts +0 -433
- package/src/body-sync-category-denormalize.test.ts +0 -352
- package/src/body-sync-category-override.test.ts +0 -501
- package/src/body-sync-category-writeonce.test.ts +0 -234
- package/src/body-sync-filter.test.ts +0 -237
- package/src/body-sync-placement-flags.test.ts +0 -347
- package/src/body-sync-placement-writeonce.test.ts +0 -337
- package/src/body-sync-quarantine.test.ts +0 -276
- package/src/body-sync-resync.test.ts +0 -605
- package/src/body-sync-unclassifiable-once.test.ts +0 -260
- package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
- package/src/filters/anchor-drift.ts +0 -55
- package/src/filters/message-embedder.test.ts +0 -39
- package/src/filters/message-embedder.ts +0 -33
- package/src/flag-push-terminal.test.ts +0 -209
- package/src/flag-queue-stars.test.ts +0 -171
- package/src/imapflow-connection.e2e.test.ts +0 -335
- package/src/imapflow-connection.integ.test.ts +0 -316
- package/src/message-delete-junk-reconcile.test.ts +0 -117
- package/src/message-delete-no-trash.test.ts +0 -245
- package/src/message-delete-unsettled-placement.test.ts +0 -396
- package/src/message-move-copy.test.ts +0 -562
- package/src/message-move-empty-trash.test.ts +0 -326
- package/src/message-move-same-mailbox.test.ts +0 -139
- package/src/message-move-unsettled-placement.test.ts +0 -378
- package/src/message-sync-address-spoof.test.ts +0 -293
- package/src/message-sync-category.test.ts +0 -142
- package/src/message-sync-changedsince.test.ts +0 -840
- package/src/message-sync-junk-harvest.test.ts +0 -244
- package/src/message-sync-quarantine.test.ts +0 -416
- package/src/message-sync-repoint.test.ts +0 -419
- package/src/outbox-queue-settled-row.test.ts +0 -140
- package/src/pass-through-unit-of-work.ts +0 -17
- package/src/placement-move-terminal.test.ts +0 -300
- package/src/placement-move-unsettled.test.ts +0 -275
- package/src/placement-transition-race.test.ts +0 -482
- package/src/stale-message-reconcile.test.ts +0 -82
- package/src/stale-message-reconcile.ts +0 -55
- package/src/test-helpers/isolated-mailbox.ts +0 -44
- package/src/test-helpers/mailfuzz-connection.ts +0 -27
- package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
- package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
- package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
- package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
- package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
- package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
- package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
- 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
|
-
});
|