@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,427 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { randomUUID } from "node:crypto";
3
- import { mkdtempSync, rmSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
- import {
7
- _resetForTest,
8
- type RemitClient,
9
- setClient,
10
- } from "@remit/backend/client";
11
- import { composeSqliteClient } from "@remit/backend/compose-sqlite";
12
- import { buildSharedDeps } from "@remit/backend/shared-deps";
13
- import type {
14
- AccountItem,
15
- MailboxItem,
16
- MailboxSpecialUseValue,
17
- MessageItem,
18
- OutboxMessageItem,
19
- QueueProducer,
20
- } from "@remit/data-ports";
21
- import {
22
- type RecordingQueue,
23
- recordingQueue,
24
- } from "@remit/data-ports/recording-queue";
25
- import {
26
- AccountAuthType,
27
- ConnectionState,
28
- MailboxCursorState,
29
- } from "@remit/domain-enums";
30
- import { createShippedSqliteFiles } from "@remit/drizzle-service/test-sqlite";
31
- import type { Logger } from "@remit/logger";
32
- import type { ManagedConnectionFactory } from "@remit/mailbox-service";
33
- import {
34
- eml,
35
- FakeImapServer,
36
- } from "@remit/mailbox-service/test-helpers/fake-imap";
37
- import { serializeEncryptedPayload } from "@remit/secrets-service";
38
- import type { createConnectionScopeWithCredentials } from "../connection-scope.js";
39
- import { syncMessages } from "../handlers/sync-messages.js";
40
- import { setWorkerQueues } from "../queues.js";
41
-
42
- export interface LogLine {
43
- level: string;
44
- fields: Record<string, unknown>;
45
- }
46
-
47
- export const capturingLogger = (lines: LogLine[]): Logger => {
48
- const at =
49
- (level: string) =>
50
- (fields: Record<string, unknown>): void => {
51
- lines.push({ level, fields });
52
- };
53
- const log = {
54
- info: at("info"),
55
- warn: at("warn"),
56
- error: at("error"),
57
- debug: at("debug"),
58
- fatal: at("fatal"),
59
- trace: at("trace"),
60
- child: () => log,
61
- } as unknown as Logger;
62
- return log;
63
- };
64
-
65
- /** A message the way a correspondent sends one, naming it by its Message-ID. */
66
- export const mail = (messageIdHeader: string): Buffer =>
67
- eml({
68
- "Message-ID": messageIdHeader,
69
- From: "Friend <friend@example.com>",
70
- To: "Person <person@example.com>",
71
- Date: "Tue, 14 Nov 2023 22:13:20 +0000",
72
- Subject: "test",
73
- "Content-Type": "text/plain; charset=utf-8",
74
- });
75
-
76
- export interface FolderOptions {
77
- specialUse?: MailboxSpecialUseValue;
78
- uidValidity?: number;
79
- cursorState?: MailboxItem["cursorState"];
80
- }
81
-
82
- export interface SeededMessage {
83
- messageId: string;
84
- uid: number;
85
- messageIdHeader: string;
86
- threadMessageId: string;
87
- }
88
-
89
- /**
90
- * One SQLite store per test file, over the shipped migrations, composed by the
91
- * backend's own SQLite composition root over filesystem storage — every
92
- * repository and service a handler reaches is the one a self-host process
93
- * runs. Each `world()` is a fresh account on it, with its own in-memory mail
94
- * server at the IMAP port, and a message reaches the store the way mail does:
95
- * delivered to the server, then synced.
96
- */
97
- export const openStore = async (): Promise<{
98
- client: RemitClient;
99
- bindWorkerQueues: () => void;
100
- world: (accountConfigId?: string) => Promise<World>;
101
- close: () => void;
102
- }> => {
103
- const directory = mkdtempSync(join(tmpdir(), "imap-worker-world-"));
104
- createShippedSqliteFiles(directory);
105
- delete process.env.S3_BUCKET_NAME;
106
- process.env.STORAGE_LOCAL_PATH = join(directory, "storage");
107
- process.env.BETTER_AUTH_URL = "https://mail.example.com";
108
- process.env.BETTER_AUTH_SECRET = "a-signing-secret-of-at-least-32-characters";
109
- _resetForTest();
110
- let current = recordingQueue();
111
- const queue: QueueProducer<unknown> = {
112
- send: (message) => current.send(message),
113
- sendBatch: (messages) => current.sendBatch(messages),
114
- };
115
- const client = await composeSqliteClient(
116
- buildSharedDeps({
117
- sync: queue,
118
- accountFanout: queue,
119
- smtp: queue,
120
- messageManagement: queue,
121
- body: queue,
122
- }),
123
- );
124
- setClient(client);
125
- const bindWorkerQueues = (): void =>
126
- setWorkerQueues({
127
- mailboxes: queue,
128
- messages: queue,
129
- flags: queue,
130
- body: queue,
131
- mailboxManagement: queue,
132
- messageManagement: queue,
133
- });
134
- bindWorkerQueues();
135
- return {
136
- client,
137
- bindWorkerQueues,
138
- world: (accountConfigId = `cfg-${randomUUID()}`) => {
139
- current = recordingQueue();
140
- return createWorld(client, current, accountConfigId);
141
- },
142
- close: () => {
143
- _resetForTest();
144
- rmSync(directory, { recursive: true, force: true });
145
- },
146
- };
147
- };
148
-
149
- export interface World {
150
- client: RemitClient;
151
- imap: FakeImapServer;
152
- account: AccountItem;
153
- logs: LogLine[];
154
- log: Logger;
155
- folder: (path: string, options?: FolderOptions) => Promise<MailboxItem>;
156
- /** A folder of a second account on this world's account config. */
157
- siblingFolder: (path: string) => Promise<MailboxItem>;
158
- message: (
159
- mailbox: MailboxItem,
160
- options?: { messageIdHeader?: string },
161
- ) => Promise<SeededMessage>;
162
- row: (messageId: string) => Promise<MessageItem | undefined>;
163
- threadRows: (
164
- messageId: string,
165
- ) => Promise<Array<{ mailboxId: string; uid?: number; isDeleted: boolean }>>;
166
- queued: () => Array<Record<string, unknown>>;
167
- refuseSends: (error: Error | undefined) => void;
168
- queue: RecordingQueue;
169
- draft: (
170
- attachment?: Buffer,
171
- ) => Promise<{ outbox: OutboxMessageItem; attachmentId?: string }>;
172
- scope: typeof createConnectionScopeWithCredentials;
173
- connectionFactory: () => ManagedConnectionFactory;
174
- }
175
-
176
- const createWorld = async (
177
- client: RemitClient,
178
- queue: RecordingQueue,
179
- accountConfigId: string,
180
- ): Promise<World> => {
181
- const imap = new FakeImapServer();
182
- const logs: LogLine[] = [];
183
- const password = await client.secrets.encrypt("app-password");
184
- const account = await client.account.create({
185
- accountConfigId,
186
- username: "person@example.com",
187
- email: "person@example.com",
188
- authType: AccountAuthType.Password,
189
- passwordHash: JSON.stringify(serializeEncryptedPayload(password)),
190
- imapHost: "imap.example.com",
191
- imapPort: 993,
192
- imapTls: true,
193
- imapStartTls: false,
194
- isActive: true,
195
- connectionState: ConnectionState.Authenticated,
196
- });
197
-
198
- const folder = async (
199
- path: string,
200
- options: FolderOptions = {},
201
- ): Promise<MailboxItem> => {
202
- const uidValidity = options.uidValidity ?? 1;
203
- imap.addFolder(path, { uidValidity });
204
- const created = await client.mailbox.create({
205
- accountId: account.accountId,
206
- namespacePrefix: "",
207
- hierarchyDelimiter: "/",
208
- fullPath: path,
209
- uidValidity,
210
- uidNext: 1,
211
- highestModseq: "0",
212
- messageCount: 0,
213
- unseenCount: 0,
214
- deletedCount: 0,
215
- totalSize: 0,
216
- lastSyncUid: 0,
217
- highWaterMarkUid: 0,
218
- lastMessageSyncAt: 0,
219
- syncStatus: "synced",
220
- });
221
- if (created.outcome !== "Created") {
222
- throw new Error(`${path} could not be created: ${created.outcome}`);
223
- }
224
- const mailbox = created.mailbox;
225
- if (options.specialUse) {
226
- await client.mailboxSpecialUse.create(
227
- mailbox.mailboxId,
228
- options.specialUse,
229
- );
230
- }
231
- if (
232
- options.cursorState &&
233
- options.cursorState !== MailboxCursorState.normal
234
- ) {
235
- await client.mailbox.update(account.accountId, mailbox.mailboxId, {
236
- cursorState: options.cursorState,
237
- });
238
- return client.mailbox.get(account.accountId, mailbox.mailboxId);
239
- }
240
- return mailbox;
241
- };
242
-
243
- const siblingFolder = async (path: string): Promise<MailboxItem> => {
244
- const sibling = await client.account.create({
245
- accountConfigId,
246
- username: "person-too@example.com",
247
- email: "person-too@example.com",
248
- authType: AccountAuthType.Password,
249
- passwordHash: JSON.stringify(serializeEncryptedPayload(password)),
250
- imapHost: "imap.example.com",
251
- imapPort: 993,
252
- imapTls: true,
253
- imapStartTls: false,
254
- isActive: true,
255
- connectionState: ConnectionState.Authenticated,
256
- });
257
- const created = await client.mailbox.create({
258
- accountId: sibling.accountId,
259
- namespacePrefix: "",
260
- hierarchyDelimiter: "/",
261
- fullPath: path,
262
- uidValidity: 1,
263
- uidNext: 1,
264
- highestModseq: "0",
265
- messageCount: 0,
266
- unseenCount: 0,
267
- deletedCount: 0,
268
- totalSize: 0,
269
- lastSyncUid: 0,
270
- highWaterMarkUid: 0,
271
- lastMessageSyncAt: 0,
272
- syncStatus: "synced",
273
- });
274
- if (created.outcome !== "Created") {
275
- throw new Error(`${path} could not be created: ${created.outcome}`);
276
- }
277
- return created.mailbox;
278
- };
279
-
280
- const message = async (
281
- mailbox: MailboxItem,
282
- options: { messageIdHeader?: string } = {},
283
- ): Promise<SeededMessage> => {
284
- const messageIdHeader =
285
- options.messageIdHeader ?? `<${randomUUID()}@example.com>`;
286
- const uid = imap.deliver(mailbox.fullPath, mail(messageIdHeader));
287
- const baseline = {
288
- connections: imap.connections,
289
- commands: imap.log.length,
290
- };
291
- await syncMessages(
292
- {
293
- type: "SYNC_MESSAGES",
294
- eventId: randomUUID(),
295
- timestamp: Date.now(),
296
- accountId: account.accountId,
297
- mailboxId: mailbox.mailboxId,
298
- },
299
- capturingLogger([]),
300
- connectionFactory,
301
- );
302
- imap.connections = baseline.connections;
303
- imap.log.length = baseline.commands;
304
- const synced = (
305
- await client.message.listAllByMailbox(mailbox.mailboxId)
306
- ).find((row) => row.uid === uid);
307
- if (!synced)
308
- throw new Error(`uid ${uid} in ${mailbox.fullPath} did not sync`);
309
- const [threadMessage] = await client.threadMessage.findAllByMessageId(
310
- accountConfigId,
311
- synced.messageId,
312
- );
313
- if (!threadMessage)
314
- throw new Error(`uid ${uid} synced without a listing row`);
315
- return {
316
- messageId: synced.messageId,
317
- uid,
318
- messageIdHeader,
319
- threadMessageId: threadMessage.threadMessageId,
320
- };
321
- };
322
-
323
- const row = async (messageId: string): Promise<MessageItem | undefined> =>
324
- (await client.message.get([messageId]))[0];
325
-
326
- const threadRows = async (messageId: string) =>
327
- (
328
- await client.threadMessage.findAllByMessageId(accountConfigId, messageId)
329
- ).map((tm) => ({
330
- mailboxId: tm.mailboxId,
331
- uid: tm.uid,
332
- isDeleted: tm.isDeleted,
333
- }));
334
-
335
- const queued = (): Array<Record<string, unknown>> =>
336
- queue.sent.map((event) => event.body);
337
-
338
- const refuseSends = (error: Error | undefined): void => {
339
- queue.answer(async () => {
340
- if (error) throw error;
341
- return {};
342
- });
343
- };
344
-
345
- const draft = async (
346
- attachment?: Buffer,
347
- ): Promise<{ outbox: OutboxMessageItem; attachmentId?: string }> => {
348
- const outbox = await client.outboxQueue.createDraft({
349
- accountId: account.accountId,
350
- accountConfigId,
351
- fromAddress: "person@example.com",
352
- fromName: "Person",
353
- toAddresses: ["friend@example.com"],
354
- subject: "Quarterly numbers",
355
- textBody: "See you Thursday.",
356
- });
357
- if (!attachment) return { outbox };
358
- const minted = await client.outboxAttachment.mint({
359
- accountConfigId,
360
- outboxMessageId: outbox.outboxMessageId,
361
- filename: "numbers.csv",
362
- contentType: "text/csv",
363
- sizeBytes: attachment.length,
364
- });
365
- if (minted.outcome !== "Minted") throw new Error("the upload was refused");
366
- const attachmentId = minted.reservation.outboxAttachmentId;
367
- await client.storage.storeOutboxAttachment({
368
- accountConfigId,
369
- accountId: account.accountId,
370
- outboxMessageId: outbox.outboxMessageId,
371
- outboxAttachmentId: attachmentId,
372
- content: attachment,
373
- });
374
- const completed = await client.outboxAttachment.complete({
375
- accountConfigId,
376
- outboxMessageId: outbox.outboxMessageId,
377
- outboxAttachmentId: attachmentId,
378
- });
379
- assert.equal(completed.outcome, "Completed");
380
- return { outbox, attachmentId };
381
- };
382
-
383
- const scope = () => {
384
- let open: ReturnType<FakeImapServer["connection"]> | undefined;
385
- return {
386
- getConnection: async () => {
387
- open ??= imap.connection();
388
- return open;
389
- },
390
- disconnect: async () => {
391
- open = undefined;
392
- },
393
- };
394
- };
395
-
396
- const connectionFactory = (): ManagedConnectionFactory => {
397
- let open: ReturnType<FakeImapServer["connection"]> | undefined;
398
- return {
399
- getConnection: () => {
400
- open ??= imap.connection();
401
- return open;
402
- },
403
- close: async () => {
404
- open = undefined;
405
- },
406
- };
407
- };
408
-
409
- return {
410
- client,
411
- imap,
412
- account,
413
- logs,
414
- log: capturingLogger(logs),
415
- folder,
416
- siblingFolder,
417
- message,
418
- row,
419
- threadRows,
420
- queued,
421
- refuseSends,
422
- queue,
423
- draft,
424
- scope,
425
- connectionFactory,
426
- };
427
- };
@@ -1,106 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type {
4
- IMailboxRepository,
5
- IMailboxSpecialUseRepository,
6
- MailboxItem,
7
- } from "@remit/data-ports";
8
- import { deriveThreadId } from "@remit/data-ports/id";
9
- import { createSqliteRepositories } from "@remit/drizzle-service";
10
- import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
11
- import { MessageSyncService, SyncWarningService } from "@remit/mailbox-service";
12
- import type { ImapMessage } from "@remit/mailbox-service/types";
13
-
14
- const ACCOUNT = "acct-1";
15
- const CONFIG = "cfg-1";
16
- const MAILBOX = { mailboxId: "mbx-1", fullPath: "INBOX" } as MailboxItem;
17
- const ROLES = {
18
- junkMailboxId: null,
19
- trashMailboxId: null,
20
- configJunkRoles: { junkMailboxIds: [], trashMailboxIds: [] },
21
- };
22
-
23
- const message = (
24
- uid: number,
25
- messageId: string,
26
- inReplyTo: string,
27
- references?: string[],
28
- ): ImapMessage =>
29
- ({
30
- uid,
31
- seq: uid,
32
- size: 100,
33
- internalDate: new Date(uid * 1000),
34
- flags: [],
35
- references,
36
- envelope: {
37
- date: new Date(uid * 1000).toISOString(),
38
- messageId,
39
- subject: `Subject ${uid}`,
40
- from: [{ name: "Sender", mailbox: "sender", host: "example.com" }],
41
- sender: [],
42
- replyTo: [],
43
- to: [],
44
- cc: [],
45
- bcc: [],
46
- inReplyTo,
47
- },
48
- }) as unknown as ImapMessage;
49
-
50
- const PARENT = message(1, "<p@example.com>", "<a@example.com>", [
51
- "<a@example.com>",
52
- ]);
53
- const REPLY = message(2, "<r@example.com>", "<p@example.com>");
54
-
55
- const syncInOrder = async (
56
- messages: ImapMessage[],
57
- ): Promise<{ threadIds: (messageId: string) => string[] }> => {
58
- const { db, sqlite } = createShippedSqliteDb();
59
- const repos = createSqliteRepositories({ app: db, mail: db, search: db });
60
- const service = new MessageSyncService(
61
- {} as never,
62
- {} as IMailboxRepository,
63
- {} as IMailboxSpecialUseRepository,
64
- repos.message,
65
- repos.envelope,
66
- repos.address,
67
- repos.threadMessage,
68
- new SyncWarningService(repos.syncWarning),
69
- );
70
- const save = (
71
- service as unknown as {
72
- saveMessage: (...args: unknown[]) => Promise<unknown>;
73
- }
74
- ).saveMessage.bind(service);
75
- for (const msg of messages) {
76
- await save(MAILBOX, ACCOUNT, CONFIG, msg, ROLES, {
77
- departed: new Set(),
78
- settled: new Set(),
79
- });
80
- }
81
- return {
82
- threadIds: (messageIdHeader) =>
83
- (
84
- sqlite
85
- .prepare(
86
- "SELECT thread_id FROM thread_message WHERE message_id_header = ?",
87
- )
88
- .all(messageIdHeader) as { thread_id: string }[]
89
- ).map((row) => row.thread_id),
90
- };
91
- };
92
-
93
- describe("a reply naming only its parent, which is itself a reply", () => {
94
- const rootThread = deriveThreadId(CONFIG, "<a@example.com>");
95
-
96
- it("joins the root's thread when the parent synced first", async () => {
97
- const { threadIds } = await syncInOrder([PARENT, REPLY]);
98
- assert.deepEqual(threadIds("<r@example.com>"), [rootThread]);
99
- });
100
-
101
- it("joins the root's thread when it synced before its parent, and stays there on a re-save", async () => {
102
- const { threadIds } = await syncInOrder([REPLY, PARENT, REPLY]);
103
- assert.deepEqual(threadIds("<r@example.com>"), [rootThread]);
104
- assert.deepEqual(threadIds("<p@example.com>"), [rootThread]);
105
- });
106
- });