@remit/imap-worker 0.0.90 → 0.0.92

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 (176) 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 +22 -0
  76. package/dist/scheduler/calendar-subscriptions.js +35 -0
  77. package/dist/scheduler/config.d.ts +29 -0
  78. package/{src/scheduler/config.ts → dist/scheduler/config.js} +9 -28
  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/dist/scheduler/handler.js +50 -0
  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 +66 -0
  86. package/dist/scheduler/run-tick.js +164 -0
  87. package/dist/scheduler/runner.d.ts +2 -0
  88. package/dist/scheduler/runner.js +102 -0
  89. package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
  90. package/dist/scheduler/stranded-placement-redrive.js +172 -0
  91. package/dist/scheduler/tick.d.ts +5 -0
  92. package/dist/scheduler/tick.js +5 -0
  93. package/dist/sqs-queues.d.ts +4 -0
  94. package/dist/sqs-queues.js +22 -0
  95. package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
  96. package/dist/with-oauth-lifecycle-deps.js +17 -0
  97. package/dist/with-oauth-lifecycle.d.ts +56 -0
  98. package/dist/with-oauth-lifecycle.js +92 -0
  99. package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
  100. package/dist/worker-version.js +12 -0
  101. package/package.json +79 -69
  102. package/src/account-check.test.ts +0 -149
  103. package/src/account-check.ts +0 -101
  104. package/src/body-sync-gate.test.ts +0 -185
  105. package/src/body-sync-gate.ts +0 -86
  106. package/src/cli.ts +0 -218
  107. package/src/connection-scope.test.ts +0 -266
  108. package/src/connection-scope.ts +0 -314
  109. package/src/credential-less-sync-fence.test.ts +0 -152
  110. package/src/e2e-processor-shim.ts +0 -232
  111. package/src/emit.test.ts +0 -145
  112. package/src/emit.ts +0 -115
  113. package/src/events.ts +0 -252
  114. package/src/failure-kind.test.ts +0 -42
  115. package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
  116. package/src/handlers/append-sent-message.ts +0 -264
  117. package/src/handlers/delete-account-objects.test.ts +0 -126
  118. package/src/handlers/delete-account-objects.ts +0 -96
  119. package/src/handlers/empty-trash.sqlite.test.ts +0 -209
  120. package/src/handlers/empty-trash.ts +0 -352
  121. package/src/handlers/flag-push.ts +0 -285
  122. package/src/handlers/imported-folder.test.ts +0 -63
  123. package/src/handlers/imported-folder.ts +0 -38
  124. package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
  125. package/src/handlers/mailbox-management.ts +0 -486
  126. package/src/handlers/mailbox-sync-order.test.ts +0 -93
  127. package/src/handlers/mailbox-sync-order.ts +0 -65
  128. package/src/handlers/message-copy.sqlite.test.ts +0 -298
  129. package/src/handlers/message-copy.ts +0 -569
  130. package/src/handlers/message-delete-terminal.ts +0 -147
  131. package/src/handlers/message-delete.sqlite.test.ts +0 -291
  132. package/src/handlers/message-delete.ts +0 -888
  133. package/src/handlers/message-move.sqlite.test.ts +0 -271
  134. package/src/handlers/message-move.test.ts +0 -216
  135. package/src/handlers/message-move.ts +0 -755
  136. package/src/handlers/placement-move-push.test.ts +0 -137
  137. package/src/handlers/placement-move-push.ts +0 -478
  138. package/src/handlers/placement-push.sqlite.test.ts +0 -299
  139. package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
  140. package/src/handlers/probe-message-id.ts +0 -15
  141. package/src/handlers/sync-mailboxes.test.ts +0 -183
  142. package/src/handlers/sync-mailboxes.ts +0 -425
  143. package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
  144. package/src/handlers/sync-message-body.test.ts +0 -150
  145. package/src/handlers/sync-message-body.ts +0 -366
  146. package/src/handlers/sync-messages.sqlite.test.ts +0 -195
  147. package/src/handlers/sync-messages.ts +0 -537
  148. package/src/index.test.ts +0 -22
  149. package/src/index.ts +0 -82
  150. package/src/mail-sync-selection.sqlite.test.ts +0 -227
  151. package/src/mailbox-row.ts +0 -20
  152. package/src/pending-sync.sqlite.test.ts +0 -165
  153. package/src/poller.ts +0 -55
  154. package/src/processor.ts +0 -62
  155. package/src/queues.ts +0 -40
  156. package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -74
  157. package/src/scheduler/attachment-sweep.ts +0 -46
  158. package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
  159. package/src/scheduler/calendar-subscriptions.ts +0 -103
  160. package/src/scheduler/config.test.ts +0 -40
  161. package/src/scheduler/decide-due.test.ts +0 -44
  162. package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
  163. package/src/scheduler/handler.ts +0 -69
  164. package/src/scheduler/loop.test.ts +0 -143
  165. package/src/scheduler/run-tick.sqlite.test.ts +0 -191
  166. package/src/scheduler/run-tick.ts +0 -281
  167. package/src/scheduler/runner.ts +0 -125
  168. package/src/scheduler/stranded-placement-redrive.ts +0 -265
  169. package/src/stranded-placement.sqlite.test.ts +0 -799
  170. package/src/test-helpers/test-world.ts +0 -425
  171. package/src/test-helpers/worker-queues.ts +0 -11
  172. package/src/thread-join.sqlite.test.ts +0 -106
  173. package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
  174. package/src/with-oauth-lifecycle-deps.ts +0 -71
  175. package/src/with-oauth-lifecycle.ts +0 -158
  176. package/tsconfig.json +0 -8
