@remit/imap-worker 0.0.89 → 0.0.91

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 (171) hide show
  1. package/dist/account-check.d.ts +27 -0
  2. package/dist/account-check.js +70 -0
  3. package/dist/body-sync-gate.d.ts +12 -0
  4. package/dist/body-sync-gate.js +53 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +193 -0
  7. package/dist/config-env.d.ts +5 -0
  8. package/dist/config-env.js +37 -0
  9. package/dist/connection-pool.d.ts +22 -0
  10. package/dist/connection-pool.js +97 -0
  11. package/dist/connection-scope.d.ts +8 -0
  12. package/dist/connection-scope.js +10 -0
  13. package/dist/context.d.ts +46 -0
  14. package/dist/context.js +15 -0
  15. package/dist/e2e-processor-shim.d.ts +2 -0
  16. package/dist/e2e-processor-shim.js +181 -0
  17. package/dist/emit.d.ts +15 -0
  18. package/dist/emit.js +76 -0
  19. package/dist/events.d.ts +203 -0
  20. package/dist/events.js +1 -0
  21. package/dist/failure-kind.d.ts +10 -0
  22. package/{src/failure-kind.ts → dist/failure-kind.js} +6 -6
  23. package/dist/handlers/append-sent-message.d.ts +3 -0
  24. package/dist/handlers/append-sent-message.js +152 -0
  25. package/dist/handlers/delete-account-objects.d.ts +3 -0
  26. package/dist/handlers/delete-account-objects.js +35 -0
  27. package/dist/handlers/empty-trash.d.ts +14 -0
  28. package/dist/handlers/empty-trash.js +217 -0
  29. package/dist/handlers/flag-push.d.ts +38 -0
  30. package/dist/handlers/flag-push.js +195 -0
  31. package/dist/handlers/imported-folder.d.ts +4 -0
  32. package/dist/handlers/imported-folder.js +23 -0
  33. package/dist/handlers/mailbox-management.d.ts +6 -0
  34. package/dist/handlers/mailbox-management.js +294 -0
  35. package/dist/handlers/mailbox-sync-order.d.ts +21 -0
  36. package/dist/handlers/mailbox-sync-order.js +50 -0
  37. package/dist/handlers/message-copy.d.ts +40 -0
  38. package/dist/handlers/message-copy.js +371 -0
  39. package/dist/handlers/message-delete-terminal.d.ts +60 -0
  40. package/dist/handlers/message-delete-terminal.js +77 -0
  41. package/dist/handlers/message-delete.d.ts +57 -0
  42. package/dist/handlers/message-delete.js +520 -0
  43. package/{src/handlers/message-move-terminal.ts → dist/handlers/message-move-terminal.d.ts} +18 -105
  44. package/dist/handlers/message-move-terminal.js +94 -0
  45. package/dist/handlers/message-move.d.ts +146 -0
  46. package/dist/handlers/message-move.js +490 -0
  47. package/dist/handlers/placement-move-push.d.ts +62 -0
  48. package/dist/handlers/placement-move-push.js +307 -0
  49. package/dist/handlers/probe-message-id.d.ts +2 -0
  50. package/dist/handlers/probe-message-id.js +6 -0
  51. package/dist/handlers/sync-mailboxes.d.ts +45 -0
  52. package/dist/handlers/sync-mailboxes.js +233 -0
  53. package/dist/handlers/sync-message-body.d.ts +23 -0
  54. package/dist/handlers/sync-message-body.js +182 -0
  55. package/dist/handlers/sync-messages.d.ts +29 -0
  56. package/dist/handlers/sync-messages.js +285 -0
  57. package/dist/index.d.ts +9 -0
  58. package/dist/index.js +67 -0
  59. package/dist/lambda-context.d.ts +8 -0
  60. package/dist/lambda-context.js +33 -0
  61. package/dist/mailbox-row.d.ts +9 -0
  62. package/dist/mailbox-row.js +6 -0
  63. package/dist/poller.d.ts +1 -0
  64. package/dist/poller.js +51 -0
  65. package/dist/process.d.ts +6 -0
  66. package/dist/process.js +3 -0
  67. package/dist/processor.d.ts +3 -0
  68. package/dist/processor.js +44 -0
  69. package/dist/queues.d.ts +10 -0
  70. package/dist/queues.js +1 -0
  71. package/dist/s3-account-objects.d.ts +3 -0
  72. package/dist/s3-account-objects.js +26 -0
  73. package/dist/scheduler/attachment-sweep.d.ts +11 -0
  74. package/dist/scheduler/attachment-sweep.js +23 -0
  75. package/dist/scheduler/calendar-subscriptions.d.ts +26 -0
  76. package/dist/scheduler/calendar-subscriptions.js +35 -0
  77. package/dist/scheduler/config.d.ts +27 -0
  78. package/{src/scheduler/config.ts → dist/scheduler/config.js} +7 -26
  79. package/{src/scheduler/decide-due.ts → dist/scheduler/decide-due.d.ts} +3 -12
  80. package/dist/scheduler/decide-due.js +17 -0
  81. package/dist/scheduler/handler.d.ts +21 -0
  82. package/{src/scheduler/handler.ts → dist/scheduler/handler.js} +24 -36
  83. package/{src/scheduler/loop.ts → dist/scheduler/loop.d.ts} +7 -23
  84. package/dist/scheduler/loop.js +8 -0
  85. package/dist/scheduler/run-tick.d.ts +62 -0
  86. package/dist/scheduler/run-tick.js +164 -0
  87. package/dist/scheduler/runner.d.ts +2 -0
  88. package/{src/scheduler/runner.ts → dist/scheduler/runner.js} +58 -84
  89. package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
  90. package/dist/scheduler/stranded-placement-redrive.js +172 -0
  91. package/dist/sqs-queues.d.ts +4 -0
  92. package/dist/sqs-queues.js +22 -0
  93. package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
  94. package/dist/with-oauth-lifecycle-deps.js +17 -0
  95. package/dist/with-oauth-lifecycle.d.ts +56 -0
  96. package/dist/with-oauth-lifecycle.js +92 -0
  97. package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
  98. package/dist/worker-version.js +12 -0
  99. package/package.json +75 -69
  100. package/src/account-check.test.ts +0 -149
  101. package/src/account-check.ts +0 -101
  102. package/src/body-sync-gate.test.ts +0 -185
  103. package/src/body-sync-gate.ts +0 -86
  104. package/src/cli.ts +0 -224
  105. package/src/connection-scope.test.ts +0 -266
  106. package/src/connection-scope.ts +0 -314
  107. package/src/credential-less-sync-fence.test.ts +0 -152
  108. package/src/e2e-processor-shim.ts +0 -232
  109. package/src/emit.test.ts +0 -127
  110. package/src/emit.ts +0 -117
  111. package/src/events.ts +0 -252
  112. package/src/failure-kind.test.ts +0 -42
  113. package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
  114. package/src/handlers/append-sent-message.ts +0 -264
  115. package/src/handlers/delete-account-objects.test.ts +0 -128
  116. package/src/handlers/delete-account-objects.ts +0 -94
  117. package/src/handlers/empty-trash.sqlite.test.ts +0 -209
  118. package/src/handlers/empty-trash.ts +0 -352
  119. package/src/handlers/flag-push.ts +0 -285
  120. package/src/handlers/imported-folder.test.ts +0 -63
  121. package/src/handlers/imported-folder.ts +0 -38
  122. package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
  123. package/src/handlers/mailbox-management.ts +0 -486
  124. package/src/handlers/mailbox-sync-order.test.ts +0 -93
  125. package/src/handlers/mailbox-sync-order.ts +0 -65
  126. package/src/handlers/message-copy.sqlite.test.ts +0 -298
  127. package/src/handlers/message-copy.ts +0 -566
  128. package/src/handlers/message-delete-terminal.ts +0 -147
  129. package/src/handlers/message-delete.sqlite.test.ts +0 -291
  130. package/src/handlers/message-delete.ts +0 -885
  131. package/src/handlers/message-move.sqlite.test.ts +0 -271
  132. package/src/handlers/message-move.test.ts +0 -216
  133. package/src/handlers/message-move.ts +0 -752
  134. package/src/handlers/placement-move-push.test.ts +0 -137
  135. package/src/handlers/placement-move-push.ts +0 -475
  136. package/src/handlers/placement-push.sqlite.test.ts +0 -298
  137. package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
  138. package/src/handlers/probe-message-id.ts +0 -15
  139. package/src/handlers/sync-mailboxes.test.ts +0 -183
  140. package/src/handlers/sync-mailboxes.ts +0 -421
  141. package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
  142. package/src/handlers/sync-message-body.test.ts +0 -150
  143. package/src/handlers/sync-message-body.ts +0 -365
  144. package/src/handlers/sync-messages.sqlite.test.ts +0 -183
  145. package/src/handlers/sync-messages.ts +0 -525
  146. package/src/index.test.ts +0 -22
  147. package/src/index.ts +0 -86
  148. package/src/mail-sync-selection.sqlite.test.ts +0 -234
  149. package/src/mailbox-row.ts +0 -20
  150. package/src/pending-sync.sqlite.test.ts +0 -166
  151. package/src/poller.ts +0 -55
  152. package/src/processor.ts +0 -60
  153. package/src/queues.ts +0 -49
  154. package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -75
  155. package/src/scheduler/attachment-sweep.ts +0 -46
  156. package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
  157. package/src/scheduler/calendar-subscriptions.ts +0 -103
  158. package/src/scheduler/config.test.ts +0 -40
  159. package/src/scheduler/decide-due.test.ts +0 -44
  160. package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
  161. package/src/scheduler/loop.test.ts +0 -143
  162. package/src/scheduler/run-tick.sqlite.test.ts +0 -191
  163. package/src/scheduler/run-tick.ts +0 -281
  164. package/src/scheduler/stranded-placement-redrive.ts +0 -265
  165. package/src/stranded-placement.sqlite.test.ts +0 -794
  166. package/src/test-helpers/test-world.ts +0 -427
  167. package/src/thread-join.sqlite.test.ts +0 -106
  168. package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
  169. package/src/with-oauth-lifecycle-deps.ts +0 -71
  170. package/src/with-oauth-lifecycle.ts +0 -158
  171. package/tsconfig.json +0 -8
