@remit/imap-worker 0.0.86 → 0.0.87

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 (71) hide show
  1. package/README.md +3 -1
  2. package/package.json +10 -2
  3. package/src/account-check.ts +1 -1
  4. package/src/body-sync-gate.test.ts +1 -1
  5. package/src/body-sync-gate.ts +1 -1
  6. package/src/e2e-processor-shim.ts +6 -21
  7. package/src/emit.ts +1 -1
  8. package/src/failure-kind.ts +1 -1
  9. package/src/handlers/append-sent-message.sqlite.test.ts +176 -0
  10. package/src/handlers/append-sent-message.ts +3 -24
  11. package/src/handlers/delete-account-objects.test.ts +1 -1
  12. package/src/handlers/delete-account-objects.ts +1 -1
  13. package/src/handlers/empty-trash.sqlite.test.ts +209 -0
  14. package/src/handlers/empty-trash.ts +3 -24
  15. package/src/handlers/flag-push.ts +11 -18
  16. package/src/handlers/mailbox-management.sqlite.test.ts +265 -0
  17. package/src/handlers/mailbox-management.ts +12 -48
  18. package/src/handlers/message-copy.sqlite.test.ts +298 -0
  19. package/src/handlers/message-copy.ts +31 -49
  20. package/src/handlers/message-delete-terminal.ts +12 -10
  21. package/src/handlers/message-delete.sqlite.test.ts +291 -0
  22. package/src/handlers/message-delete.ts +26 -32
  23. package/src/handlers/message-move-terminal.ts +12 -10
  24. package/src/handlers/message-move.sqlite.test.ts +271 -0
  25. package/src/handlers/message-move.test.ts +76 -863
  26. package/src/handlers/message-move.ts +49 -39
  27. package/src/handlers/placement-move-push.test.ts +96 -329
  28. package/src/handlers/placement-move-push.ts +75 -29
  29. package/src/handlers/placement-push.sqlite.test.ts +298 -0
  30. package/src/handlers/probe-message-id.sqlite.test.ts +197 -0
  31. package/src/handlers/probe-message-id.ts +15 -0
  32. package/src/handlers/sync-mailboxes.test.ts +0 -1
  33. package/src/handlers/sync-mailboxes.ts +1 -1
  34. package/src/handlers/sync-message-body.sqlite.test.ts +314 -0
  35. package/src/handlers/sync-message-body.test.ts +3 -299
  36. package/src/handlers/sync-message-body.ts +21 -11
  37. package/src/handlers/sync-messages.sqlite.test.ts +183 -0
  38. package/src/handlers/sync-messages.ts +27 -33
  39. package/src/index.ts +5 -3
  40. package/src/mail-sync-selection.sqlite.test.ts +16 -18
  41. package/src/poller.ts +5 -4
  42. package/src/processor.test.ts +1 -1
  43. package/src/processor.ts +1 -1
  44. package/src/scheduler/attachment-sweep.sqlite.test.ts +74 -0
  45. package/src/scheduler/attachment-sweep.ts +1 -1
  46. package/src/scheduler/{calendar-subscriptions.test.ts → calendar-subscriptions.sqlite.test.ts} +43 -55
  47. package/src/scheduler/calendar-subscriptions.ts +6 -6
  48. package/src/scheduler/half-deleted-accounts.sqlite.test.ts +203 -0
  49. package/src/scheduler/handler.ts +12 -1
  50. package/src/scheduler/loop.test.ts +2 -0
  51. package/src/scheduler/run-tick.sqlite.test.ts +192 -0
  52. package/src/scheduler/run-tick.ts +39 -1
  53. package/src/scheduler/runner.ts +21 -1
  54. package/src/scheduler/stranded-placement-redrive.ts +265 -0
  55. package/src/stranded-placement.sqlite.test.ts +795 -0
  56. package/src/test-helpers/test-world.ts +398 -0
  57. package/src/thread-join.sqlite.test.ts +115 -0
  58. package/src/with-oauth-lifecycle-deps.sqlite.test.ts +138 -14
  59. package/src/with-oauth-lifecycle.ts +1 -1
  60. package/src/handlers/append-sent-message.test.ts +0 -529
  61. package/src/handlers/empty-trash.test.ts +0 -728
  62. package/src/handlers/flag-push-placement-race.test.ts +0 -385
  63. package/src/handlers/flag-push.test.ts +0 -325
  64. package/src/handlers/mailbox-management.test.ts +0 -850
  65. package/src/handlers/message-copy.test.ts +0 -654
  66. package/src/handlers/message-delete.test.ts +0 -1869
  67. package/src/handlers/message-move-terminal.test.ts +0 -412
  68. package/src/handlers/sync-messages.test.ts +0 -569
  69. package/src/scheduler/attachment-sweep.test.ts +0 -169
  70. package/src/scheduler/run-tick.test.ts +0 -478
  71. package/src/with-oauth-lifecycle.test.ts +0 -318
