@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,421 +0,0 @@
1
- import { getClient } from "@remit/backend/client";
2
- import { writeFolderRoleAppointment } from "@remit/backend/folder-role-appointments";
3
- import {
4
- bindImportedFolders,
5
- type ConfigBinderDeps,
6
- disableFiltersMissingFolders,
7
- } from "@remit/config-transfer";
8
- import type {
9
- AccountItem,
10
- IAccountRepository,
11
- IMailboxRepository,
12
- IMailboxSpecialUseRepository,
13
- } from "@remit/data-ports";
14
- import type { CanonicalMailboxRoleValue } from "@remit/data-ports/folder-role";
15
- import { SyncPhase } from "@remit/domain-enums";
16
- import type { Logger } from "@remit/logger";
17
- import { RefreshTokenError } from "@remit/mail-oauth-service";
18
- import {
19
- createConnectionWithCredentials,
20
- MailboxQueueService,
21
- MailboxSyncService,
22
- MailConnectionError,
23
- type MailCredentials,
24
- } from "@remit/mailbox-service";
25
- import pMap from "p-map";
26
- import {
27
- isAccountDeleted,
28
- isMailSyncDisabled,
29
- isUnsyncableHost,
30
- } from "../account-check.js";
31
- import { emitEvent } from "../emit.js";
32
- import type { SyncMailboxesEvent, SyncMessagesEvent } from "../events.js";
33
- import { workerQueues } from "../queues.js";
34
- import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
35
- import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
36
- import { createImportedFolder } from "./imported-folder.js";
37
- import { orderMailboxesForSync } from "./mailbox-sync-order.js";
38
-
39
- const EVENT_EMIT_CONCURRENCY = 20;
40
-
41
- /**
42
- * How recently a mailbox must have been synced for a side-effect trigger to
43
- * leave it alone. Short enough that a folder is never more than a minute
44
- * staler than the trigger that arrived, long enough to collapse the burst a
45
- * client produces when it loads (`GET /config` triggers a sync per account)
46
- * into one round of IMAP work.
47
- *
48
- * This gates side-effect triggers only. The web client's automatic poll asks
49
- * by name and so skips it; what bounds that one is its own floor
50
- * (`MIN_POLL_INTERVAL_MS` in useStaleAccountSync, 30s).
51
- */
52
- export const DEFAULT_MAILBOX_FRESHNESS_MS = 60_000;
53
-
54
- /**
55
- * Resolve the freshness window from the environment, defaulting to production's
56
- * {@link DEFAULT_MAILBOX_FRESHNESS_MS}. Deployments that drive sync from a test
57
- * harness rather than a real client set `MAILBOX_FRESHNESS_MS` low so their
58
- * explicit nudges are never no-oped by the gate; nothing else touches it, so
59
- * production keeps the default.
60
- */
61
- export const resolveMailboxFreshnessMs = (
62
- env: NodeJS.ProcessEnv = process.env,
63
- ): number => {
64
- const value = env.MAILBOX_FRESHNESS_MS?.trim();
65
- if (!value) return DEFAULT_MAILBOX_FRESHNESS_MS;
66
- const raw = Number(value);
67
- return Number.isInteger(raw) && raw >= 0 ? raw : DEFAULT_MAILBOX_FRESHNESS_MS;
68
- };
69
-
70
- export const MAILBOX_FRESHNESS_MS = resolveMailboxFreshnessMs();
71
-
72
- /**
73
- * Whether this fan-out should enqueue a mailbox.
74
- *
75
- * A sync asked for by name — POST /sync, which is the refresh control,
76
- * pull-to-refresh, and the client's automatic poll — always syncs every
77
- * mailbox, whatever ran a moment ago. A sync that happens as a side effect of
78
- * something else (config load, OAuth connect, account create, the scheduled
79
- * tick) skips mailboxes synced inside {@link MAILBOX_FRESHNESS_MS}, which is
80
- * what stops those triggers from re-enumerating an account's folders on every
81
- * page load.
82
- *
83
- * A mailbox that has never synced is always due.
84
- */
85
- export const mailboxNeedsSync = (
86
- mailbox: { lastMessageSyncAt?: number },
87
- event: Pick<SyncMailboxesEvent, "explicitRequest">,
88
- now: number,
89
- ): boolean => {
90
- if (event.explicitRequest) return true;
91
- if (!mailbox.lastMessageSyncAt) return true;
92
- return now - mailbox.lastMessageSyncAt >= MAILBOX_FRESHNESS_MS;
93
- };
94
-
95
- export const syncMailboxes = async (
96
- event: SyncMailboxesEvent,
97
- log: Logger,
98
- ): Promise<void> => {
99
- const client = await getClient();
100
- const {
101
- account: accountService,
102
- mailbox: mailboxService,
103
- mailboxSpecialUse: mailboxSpecialUseService,
104
- secrets,
105
- } = client;
106
- const binder: ConfigBinderDeps = {
107
- repositories: client,
108
- appointFolderRole: (configId, accountId, role, mailboxId, lastKnownPath) =>
109
- writeFolderRoleAppointment(
110
- client.accountSetting,
111
- configId,
112
- accountId,
113
- role as CanonicalMailboxRoleValue,
114
- mailboxId,
115
- lastKnownPath,
116
- ),
117
- createFolder: createImportedFolder(
118
- mailboxService,
119
- new MailboxQueueService({
120
- mailboxService,
121
- queue: workerQueues().mailboxManagement,
122
- logger: log,
123
- }),
124
- ),
125
- };
126
-
127
- const { accountId } = event;
128
- log.info({ event: event.type, accountId }, "Handling event");
129
-
130
- const account = await accountService.get(accountId);
131
- if (!account) {
132
- throw new Error(`Account ${accountId} not found`);
133
- }
134
-
135
- if (isAccountDeleted(account, log)) {
136
- return;
137
- }
138
-
139
- if (isMailSyncDisabled(account, log)) {
140
- return;
141
- }
142
-
143
- // A reserved/never-resolvable IMAP host (RFC 2606) can never connect, so a
144
- // sync attempt would retry and dead-letter forever. Skip cleanly — ack the
145
- // event without connecting or throwing.
146
- if (isUnsyncableHost(account, log)) {
147
- return;
148
- }
149
-
150
- // withOAuthLifecycle owns the reauth/ACK contract (skip-if-reauth, flip on
151
- // terminal auth failure, rethrow transient). The inner try/catch only
152
- // records the terminal non-auth error phase before letting the wrapper
153
- // rethrow for SQS retry/DLQ.
154
- await withOAuthLifecycle(
155
- buildLifecycleDeps(secrets, accountService),
156
- account,
157
- log,
158
- async (credentials) => {
159
- try {
160
- await syncMailboxesForAccount(
161
- event,
162
- account,
163
- credentials,
164
- mailboxService,
165
- mailboxSpecialUseService,
166
- accountService,
167
- binder,
168
- log,
169
- );
170
- } catch (err) {
171
- // Auth failures are handled by the wrapper — rethrow untouched so it
172
- // flips the account to reauth_required rather than recording an error
173
- // phase.
174
- if (
175
- err instanceof RefreshTokenError ||
176
- (err instanceof MailConnectionError && err.kind === "auth")
177
- ) {
178
- throw err;
179
- }
180
- // Record the terminal error phase before crashing (let-it-crash:
181
- // record state, then rethrow so the event is retried/DLQ'd).
182
- const message = err instanceof Error ? err.message : String(err);
183
- await accountService.update(accountId, {
184
- syncPhase: SyncPhase.error,
185
- lastError: message,
186
- });
187
- throw err;
188
- }
189
- },
190
- );
191
- };
192
-
193
- const syncMailboxesForAccount = async (
194
- event: SyncMailboxesEvent,
195
- account: AccountItem,
196
- credentials: MailCredentials,
197
- mailboxService: IMailboxRepository,
198
- mailboxSpecialUseService: IMailboxSpecialUseRepository,
199
- accountService: IAccountRepository,
200
- binder: ConfigBinderDeps,
201
- log: Logger,
202
- ): Promise<void> => {
203
- const { accountId } = account;
204
-
205
- const connection = createConnectionWithCredentials(
206
- {
207
- username: account.username,
208
- imapHost: account.imapHost,
209
- imapPort: account.imapPort,
210
- imapTls: account.imapTls,
211
- },
212
- credentials,
213
- );
214
-
215
- await connection.connect();
216
-
217
- await accountService.markAuthenticated(accountId);
218
-
219
- // Phase transition: discovering mailboxes
220
- await accountService.update(accountId, {
221
- syncPhase: SyncPhase.discovering_mailboxes,
222
- });
223
-
224
- const mailboxSyncService = new MailboxSyncService(
225
- mailboxService,
226
- mailboxSpecialUseService,
227
- log,
228
- );
229
-
230
- const result = await mailboxSyncService
231
- .syncMailboxes({ accountId }, connection)
232
- .finally(() => connection.disconnect());
233
-
234
- await accountService.update(accountId, { lastSyncAt: Date.now() });
235
-
236
- log.info({ result }, "Mailbox sync complete");
237
-
238
- // The folder list this account holds is now known, which is the one thing a
239
- // configuration import could not know when it ran: every folder in a file is
240
- // named by IMAP path, and the ids are minted here. Binding what is bindable
241
- // belongs at exactly this point, and is idempotent, so a later discovery
242
- // simply finds nothing left to do.
243
- const bound = await bindImportedFolders(
244
- binder,
245
- account.accountConfigId,
246
- accountId,
247
- );
248
- if (Object.values(bound).some((count) => count > 0)) {
249
- log.info({ accountId, ...bound }, "Bound imported folder references");
250
- }
251
-
252
- const disabled = await disableFiltersMissingFolders(
253
- binder.repositories,
254
- account.accountConfigId,
255
- );
256
- if (disabled > 0) {
257
- log.warn(
258
- { accountId, disabled },
259
- "Disabled filters whose move folder no longer exists",
260
- );
261
- }
262
-
263
- const allMailboxes = await collectAllMailboxes(accountId, mailboxService);
264
-
265
- // This fan-out is where an account's IMAP work is decided: one SEARCH per
266
- // mailbox per event, on a queue every account shares. `GET /config` fires a
267
- // trigger per account on every call, so without a gate here an idle client
268
- // re-enumerates every folder it owns on every page load.
269
- //
270
- // The gate is per mailbox and a sync asked for by name skips it outright.
271
- // That distinction is the whole point: the previous cooldown gated
272
- // everything, which is what made a refresh a no-op whenever a side-effect
273
- // trigger had just run (issue #37).
274
- const now = Date.now();
275
- const mailboxes = allMailboxes.filter((mailbox) =>
276
- mailboxNeedsSync(mailbox, event, now),
277
- );
278
-
279
- const skipped = allMailboxes.length - mailboxes.length;
280
- if (skipped > 0) {
281
- log.info(
282
- { accountId, skipped },
283
- "Skipped recently-synced mailboxes for a side-effect trigger",
284
- );
285
- }
286
-
287
- if (allMailboxes.length === 0) {
288
- log.info({ accountId }, "No mailboxes to sync messages for");
289
- await accountService.update(accountId, {
290
- syncPhase: SyncPhase.complete,
291
- mailboxCountTotal: 0,
292
- mailboxCountSynced: 0,
293
- });
294
- return;
295
- }
296
-
297
- if (mailboxes.length === 0) {
298
- log.info({ accountId }, "Every mailbox is fresh; nothing to sync");
299
- await accountService.update(accountId, {
300
- syncPhase: SyncPhase.complete,
301
- mailboxCountTotal: allMailboxes.length,
302
- mailboxCountSynced: allMailboxes.length,
303
- });
304
- return;
305
- }
306
-
307
- log.info(
308
- { accountId, count: mailboxes.length },
309
- "Emitting SYNC_MESSAGES events",
310
- );
311
-
312
- // Phase transition. Only the enqueued mailboxes emit a completion, so the
313
- // gated ones are pre-credited — otherwise synced could never reach total.
314
- //
315
- // The counter is progress through THIS round, not a lifetime total: a new
316
- // round restarts it, which is why it can read lower than a moment ago while
317
- // a previous round is still draining. That is the same instant
318
- // `account.lastSyncAt` is stamped, and the per-mailbox completion guard in
319
- // sync-messages.ts keys off exactly that stamp — so a completion still in
320
- // flight from the previous round counts once towards the new one, and each
321
- // mailbox counts at most once per round.
322
- const inboxEnqueued = mailboxes.some(
323
- (m) => m.fullPath.toUpperCase() === "INBOX",
324
- );
325
- await accountService.update(accountId, {
326
- syncPhase: inboxEnqueued
327
- ? SyncPhase.syncing_inbox
328
- : SyncPhase.syncing_others,
329
- mailboxCountTotal: allMailboxes.length,
330
- mailboxCountSynced: skipped,
331
- });
332
-
333
- await emitSyncMessagesEvents(accountId, mailboxes, emitEvent);
334
- };
335
-
336
- type SyncMessagesInput = Omit<SyncMessagesEvent, "eventId" | "timestamp">;
337
-
338
- /**
339
- * Separate INBOX from the rest of the fan-out. `collectAllMailboxes` sorts it
340
- * first, but sort order alone decides nothing once the emits go out together.
341
- */
342
- export const splitInboxFirst = <T extends { fullPath: string }>(
343
- mailboxes: readonly T[],
344
- ): { inbox: T | undefined; rest: T[] } => {
345
- const at = mailboxes.findIndex(
346
- (mailbox) => mailbox.fullPath.toUpperCase() === "INBOX",
347
- );
348
- if (at === -1) return { inbox: undefined, rest: [...mailboxes] };
349
- return {
350
- inbox: mailboxes[at],
351
- rest: mailboxes.filter((_, index) => index !== at),
352
- };
353
- };
354
-
355
- /**
356
- * Emit INBOX's SYNC_MESSAGES event and wait for it before emitting the rest.
357
- *
358
- * Every event of an account shares one FIFO group (`MessageGroupId =
359
- * accountId`), so the queue serves them strictly in arrival order. Emitting
360
- * the whole list through `pMap` only bounds concurrency — it races the writes,
361
- * and INBOX could land behind up to `EVENT_EMIT_CONCURRENCY - 1` other
362
- * folders. The mail a person pressed refresh for then arrived only after every
363
- * one of those folders had finished syncing.
364
- */
365
- export const emitSyncMessagesEvents = async (
366
- accountId: string,
367
- mailboxes: readonly { mailboxId: string; fullPath: string }[],
368
- emit: (event: SyncMessagesInput) => Promise<unknown>,
369
- ): Promise<void> => {
370
- const eventFor = (mailboxId: string): SyncMessagesInput => ({
371
- type: "SYNC_MESSAGES",
372
- accountId,
373
- mailboxId,
374
- });
375
-
376
- const { inbox, rest } = splitInboxFirst(mailboxes);
377
- if (inbox) await emit(eventFor(inbox.mailboxId));
378
- await pMap(rest, ({ mailboxId }) => emit(eventFor(mailboxId)), {
379
- concurrency: EVENT_EMIT_CONCURRENCY,
380
- });
381
- };
382
-
383
- type MailboxSortEntry = {
384
- mailboxId: string;
385
- fullPath: string;
386
- lastMessageSyncAt: number;
387
- specialUse?: readonly string[];
388
- };
389
-
390
- /**
391
- * Collect all mailboxes for an account, ordered for sync fan-out by
392
- * special-use: INBOX first, then Sent/Drafts, then normal folders, with
393
- * Junk/Spam and Trash last (issue #567). Real mail dispatches ahead of bulk
394
- * folders so a fresh account fills its inbox before its spam.
395
- */
396
- const collectAllMailboxes = async (
397
- accountId: string,
398
- mailboxService: IMailboxRepository,
399
- ): Promise<MailboxSortEntry[]> => {
400
- const mailboxes: MailboxSortEntry[] = [];
401
- let continuationToken: string | undefined;
402
-
403
- do {
404
- const result = await mailboxService.listByAccount(accountId, {
405
- continuationToken,
406
- });
407
-
408
- for (const mailbox of result.items) {
409
- mailboxes.push({
410
- mailboxId: mailbox.mailboxId,
411
- fullPath: mailbox.fullPath,
412
- lastMessageSyncAt: mailbox.lastMessageSyncAt,
413
- specialUse: mailbox.specialUse,
414
- });
415
- }
416
-
417
- continuationToken = result.continuationToken ?? undefined;
418
- } while (continuationToken);
419
-
420
- return orderMailboxesForSync(mailboxes);
421
- };