@@ -1,298 +0,0 @@
1
- /**
2
- * The two pushes body sync leaves behind: a placement move (Junk, a filter's
3
- * folder) and a flag (auto-mark-read). They ride different queues, so either
4
- * can reach the server first, and the flag must never land on the uid of a
5
- * move that has not settled (#496).
6
- */
7
- import assert from "node:assert/strict";
8
- import { after, before, describe, it, mock } from "node:test";
9
- import type { MailboxItem } from "@remit/data-ports";
10
- import {
11
- MailboxCursorState,
12
- MailboxSpecialUse,
13
- MessageStatus,
14
- } from "@remit/domain-enums";
15
- import { PlacementMoveService } from "@remit/mailbox-service";
16
- import type { FlagPushEvent, PlacementMovePushEvent } from "../events.js";
17
- import {
18
- openStore,
19
- type SeededMessage,
20
- type World,
21
- } from "../test-helpers/test-world.js";
22
- import { FLAG_PUSH_DEFER_MAX_MS, handleFlagPush } from "./flag-push.js";
23
- import { handlePlacementMovePush } from "./placement-move-push.js";
24
-
25
- const SEEN = "\\Seen";
26
-
27
- let store: Awaited<ReturnType<typeof openStore>>;
28
-
29
- before(async () => {
30
- store = await openStore();
31
- });
32
-
33
- after(() => store.close());
34
-
35
- interface Arranged {
36
- w: World;
37
- inbox: MailboxItem;
38
- junk: MailboxItem;
39
- message: SeededMessage;
40
- placement: PlacementMoveService;
41
- }
42
-
43
- const arrange = async (): Promise<Arranged> => {
44
- const w = await store.world();
45
- const inbox = await w.folder("INBOX");
46
- const junk = await w.folder("Junk", { specialUse: MailboxSpecialUse.Junk });
47
- const message = await w.message(inbox);
48
- const placement = new PlacementMoveService({
49
- messageService: w.client.message,
50
- threadMessageService: w.client.threadMessage,
51
- markerService: w.client.placementMove,
52
- addressService: w.client.address,
53
- mailboxSpecialUseService: w.client.mailboxSpecialUse,
54
- mailboxService: w.client.mailbox,
55
- queue: w.client.queues.messageManagement,
56
- });
57
- return { w, inbox, junk, message, placement };
58
- };
59
-
60
- const queuedOf = <T>(w: World, type: string): T => {
61
- const event = w
62
- .queued()
63
- .filter((body) => body.type === type)
64
- .at(-1) as T | undefined;
65
- assert.ok(event, `a ${type} was enqueued`);
66
- return event;
67
- };
68
-
69
- const markAsRead = async (a: Arranged): Promise<FlagPushEvent> => {
70
- await a.w.client.flagQueue.markAsRead(
71
- a.w.account.accountConfigId,
72
- a.message.messageId,
73
- a.w.account.accountId,
74
- );
75
- return queuedOf<FlagPushEvent>(a.w, "FLAG_PUSH");
76
- };
77
-
78
- const moveToJunk = async (a: Arranged): Promise<PlacementMovePushEvent> => {
79
- await a.placement.moveMessage(
80
- a.w.account.accountConfigId,
81
- a.message.messageId,
82
- a.junk.mailboxId,
83
- a.w.account.accountId,
84
- );
85
- return queuedOf<PlacementMovePushEvent>(a.w, "PLACEMENT_MOVE_PUSH");
86
- };
87
-
88
- const pushFlag = (a: Arranged, event: FlagPushEvent, receiveCount = 1) =>
89
- handleFlagPush(event, a.w.log, receiveCount, a.w.scope);
90
-
91
- const pushMove = (
92
- a: Arranged,
93
- event: PlacementMovePushEvent,
94
- receiveCount = 1,
95
- ) => handlePlacementMovePush(event, a.w.log, receiveCount, a.w.scope);
96
-
97
- const seenOnServer = (a: Arranged, path: string): boolean[] =>
98
- a.w.imap
99
- .messages(path)
100
- .filter((m) => m.messageIdHeader === a.message.messageIdHeader)
101
- .map((m) => m.flags.includes(SEEN));
102
-
103
- describe("a flag push", () => {
104
- it("stores the flag on the server and clears its marker", async () => {
105
- const a = await arrange();
106
- const event = await markAsRead(a);
107
-
108
- await pushFlag(a, event);
109
-
110
- assert.deepEqual(seenOnServer(a, "INBOX"), [true]);
111
- assert.equal(
112
- await a.w.client.flagPush.find(a.message.messageId, SEEN),
113
- null,
114
- );
115
- });
116
-
117
- it("waits, without connecting, while the message's placement move has not settled", async () => {
118
- const a = await arrange();
119
- await moveToJunk(a);
120
- const event = await markAsRead(a);
121
-
122
- await pushFlag(a, event);
123
-
124
- assert.equal(a.w.imap.connections, 0);
125
- assert.equal(
126
- (await a.w.client.flagPush.find(a.message.messageId, SEEN))?.state,
127
- "pending",
128
- );
129
- assert.deepEqual(seenOnServer(a, "INBOX"), [false]);
130
- });
131
-
132
- it("lands on the moved message once the placement move has settled", async () => {
133
- const a = await arrange();
134
- const move = await moveToJunk(a);
135
- const flag = await markAsRead(a);
136
- await pushFlag(a, flag);
137
-
138
- await pushMove(a, move);
139
- await pushFlag(a, flag);
140
-
141
- assert.deepEqual(a.w.imap.uids("INBOX"), []);
142
- assert.deepEqual(seenOnServer(a, "Junk"), [true]);
143
- assert.equal(
144
- await a.w.client.flagPush.find(a.message.messageId, SEEN),
145
- null,
146
- );
147
- });
148
-
149
- // A move that never settles would otherwise park the flag forever, one
150
- // queue round trip per sync tick.
151
- it("drops the marker without connecting once the move has been stuck past the window", async (t) => {
152
- const a = await arrange();
153
- await moveToJunk(a);
154
- const event = await markAsRead(a);
155
- mock.timers.enable({
156
- apis: ["Date"],
157
- now: Date.now() + FLAG_PUSH_DEFER_MAX_MS + 1,
158
- });
159
- t.after(() => mock.timers.reset());
160
-
161
- await pushFlag(a, event);
162
-
163
- assert.equal(a.w.imap.connections, 0);
164
- assert.equal(
165
- await a.w.client.flagPush.find(a.message.messageId, SEEN),
166
- null,
167
- );
168
- assert.deepEqual(seenOnServer(a, "INBOX"), [false]);
169
- });
170
-
171
- it("drops the marker without connecting when the mailbox was deleted", async () => {
172
- const a = await arrange();
173
- const event = await markAsRead(a);
174
- await a.w.client.mailbox.delete(a.w.account.accountId, a.inbox.mailboxId);
175
-
176
- await pushFlag(a, event);
177
-
178
- assert.equal(a.w.imap.connections, 0);
179
- assert.equal(
180
- await a.w.client.flagPush.find(a.message.messageId, SEEN),
181
- null,
182
- );
183
- });
184
-
185
- it("drops the marker without connecting when the message row is gone", async () => {
186
- const a = await arrange();
187
- const event = await markAsRead(a);
188
- await a.w.client.message.delete(a.message.messageId);
189
-
190
- await pushFlag(a, event);
191
-
192
- assert.equal(a.w.imap.connections, 0);
193
- assert.equal(
194
- await a.w.client.flagPush.find(a.message.messageId, SEEN),
195
- null,
196
- );
197
- });
198
-
199
- it("keeps the marker and stores nothing while the cursor is rebuilding", async () => {
200
- const a = await arrange();
201
- const event = await markAsRead(a);
202
- await a.w.client.mailbox.update(a.w.account.accountId, a.inbox.mailboxId, {
203
- cursorState: MailboxCursorState.rebuilding,
204
- });
205
-
206
- await pushFlag(a, event);
207
-
208
- assert.equal(a.w.imap.connections, 0);
209
- assert.ok(await a.w.client.flagPush.find(a.message.messageId, SEEN));
210
- });
211
-
212
- it("stores nothing when the mailbox's UIDVALIDITY changed under it", async () => {
213
- const a = await arrange();
214
- const event = await markAsRead(a);
215
- a.w.imap.setUidValidity("INBOX", 2);
216
-
217
- await pushFlag(a, event);
218
-
219
- assert.deepEqual(a.w.imap.commands("STORE"), []);
220
- assert.ok(await a.w.client.flagPush.find(a.message.messageId, SEEN));
221
- });
222
-
223
- it("keeps the marker and redelivers when the connection drops", async () => {
224
- const a = await arrange();
225
- const event = await markAsRead(a);
226
- a.w.imap.failNext.STORE = new Error("Connection reset");
227
-
228
- await assert.rejects(pushFlag(a, event), /Connection reset/);
229
-
230
- assert.ok(await a.w.client.flagPush.find(a.message.messageId, SEEN));
231
- });
232
- });
233
-
234
- describe("a placement move push", () => {
235
- it("moves the message on the server, settles the row and clears the marker", async () => {
236
- const a = await arrange();
237
- const event = await moveToJunk(a);
238
-
239
- await pushMove(a, event);
240
-
241
- const [junkUid] = a.w.imap.uids("Junk", a.message.messageIdHeader);
242
- const row = await a.w.row(a.message.messageId);
243
- assert.equal(row?.mailboxId, a.junk.mailboxId);
244
- assert.equal(row?.uid, junkUid);
245
- assert.equal(row?.status, MessageStatus.active);
246
- assert.equal(
247
- await a.w.client.placementMove.find(a.message.messageId),
248
- null,
249
- );
250
- });
251
-
252
- it("drops the marker without connecting when a newer move took the message elsewhere", async () => {
253
- const a = await arrange();
254
- const event = await moveToJunk(a);
255
- const archive = await a.w.folder("Archive");
256
- await a.w.client.message.transitionPlacement(
257
- a.message.messageId,
258
- { mailboxId: a.junk.mailboxId },
259
- { mailboxId: archive.mailboxId },
260
- );
261
-
262
- await pushMove(a, event);
263
-
264
- assert.equal(a.w.imap.connections, 0);
265
- assert.equal(
266
- await a.w.client.placementMove.find(a.message.messageId),
267
- null,
268
- );
269
- });
270
-
271
- it("drops the marker without connecting when the destination mailbox was deleted", async () => {
272
- const a = await arrange();
273
- const event = await moveToJunk(a);
274
- await a.w.client.mailbox.delete(a.w.account.accountId, a.junk.mailboxId);
275
-
276
- await pushMove(a, event);
277
-
278
- assert.equal(a.w.imap.connections, 0);
279
- assert.equal(
280
- await a.w.client.placementMove.find(a.message.messageId),
281
- null,
282
- );
283
- });
284
-
285
- it("keeps the marker and moves nothing while the source cursor is rebuilding", async () => {
286
- const a = await arrange();
287
- const event = await moveToJunk(a);
288
- await a.w.client.mailbox.update(a.w.account.accountId, a.inbox.mailboxId, {
289
- cursorState: MailboxCursorState.rebuilding,
290
- });
291
-
292
- await pushMove(a, event);
293
-
294
- assert.equal(a.w.imap.connections, 0);
295
- assert.ok(await a.w.client.placementMove.find(a.message.messageId));
296
- assert.deepEqual(a.w.imap.uids("INBOX"), [a.message.uid]);
297
- });
298
- });
@@ -1,192 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, beforeEach, describe, it, mock } from "node:test";
3
- import type { MailboxItem } from "@remit/data-ports";
4
- import { recordingQueue } from "@remit/data-ports/recording-queue";
5
- import {
6
- AccountAuthType,
7
- ConnectionState,
8
- MailboxCursorState,
9
- } from "@remit/domain-enums";
10
- import {
11
- createSqliteRepositories,
12
- type DrizzleMessageRepository,
13
- type DrizzleThreadMessageRepository,
14
- type MailboxRepo,
15
- type SqliteRepositories,
16
- } from "@remit/drizzle-service";
17
- import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
18
- import {
19
- type IImapConnection,
20
- type ManagedConnectionFactory,
21
- MessageMoveService,
22
- MessageSyncService,
23
- SyncWarningService,
24
- } from "@remit/mailbox-service";
25
- import type { ImapMessage } from "@remit/mailbox-service/types";
26
- import { probeableMessageIdHeader } from "./probe-message-id.js";
27
-
28
- const ACCOUNT_CONFIG_ID = "cfg-msgid";
29
- const UID_VALIDITY = 7;
30
-
31
- const inboxHolding = (messageIdHeader: string): ImapMessage => ({
32
- uid: 3,
33
- seq: 1,
34
- flags: [],
35
- internalDate: new Date(0),
36
- size: 100,
37
- envelope: {
38
- date: new Date(0).toISOString(),
39
- messageId: messageIdHeader,
40
- subject: "Quarterly report",
41
- from: [{ name: "Sender", mailbox: "sender", host: "example.com" }],
42
- sender: [],
43
- replyTo: [],
44
- to: [],
45
- cc: [],
46
- bcc: [],
47
- inReplyTo: "",
48
- },
49
- });
50
-
51
- const serverWithInbox = (message: ImapMessage): IImapConnection =>
52
- ({
53
- openBox: async (path: string) => ({
54
- name: path,
55
- readOnly: false,
56
- uidvalidity: UID_VALIDITY,
57
- uidnext: message.uid + 1,
58
- flags: [],
59
- permFlags: [],
60
- persistentUIDs: true,
61
- messages: { total: 1, new: 0 },
62
- newKeywords: false,
63
- }),
64
- getMailboxStatus: async () => ({
65
- messages: 1,
66
- recent: 0,
67
- unseen: 0,
68
- uidNext: message.uid + 1,
69
- uidValidity: UID_VALIDITY,
70
- highestModseq: "0",
71
- deletedCount: 0,
72
- }),
73
- supportsCondstore: () => false,
74
- search: async () => [message.uid],
75
- fetchMessages: async () => [message],
76
- }) as unknown as IImapConnection;
77
-
78
- describe("the Message-ID the move, delete and copy probes ask with", () => {
79
- let close: () => void;
80
- let repos: SqliteRepositories;
81
- let accountId: string;
82
- let mailboxes: MailboxRepo;
83
- let messages: DrizzleMessageRepository;
84
- let threadMessages: DrizzleThreadMessageRepository;
85
-
86
- const createMailbox = async (fullPath: string): Promise<MailboxItem> => {
87
- const created = await mailboxes.create({
88
- accountId,
89
- namespacePrefix: "",
90
- hierarchyDelimiter: "/",
91
- fullPath,
92
- uidValidity: UID_VALIDITY,
93
- uidNext: 1,
94
- highestModseq: "0",
95
- messageCount: 0,
96
- unseenCount: 0,
97
- deletedCount: 0,
98
- totalSize: 0,
99
- lastSyncUid: 0,
100
- highWaterMarkUid: 0,
101
- lastMessageSyncAt: 0,
102
- cursorState: MailboxCursorState.normal,
103
- } as Parameters<MailboxRepo["create"]>[0]);
104
- if (created.outcome !== "Created") throw new Error(`${fullPath} taken`);
105
- return created.mailbox;
106
- };
107
-
108
- const syncInbox = async (messageIdHeader: string): Promise<string> => {
109
- const inbox = await createMailbox("INBOX");
110
- const connection = serverWithInbox(inboxHolding(messageIdHeader));
111
- const factory: ManagedConnectionFactory = {
112
- getConnection: () => connection,
113
- close: async () => {},
114
- };
115
- await new MessageSyncService(
116
- factory,
117
- mailboxes,
118
- repos.mailboxSpecialUse,
119
- messages,
120
- repos.envelope,
121
- repos.address,
122
- threadMessages,
123
- new SyncWarningService(repos.syncWarning),
124
- ).syncMessages(inbox.mailboxId, accountId, ACCOUNT_CONFIG_ID);
125
- const [synced] = await messages.listAllByMailbox(inbox.mailboxId);
126
- if (!synced) throw new Error("sync stored no message");
127
- return synced.messageId;
128
- };
129
-
130
- const probeable = (messageId: string): Promise<string | undefined> =>
131
- probeableMessageIdHeader(threadMessages, ACCOUNT_CONFIG_ID, messageId);
132
-
133
- beforeEach(async () => {
134
- const store = createShippedSqliteDb();
135
- close = store.close;
136
- repos = createSqliteRepositories({
137
- app: store.db,
138
- mail: store.db,
139
- search: store.db,
140
- });
141
- mailboxes = repos.mailbox;
142
- messages = repos.message;
143
- threadMessages = repos.threadMessage;
144
- const account = await repos.account.create({
145
- accountConfigId: ACCOUNT_CONFIG_ID,
146
- username: "person@example.com",
147
- email: "person@example.com",
148
- authType: AccountAuthType.Password,
149
- passwordHash: '{"ciphertext":"x"}',
150
- imapHost: "imap.example.com",
151
- imapPort: 993,
152
- imapTls: true,
153
- imapStartTls: false,
154
- isActive: true,
155
- connectionState: ConnectionState.Authenticated,
156
- });
157
- accountId = account.accountId;
158
- });
159
-
160
- afterEach(() => {
161
- mock.restoreAll();
162
- close();
163
- });
164
-
165
- it("reads the Message-ID a real sync stored", async () => {
166
- const messageId = await syncInbox("<report@example.com>");
167
-
168
- assert.equal(await probeable(messageId), "<report@example.com>");
169
- });
170
-
171
- it("reads the source's Message-ID for a copy", async () => {
172
- const sourceId = await syncInbox("<report@example.com>");
173
- const archive = await createMailbox("Archive");
174
- const copyId = await new MessageMoveService({
175
- messageService: messages,
176
- mailboxService: mailboxes,
177
- mailboxSpecialUseService: repos.mailboxSpecialUse,
178
- threadMessageService: threadMessages,
179
- addressService: repos.address,
180
- queue: recordingQueue(),
181
- }).copyMessage(ACCOUNT_CONFIG_ID, sourceId, archive.mailboxId, accountId);
182
-
183
- assert.notEqual(copyId, sourceId);
184
- assert.equal(await probeable(copyId), "<report@example.com>");
185
- });
186
-
187
- it("answers unprobeable for a bounce whose Message-ID is <>", async () => {
188
- const messageId = await syncInbox("<>");
189
-
190
- assert.equal(await probeable(messageId), undefined);
191
- });
192
- });
@@ -1,15 +0,0 @@
1
- import type { IThreadMessageRepository } from "@remit/data-ports";
2
- import { isValidMessageId } from "@remit/data-ports/id";
3
-
4
- export const probeableMessageIdHeader = async (
5
- threadMessageService: Pick<IThreadMessageRepository, "findByMessageId">,
6
- accountConfigId: string,
7
- messageId: string,
8
- ): Promise<string | undefined> => {
9
- const row = await threadMessageService.findByMessageId(
10
- accountConfigId,
11
- messageId,
12
- );
13
- const header = row?.messageIdHeader;
14
- return isValidMessageId(header) ? header : undefined;
15
- };
@@ -1,183 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import {
4
- DEFAULT_MAILBOX_FRESHNESS_MS,
5
- emitSyncMessagesEvents,
6
- MAILBOX_FRESHNESS_MS,
7
- mailboxNeedsSync,
8
- resolveMailboxFreshnessMs,
9
- splitInboxFirst,
10
- } from "./sync-mailboxes.js";
11
-
12
- const NOW = 1_700_000_000_000;
13
-
14
- const askedForByName = { explicitRequest: true } as const;
15
- const sideEffect = {} as const;
16
-
17
- describe("mailboxNeedsSync", () => {
18
- // Issue #37: the gate this replaces applied to every trigger, so a refresh
19
- // that landed just after a side-effect sync did nothing at all. A sync
20
- // asked for by name (POST /sync) is never gated, however recently one ran.
21
- it("syncs a mailbox synced a moment ago when the sync was asked for by name", () => {
22
- const mailbox = { lastMessageSyncAt: NOW - 1_000 };
23
-
24
- assert.equal(mailboxNeedsSync(mailbox, askedForByName, NOW), true);
25
- });
26
-
27
- // Without this, `GET /config` — which fires a trigger per account on every
28
- // call — re-enumerates every folder an account owns on every page load.
29
- it("skips a freshly-synced mailbox for a side-effect trigger", () => {
30
- const mailbox = { lastMessageSyncAt: NOW - 1_000 };
31
-
32
- assert.equal(mailboxNeedsSync(mailbox, sideEffect, NOW), false);
33
- });
34
-
35
- it("syncs a stale mailbox for a side-effect trigger", () => {
36
- const mailbox = { lastMessageSyncAt: NOW - MAILBOX_FRESHNESS_MS - 1 };
37
-
38
- assert.equal(mailboxNeedsSync(mailbox, sideEffect, NOW), true);
39
- });
40
-
41
- it("syncs exactly at the freshness threshold", () => {
42
- const mailbox = { lastMessageSyncAt: NOW - MAILBOX_FRESHNESS_MS };
43
-
44
- assert.equal(mailboxNeedsSync(mailbox, sideEffect, NOW), true);
45
- });
46
-
47
- it("always syncs a mailbox that has never synced", () => {
48
- assert.equal(mailboxNeedsSync({}, sideEffect, NOW), true);
49
- assert.equal(
50
- mailboxNeedsSync({ lastMessageSyncAt: 0 }, sideEffect, NOW),
51
- true,
52
- );
53
- });
54
- });
55
-
56
- describe("resolveMailboxFreshnessMs", () => {
57
- it("defaults to the production window when unset", () => {
58
- assert.equal(resolveMailboxFreshnessMs({}), DEFAULT_MAILBOX_FRESHNESS_MS);
59
- });
60
-
61
- it("honors a valid override", () => {
62
- assert.equal(resolveMailboxFreshnessMs({ MAILBOX_FRESHNESS_MS: "0" }), 0);
63
- assert.equal(
64
- resolveMailboxFreshnessMs({ MAILBOX_FRESHNESS_MS: "1000" }),
65
- 1000,
66
- );
67
- });
68
-
69
- it("falls back to the default for a non-numeric or negative value", () => {
70
- assert.equal(
71
- resolveMailboxFreshnessMs({ MAILBOX_FRESHNESS_MS: "" }),
72
- DEFAULT_MAILBOX_FRESHNESS_MS,
73
- );
74
- assert.equal(
75
- resolveMailboxFreshnessMs({ MAILBOX_FRESHNESS_MS: "abc" }),
76
- DEFAULT_MAILBOX_FRESHNESS_MS,
77
- );
78
- assert.equal(
79
- resolveMailboxFreshnessMs({ MAILBOX_FRESHNESS_MS: "-5" }),
80
- DEFAULT_MAILBOX_FRESHNESS_MS,
81
- );
82
- assert.equal(
83
- resolveMailboxFreshnessMs({ MAILBOX_FRESHNESS_MS: "1.5" }),
84
- DEFAULT_MAILBOX_FRESHNESS_MS,
85
- );
86
- });
87
- });
88
-
89
- describe("splitInboxFirst", () => {
90
- it("pulls INBOX out of the list whatever case it is spelled in", () => {
91
- const { inbox, rest } = splitInboxFirst([
92
- { mailboxId: "mb-sent", fullPath: "Sent" },
93
- { mailboxId: "mb-inbox", fullPath: "Inbox" },
94
- { mailboxId: "mb-junk", fullPath: "Junk" },
95
- ]);
96
-
97
- assert.equal(inbox?.mailboxId, "mb-inbox");
98
- assert.deepEqual(
99
- rest.map((mailbox) => mailbox.mailboxId),
100
- ["mb-sent", "mb-junk"],
101
- );
102
- });
103
-
104
- it("leaves an account without an INBOX intact", () => {
105
- const { inbox, rest } = splitInboxFirst([
106
- { mailboxId: "mb-sent", fullPath: "Sent" },
107
- ]);
108
-
109
- assert.equal(inbox, undefined);
110
- assert.deepEqual(
111
- rest.map((mailbox) => mailbox.mailboxId),
112
- ["mb-sent"],
113
- );
114
- });
115
- });
116
-
117
- describe("emitSyncMessagesEvents", () => {
118
- // Every event of an account shares one FIFO group, so arrival order is
119
- // service order. pMap only bounds concurrency: it used to race all the
120
- // emits, letting INBOX queue behind up to nineteen other folders while a
121
- // person waited for their new mail.
122
- it("emits INBOX before any other folder", async () => {
123
- const emitted: string[] = [];
124
- const mailboxes = Array.from({ length: 25 }, (_, index) => ({
125
- mailboxId: `mb-${index}`,
126
- fullPath: `Folder ${index}`,
127
- }));
128
- mailboxes.push({ mailboxId: "mb-inbox", fullPath: "INBOX" });
129
-
130
- await emitSyncMessagesEvents("acc-1", mailboxes, async (event) => {
131
- emitted.push(event.mailboxId);
132
- });
133
-
134
- assert.equal(emitted[0], "mb-inbox");
135
- assert.equal(emitted.length, 26);
136
- });
137
-
138
- // Emitting INBOX and awaiting it is the whole fix: a fire-and-forget emit
139
- // would leave it racing the fan-out again.
140
- it("waits for the INBOX emit to resolve before emitting the rest", async () => {
141
- const emitted: string[] = [];
142
- let releaseInbox: () => void = () => {};
143
- const inboxEmitted = new Promise<void>((resolve) => {
144
- releaseInbox = resolve;
145
- });
146
-
147
- const done = emitSyncMessagesEvents(
148
- "acc-1",
149
- [
150
- { mailboxId: "mb-inbox", fullPath: "INBOX" },
151
- { mailboxId: "mb-sent", fullPath: "Sent" },
152
- ],
153
- async (event) => {
154
- emitted.push(event.mailboxId);
155
- if (event.mailboxId === "mb-inbox") await inboxEmitted;
156
- },
157
- );
158
-
159
- await new Promise((resolve) => setImmediate(resolve));
160
- assert.deepEqual(emitted, ["mb-inbox"]);
161
-
162
- releaseInbox();
163
- await done;
164
- assert.deepEqual(emitted, ["mb-inbox", "mb-sent"]);
165
- });
166
-
167
- it("emits every folder for an account with no INBOX", async () => {
168
- const emitted: string[] = [];
169
-
170
- await emitSyncMessagesEvents(
171
- "acc-1",
172
- [
173
- { mailboxId: "mb-sent", fullPath: "Sent" },
174
- { mailboxId: "mb-junk", fullPath: "Junk" },
175
- ],
176
- async (event) => {
177
- emitted.push(event.mailboxId);
178
- },
179
- );
180
-
181
- assert.deepEqual(emitted.sort(), ["mb-junk", "mb-sent"]);
182
- });
183
- });