@@ -0,0 +1,271 @@
1
+ import assert from "node:assert/strict";
2
+ import { after, before, describe, it } from "node:test";
3
+ import {
4
+ MailboxCursorState,
5
+ MailboxSpecialUse,
6
+ MessageMutation,
7
+ MessageStatus,
8
+ MessageSyncStatus,
9
+ } from "@remit/domain-enums";
10
+ import type { MessageMoveEvent } from "../events.js";
11
+ import { mail, openStore, type World } from "../test-helpers/test-world.js";
12
+ import { handleMessageMove } from "./message-move.js";
13
+
14
+ let store: Awaited<ReturnType<typeof openStore>>;
15
+
16
+ before(async () => {
17
+ store = await openStore();
18
+ });
19
+
20
+ after(() => store.close());
21
+
22
+ interface Arranged {
23
+ w: World;
24
+ messageId: string;
25
+ uid: number;
26
+ messageIdHeader: string;
27
+ inboxId: string;
28
+ archiveId: string;
29
+ event: MessageMoveEvent;
30
+ }
31
+
32
+ /** A message in INBOX, moved to Archive through the service the API calls. */
33
+ const arrange = async (
34
+ options: { olderCopyInArchive?: boolean } = {},
35
+ ): Promise<Arranged> => {
36
+ const w = await store.world();
37
+ const inbox = await w.folder("INBOX");
38
+ const archive = await w.folder("Archive", {
39
+ specialUse: MailboxSpecialUse.Archive,
40
+ });
41
+ const seeded = await w.message(inbox);
42
+ if (options.olderCopyInArchive) {
43
+ w.imap.deliver("Archive", mail(seeded.messageIdHeader));
44
+ }
45
+
46
+ await w.client.messageMove.moveMessage(
47
+ w.account.accountConfigId,
48
+ seeded.messageId,
49
+ archive.mailboxId,
50
+ w.account.accountId,
51
+ );
52
+ const event = w
53
+ .queued()
54
+ .find(
55
+ (body) => body.type === "MESSAGE_MOVE",
56
+ ) as unknown as MessageMoveEvent;
57
+ assert.ok(event, "the move enqueued a MESSAGE_MOVE");
58
+
59
+ return {
60
+ w,
61
+ messageId: seeded.messageId,
62
+ uid: seeded.uid,
63
+ messageIdHeader: seeded.messageIdHeader,
64
+ inboxId: inbox.mailboxId,
65
+ archiveId: archive.mailboxId,
66
+ event,
67
+ };
68
+ };
69
+
70
+ const run = (a: Arranged, receiveCount = 1) =>
71
+ handleMessageMove(a.event, a.w.log, receiveCount, a.w.scope);
72
+
73
+ const resyncs = (w: World): unknown[] =>
74
+ w
75
+ .queued()
76
+ .filter((body) => body.type === "SYNC_MESSAGES")
77
+ .map((body) => body.mailboxId);
78
+
79
+ const logged = (w: World, key: string, value: string): number =>
80
+ w.logs.filter((line) => line.fields[key] === value).length;
81
+
82
+ describe("a message move", () => {
83
+ it("lands at the destination and settles the row on the uid the server gave it", async () => {
84
+ const a = await arrange();
85
+
86
+ await run(a);
87
+
88
+ assert.deepEqual(a.w.imap.uids("INBOX"), []);
89
+ const [archiveUid] = a.w.imap.uids("Archive", a.messageIdHeader);
90
+ const row = await a.w.row(a.messageId);
91
+ assert.equal(row?.mailboxId, a.archiveId);
92
+ assert.equal(row?.uid, archiveUid);
93
+ assert.equal(row?.status, MessageStatus.active);
94
+ assert.deepEqual(await a.w.threadRows(a.messageId), [
95
+ { mailboxId: a.archiveId, uid: archiveUid, isDeleted: false },
96
+ ]);
97
+ assert.deepEqual(resyncs(a.w), [a.inboxId, a.archiveId]);
98
+ });
99
+
100
+ // Issue #1122: without COPYUID the destination is asked by Message-ID, and
101
+ // an older copy already there must not take the row.
102
+ it("binds to the copy this move delivered when the server sends no COPYUID", async () => {
103
+ const a = await arrange({ olderCopyInArchive: true });
104
+ a.w.imap.uidplus = false;
105
+
106
+ await run(a);
107
+
108
+ const copies = a.w.imap.uids("Archive", a.messageIdHeader);
109
+ assert.equal(copies.length, 2);
110
+ assert.equal((await a.w.row(a.messageId))?.uid, Math.max(...copies));
111
+ });
112
+
113
+ it("issues nothing when a redelivery finds the move already settled", async () => {
114
+ const a = await arrange();
115
+ await run(a);
116
+ const commands = a.w.imap.log.length;
117
+
118
+ await run(a);
119
+
120
+ assert.equal(a.w.imap.log.length, commands);
121
+ });
122
+
123
+ it("marks the row failed and redelivers when the connection drops mid-move", async () => {
124
+ const a = await arrange();
125
+ a.w.imap.failNext.MOVE = new Error("Connection reset");
126
+
127
+ await assert.rejects(run(a), /Connection reset/);
128
+
129
+ const row = await a.w.row(a.messageId);
130
+ assert.equal(row?.status, MessageStatus.moving);
131
+ assert.equal(row?.syncStatus, MessageSyncStatus.failed);
132
+ });
133
+
134
+ // Issue #655, BROKEN: the message never left, so the row goes back on the
135
+ // pair the server confirmed and says a move gave up.
136
+ it("puts the row back on its source at the ceiling when the message never left", async () => {
137
+ const a = await arrange();
138
+ a.w.imap.failNext.MOVE = new Error("Connection reset");
139
+
140
+ await run(a, 3);
141
+
142
+ const row = await a.w.row(a.messageId);
143
+ assert.equal(row?.mailboxId, a.inboxId);
144
+ assert.equal(row?.uid, a.uid);
145
+ assert.notEqual(row?.status, MessageStatus.moving);
146
+ assert.equal(row?.abandonedMutation, MessageMutation.move);
147
+ assert.deepEqual(resyncs(a.w), [a.inboxId, a.archiveId]);
148
+ });
149
+
150
+ // Issue #655, RECONCILED: the message is gone from the source, so the stale
151
+ // row goes and the resync re-projects it wherever it actually is.
152
+ it("reconciles the row away at the ceiling when the source no longer holds it", async () => {
153
+ const a = await arrange();
154
+ a.w.imap.empty("INBOX");
155
+ a.w.imap.failNext.MOVE = new Error("Connection reset");
156
+
157
+ await run(a, 3);
158
+
159
+ assert.equal(await a.w.row(a.messageId), undefined);
160
+ assert.deepEqual(resyncs(a.w), [a.inboxId, a.archiveId]);
161
+ });
162
+
163
+ it("creates a destination the server does not have and redelivers", async () => {
164
+ const a = await arrange();
165
+ a.w.imap.removeFolder("Archive");
166
+
167
+ await assert.rejects(run(a), /TRYCREATE/);
168
+
169
+ assert.deepEqual(a.w.imap.commands("CREATE"), ["CREATE Archive"]);
170
+ assert.deepEqual(a.w.imap.uids("INBOX"), [a.uid]);
171
+ });
172
+
173
+ it("hands the row back without connecting when the destination mailbox was deleted", async () => {
174
+ const a = await arrange();
175
+ await a.w.client.mailbox.delete(a.w.account.accountId, a.archiveId);
176
+
177
+ await run(a);
178
+
179
+ assert.equal(a.w.imap.connections, 0);
180
+ const row = await a.w.row(a.messageId);
181
+ assert.equal(row?.mailboxId, a.inboxId);
182
+ assert.equal(row?.uid, a.uid);
183
+ assert.equal(row?.status, MessageStatus.active);
184
+ assert.equal(logged(a.w, "metric", "message_move_destination_gone"), 1);
185
+ assert.deepEqual(resyncs(a.w), [a.inboxId]);
186
+ });
187
+
188
+ it("acks without connecting when the source mailbox was deleted", async () => {
189
+ const a = await arrange();
190
+ await a.w.client.mailbox.delete(a.w.account.accountId, a.inboxId);
191
+
192
+ await run(a);
193
+
194
+ assert.equal(a.w.imap.connections, 0);
195
+ });
196
+
197
+ // Issue #1203: a paused cursor that acked left the row naming the
198
+ // destination with the source's uid, unmovable and undeletable for good.
199
+ it("hands the row back to its source without connecting while the cursor is rebuilding", async () => {
200
+ const a = await arrange();
201
+ await a.w.client.mailbox.update(a.w.account.accountId, a.inboxId, {
202
+ cursorState: MailboxCursorState.rebuilding,
203
+ });
204
+
205
+ await run(a);
206
+
207
+ assert.equal(a.w.imap.connections, 0);
208
+ const row = await a.w.row(a.messageId);
209
+ assert.equal(row?.mailboxId, a.inboxId);
210
+ assert.equal(row?.uid, a.uid);
211
+ assert.equal(row?.status, MessageStatus.active);
212
+ assert.equal(logged(a.w, "metric", "message_move_dropped_on_pause"), 1);
213
+ assert.deepEqual(resyncs(a.w), [a.inboxId, a.archiveId]);
214
+ });
215
+
216
+ // The redelivery half of #1203: the source answers "no" because it was
217
+ // recreated, and Archive holds an older copy. Settling on it would bind the
218
+ // row to mail this move never touched.
219
+ it("never settles a redelivered paused move off a sighting at the destination", async () => {
220
+ const a = await arrange({ olderCopyInArchive: true });
221
+ a.w.imap.empty("INBOX");
222
+ await a.w.client.mailbox.update(a.w.account.accountId, a.inboxId, {
223
+ cursorState: MailboxCursorState.rebuilding,
224
+ });
225
+
226
+ await run(a, 2);
227
+
228
+ assert.deepEqual(a.w.imap.commands("MOVE"), []);
229
+ const row = await a.w.row(a.messageId);
230
+ assert.equal(row?.mailboxId, a.inboxId);
231
+ assert.equal(row?.uid, a.uid);
232
+ assert.equal(row?.syncStatus, MessageSyncStatus.abandoned);
233
+ assert.equal(row?.abandonedMutation, MessageMutation.move);
234
+ assert.equal(
235
+ logged(a.w, "alert", "message_move_paused_placement_unproven"),
236
+ 1,
237
+ );
238
+ });
239
+
240
+ it("moves nothing when the source's UIDVALIDITY changed under it", async () => {
241
+ const a = await arrange();
242
+ a.w.imap.setUidValidity("INBOX", 2);
243
+
244
+ await run(a);
245
+
246
+ assert.deepEqual(a.w.imap.commands("MOVE"), []);
247
+ assert.equal((await a.w.row(a.messageId))?.mailboxId, a.inboxId);
248
+ });
249
+
250
+ it("moves under the destination's current path when it was renamed after enqueue", async () => {
251
+ const a = await arrange();
252
+ a.w.imap.renameFolder("Archive", "Records");
253
+ await a.w.client.mailbox.update(a.w.account.accountId, a.archiveId, {
254
+ fullPath: "Records",
255
+ });
256
+
257
+ await run(a);
258
+
259
+ assert.equal(a.w.imap.uids("Records", a.messageIdHeader).length, 1);
260
+ assert.equal((await a.w.row(a.messageId))?.mailboxId, a.archiveId);
261
+ });
262
+
263
+ it("acks without connecting when the message row is already gone", async () => {
264
+ const a = await arrange();
265
+ await a.w.client.message.delete(a.messageId);
266
+
267
+ await run(a);
268
+
269
+ assert.equal(a.w.imap.connections, 0);
270
+ });
271
+ });