@@ -1,299 +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
- ) =>
96
- handlePlacementMovePush(event, a.w.log, a.w.queues, receiveCount, a.w.scope);
97
-
98
- const seenOnServer = (a: Arranged, path: string): boolean[] =>
99
- a.w.imap
100
- .messages(path)
101
- .filter((m) => m.messageIdHeader === a.message.messageIdHeader)
102
- .map((m) => m.flags.includes(SEEN));
103
-
104
- describe("a flag push", () => {
105
- it("stores the flag on the server and clears its marker", async () => {
106
- const a = await arrange();
107
- const event = await markAsRead(a);
108
-
109
- await pushFlag(a, event);
110
-
111
- assert.deepEqual(seenOnServer(a, "INBOX"), [true]);
112
- assert.equal(
113
- await a.w.client.flagPush.find(a.message.messageId, SEEN),
114
- null,
115
- );
116
- });
117
-
118
- it("waits, without connecting, while the message's placement move has not settled", async () => {
119
- const a = await arrange();
120
- await moveToJunk(a);
121
- const event = await markAsRead(a);
122
-
123
- await pushFlag(a, event);
124
-
125
- assert.equal(a.w.imap.connections, 0);
126
- assert.equal(
127
- (await a.w.client.flagPush.find(a.message.messageId, SEEN))?.state,
128
- "pending",
129
- );
130
- assert.deepEqual(seenOnServer(a, "INBOX"), [false]);
131
- });
132
-
133
- it("lands on the moved message once the placement move has settled", async () => {
134
- const a = await arrange();
135
- const move = await moveToJunk(a);
136
- const flag = await markAsRead(a);
137
- await pushFlag(a, flag);
138
-
139
- await pushMove(a, move);
140
- await pushFlag(a, flag);
141
-
142
- assert.deepEqual(a.w.imap.uids("INBOX"), []);
143
- assert.deepEqual(seenOnServer(a, "Junk"), [true]);
144
- assert.equal(
145
- await a.w.client.flagPush.find(a.message.messageId, SEEN),
146
- null,
147
- );
148
- });
149
-
150
- // A move that never settles would otherwise park the flag forever, one
151
- // queue round trip per sync tick.
152
- it("drops the marker without connecting once the move has been stuck past the window", async (t) => {
153
- const a = await arrange();
154
- await moveToJunk(a);
155
- const event = await markAsRead(a);
156
- mock.timers.enable({
157
- apis: ["Date"],
158
- now: Date.now() + FLAG_PUSH_DEFER_MAX_MS + 1,
159
- });
160
- t.after(() => mock.timers.reset());
161
-
162
- await pushFlag(a, event);
163
-
164
- assert.equal(a.w.imap.connections, 0);
165
- assert.equal(
166
- await a.w.client.flagPush.find(a.message.messageId, SEEN),
167
- null,
168
- );
169
- assert.deepEqual(seenOnServer(a, "INBOX"), [false]);
170
- });
171
-
172
- it("drops the marker without connecting when the mailbox was deleted", async () => {
173
- const a = await arrange();
174
- const event = await markAsRead(a);
175
- await a.w.client.mailbox.delete(a.w.account.accountId, a.inbox.mailboxId);
176
-
177
- await pushFlag(a, event);
178
-
179
- assert.equal(a.w.imap.connections, 0);
180
- assert.equal(
181
- await a.w.client.flagPush.find(a.message.messageId, SEEN),
182
- null,
183
- );
184
- });
185
-
186
- it("drops the marker without connecting when the message row is gone", async () => {
187
- const a = await arrange();
188
- const event = await markAsRead(a);
189
- await a.w.client.message.delete(a.message.messageId);
190
-
191
- await pushFlag(a, event);
192
-
193
- assert.equal(a.w.imap.connections, 0);
194
- assert.equal(
195
- await a.w.client.flagPush.find(a.message.messageId, SEEN),
196
- null,
197
- );
198
- });
199
-
200
- it("keeps the marker and stores nothing while the cursor is rebuilding", async () => {
201
- const a = await arrange();
202
- const event = await markAsRead(a);
203
- await a.w.client.mailbox.update(a.w.account.accountId, a.inbox.mailboxId, {
204
- cursorState: MailboxCursorState.rebuilding,
205
- });
206
-
207
- await pushFlag(a, event);
208
-
209
- assert.equal(a.w.imap.connections, 0);
210
- assert.ok(await a.w.client.flagPush.find(a.message.messageId, SEEN));
211
- });
212
-
213
- it("stores nothing when the mailbox's UIDVALIDITY changed under it", async () => {
214
- const a = await arrange();
215
- const event = await markAsRead(a);
216
- a.w.imap.setUidValidity("INBOX", 2);
217
-
218
- await pushFlag(a, event);
219
-
220
- assert.deepEqual(a.w.imap.commands("STORE"), []);
221
- assert.ok(await a.w.client.flagPush.find(a.message.messageId, SEEN));
222
- });
223
-
224
- it("keeps the marker and redelivers when the connection drops", async () => {
225
- const a = await arrange();
226
- const event = await markAsRead(a);
227
- a.w.imap.failNext.STORE = new Error("Connection reset");
228
-
229
- await assert.rejects(pushFlag(a, event), /Connection reset/);
230
-
231
- assert.ok(await a.w.client.flagPush.find(a.message.messageId, SEEN));
232
- });
233
- });
234
-
235
- describe("a placement move push", () => {
236
- it("moves the message on the server, settles the row and clears the marker", async () => {
237
- const a = await arrange();
238
- const event = await moveToJunk(a);
239
-
240
- await pushMove(a, event);
241
-
242
- const [junkUid] = a.w.imap.uids("Junk", a.message.messageIdHeader);
243
- const row = await a.w.row(a.message.messageId);
244
- assert.equal(row?.mailboxId, a.junk.mailboxId);
245
- assert.equal(row?.uid, junkUid);
246
- assert.equal(row?.status, MessageStatus.active);
247
- assert.equal(
248
- await a.w.client.placementMove.find(a.message.messageId),
249
- null,
250
- );
251
- });
252
-
253
- it("drops the marker without connecting when a newer move took the message elsewhere", async () => {
254
- const a = await arrange();
255
- const event = await moveToJunk(a);
256
- const archive = await a.w.folder("Archive");
257
- await a.w.client.message.transitionPlacement(
258
- a.message.messageId,
259
- { mailboxId: a.junk.mailboxId },
260
- { mailboxId: archive.mailboxId },
261
- );
262
-
263
- await pushMove(a, event);
264
-
265
- assert.equal(a.w.imap.connections, 0);
266
- assert.equal(
267
- await a.w.client.placementMove.find(a.message.messageId),
268
- null,
269
- );
270
- });
271
-
272
- it("drops the marker without connecting when the destination mailbox was deleted", async () => {
273
- const a = await arrange();
274
- const event = await moveToJunk(a);
275
- await a.w.client.mailbox.delete(a.w.account.accountId, a.junk.mailboxId);
276
-
277
- await pushMove(a, event);
278
-
279
- assert.equal(a.w.imap.connections, 0);
280
- assert.equal(
281
- await a.w.client.placementMove.find(a.message.messageId),
282
- null,
283
- );
284
- });
285
-
286
- it("keeps the marker and moves nothing while the source cursor is rebuilding", async () => {
287
- const a = await arrange();
288
- const event = await moveToJunk(a);
289
- await a.w.client.mailbox.update(a.w.account.accountId, a.inbox.mailboxId, {
290
- cursorState: MailboxCursorState.rebuilding,
291
- });
292
-
293
- await pushMove(a, event);
294
-
295
- assert.equal(a.w.imap.connections, 0);
296
- assert.ok(await a.w.client.placementMove.find(a.message.messageId));
297
- assert.deepEqual(a.w.imap.uids("INBOX"), [a.message.uid]);
298
- });
299
- });
@@ -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().fifo,
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
- });