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