@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,365 +0,0 @@
1
- import { getClient } from "@remit/backend/client";
2
- import { isNotFoundError } from "@remit/data-ports/errors";
3
- import { SYNC_WARNING_SURFACE_ATTEMPTS } from "@remit/data-ports/sync-warning-state";
4
- import type { Logger } from "@remit/logger";
5
- import { recordImapFailure } from "@remit/logger-lambda/metrics";
6
- import {
7
- BodySyncService,
8
- guardConnectionCursor,
9
- isCursorRebuildNeeded,
10
- isFolderMutationInFlight,
11
- MailboxCursorPausedError,
12
- PlacementMoveService,
13
- QuarantineService,
14
- resolveExhaustedBodySyncFailures,
15
- resolveFolderPlacement,
16
- SyncWarningService,
17
- } from "@remit/mailbox-service";
18
- import { buildFilterConfig } from "@remit/mailbox-service/filter-config";
19
- import { attemptBudget } from "@remit/sqs-client/attempt-budget";
20
- import { env } from "expect-env";
21
- import { isAccountDeleted } from "../account-check.js";
22
- import { isBodySyncEnabled } from "../body-sync-gate.js";
23
- import {
24
- borrowWarmConnection,
25
- createConnectionScopeWithCredentials,
26
- } from "../connection-scope.js";
27
- import type { SyncMessageBodyEvent } from "../events.js";
28
- import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
29
- import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
30
- import { workerVersion } from "../worker-version.js";
31
-
32
- const bodySyncEnabledParameterName = env.BODY_SYNC_ENABLED_PARAMETER_NAME;
33
-
34
- export const getBodySyncMaxAttempts = (
35
- processEnv: NodeJS.ProcessEnv = process.env,
36
- ): number =>
37
- attemptBudget(
38
- "BODY_SYNC_MAX_ATTEMPTS",
39
- SYNC_WARNING_SURFACE_ATTEMPTS,
40
- processEnv,
41
- );
42
-
43
- export const BODY_SYNC_MAX_ATTEMPTS = getBodySyncMaxAttempts();
44
-
45
- /**
46
- * The ordered message ids to sync, plus a uid lookup when the event carried the
47
- * preferred `messages` shape. The uid map lets the body-sync service skip the
48
- * per-message DDB get; legacy events (ids only) leave it undefined and the
49
- * service resolves uids itself.
50
- */
51
- export interface ResolvedBatch {
52
- messageIds: string[];
53
- uidByMessageId?: Map<string, number>;
54
- force: boolean;
55
- }
56
-
57
- export const resolveBatch = (event: SyncMessageBodyEvent): ResolvedBatch => {
58
- const force = event.force === true;
59
- if (event.messages !== undefined) {
60
- return {
61
- messageIds: event.messages.map((m) => m.messageId),
62
- uidByMessageId: new Map(
63
- event.messages.map((m): [string, number] => [m.messageId, m.uid]),
64
- ),
65
- force,
66
- };
67
- }
68
- return { messageIds: event.messageIds, force };
69
- };
70
-
71
- /**
72
- * Build the loud, structured error thrown when a batch still has failed
73
- * messages and SQS redelivery budget left. Throwing (rather than swallowing
74
- * into a fresh re-enqueue) is what lets a genuine processing failure reach
75
- * the body-dlq once `BODY_SYNC_MAX_ATTEMPTS` is exhausted — the SQS-level
76
- * redrive owns retry scheduling now, not this handler.
77
- */
78
- export const buildRetryableFailureError = (
79
- failedMessageIds: string[],
80
- receiveCount: number,
81
- ): Error =>
82
- new Error(
83
- `Body sync failed for ${failedMessageIds.length} message(s) ` +
84
- `(attempt ${receiveCount}/${BODY_SYNC_MAX_ATTEMPTS}): ${failedMessageIds.join(", ")}`,
85
- );
86
-
87
- export const syncMessageBody = async (
88
- event: SyncMessageBodyEvent,
89
- log: Logger,
90
- receiveCount = 1,
91
- createConnectionScope = createConnectionScopeWithCredentials,
92
- ): Promise<void> => {
93
- const { accountId, mailboxId } = event;
94
-
95
- // Prefer the messageId+uid pairs when present (one ranged FETCH, no
96
- // per-message UID lookup); fall back to the legacy id-only list otherwise.
97
- const { messageIds, force } = resolveBatch(event);
98
-
99
- log.info(
100
- {
101
- event: event.type,
102
- accountId,
103
- mailboxId,
104
- messageCount: messageIds.length,
105
- hasUids: event.messages !== undefined,
106
- force,
107
- receiveCount,
108
- },
109
- "Handling event",
110
- );
111
-
112
- // Pause gate stays first: ack-and-skip before touching any account/IMAP state.
113
- if (!(await isBodySyncEnabled(bodySyncEnabledParameterName, log))) {
114
- log.info(
115
- {
116
- event: event.type,
117
- accountId,
118
- mailboxId,
119
- messageCount: messageIds.length,
120
- },
121
- "Body sync paused via SSM toggle, acking and skipping",
122
- );
123
- return;
124
- }
125
-
126
- const {
127
- account: accountService,
128
- mailbox: mailboxService,
129
- mailboxSpecialUse: mailboxSpecialUseService,
130
- message: messageService,
131
- threadMessage: threadMessageService,
132
- address: addressService,
133
- envelope: envelopeService,
134
- placementMove: markerService,
135
- filter: filterService,
136
- messageLabel: messageLabelService,
137
- storage,
138
- secrets,
139
- mailboxSpecialUse: mailboxSpecialUseRepository,
140
- quarantine: quarantineRepository,
141
- syncWarning: syncWarningRepository,
142
- flagQueue: flagQueueService,
143
- calendarSuggestion: calendarSuggestionService,
144
- calendarStore,
145
- queues,
146
- } = await getClient();
147
-
148
- const account = await accountService.get(accountId);
149
- if (!account) {
150
- throw new Error(`Account ${accountId} not found`);
151
- }
152
-
153
- if (isAccountDeleted(account, log)) {
154
- return;
155
- }
156
-
157
- // PLACEMENT_MOVE_PUSH rides messageMgmtQueue (issue #1271) — same queue
158
- // the general MessageMoveService move/delete/copy machinery uses, not a
159
- // dedicated one.
160
- const placementMoveService = new PlacementMoveService({
161
- messageService,
162
- threadMessageService,
163
- markerService,
164
- addressService,
165
- mailboxSpecialUseService,
166
- mailboxService,
167
- queue: queues.messageManagement,
168
- });
169
-
170
- await withOAuthLifecycle(
171
- buildLifecycleDeps(secrets, accountService),
172
- account,
173
- log,
174
- async (credentials) => {
175
- // A SYNC_MESSAGE_BODY trigger can outlive the mailbox it targets: deleting
176
- // a folder that held mail leaves already-queued body events pointing at a
177
- // row that is now gone, and the lookup then throws NotFoundError forever.
178
- // Since the body queue carries MessageGroupId=accountId, that head message
179
- // stalls the account's whole body pipeline (issues #287, #289, #290). A
180
- // deleted folder is an expected terminal outcome: ack with a WARN.
181
- const mailbox = await mailboxService
182
- .get(accountId, mailboxId)
183
- .catch((error: unknown) => {
184
- if (isNotFoundError(error)) return null;
185
- throw error;
186
- });
187
- // A folder still `pending` is terminal for the same reason: the batch
188
- // was cut before the folder reached the server, so there is nothing
189
- // there to fetch from and no retry that changes it.
190
- if (!mailbox || isFolderMutationInFlight(mailbox)) {
191
- log.warn(
192
- { accountId, mailboxId, eventId: event.eventId },
193
- "Skipping SYNC_MESSAGE_BODY: a folder mutation is in flight",
194
- );
195
- return;
196
- }
197
-
198
- // Cheap frugal skip (epic #1281 invariant 6): a mailbox already known
199
- // paused never even borrows a connection. This is an optimization only
200
- // — guardConnectionCursor below is the structural guarantee, so a
201
- // handler that forgot this check still cannot reach a stale UID.
202
- if (isCursorRebuildNeeded(mailbox.cursorState)) {
203
- log.info(
204
- { accountId, mailboxId, cursorState: mailbox.cursorState },
205
- "Mailbox cursor not normal; pausing outbound body sync this round",
206
- );
207
- return;
208
- }
209
-
210
- // Warm reuse: borrow a live IMAP connection from the module-scoped pool
211
- // (keyed by accountId) instead of dialing a fresh one per invocation. A
212
- // warm container skips TCP+TLS+LOGIN+SELECT; a dead pooled connection is
213
- // liveness-checked and replaced inside borrowWarmConnection.
214
- const borrowed = borrowWarmConnection(accountId, () =>
215
- createConnectionScope(account, credentials),
216
- );
217
-
218
- // A confident, actionable placement verdict moves mail directly on
219
- // body-sync. Safety lives in the verdict itself (only confident,
220
- // INBOX/Junk-only) and the movedByRemit loop guard.
221
- const placementConfig = {
222
- mailboxSpecialUseService,
223
- placementMoveService,
224
- };
225
-
226
- // A matched filter's actions (label upsert, exclusive move) apply on
227
- // the same body-sync pass, reusing the placement mover for the move
228
- // (RFC 034 Decision 3.1).
229
- const filterConfig = buildFilterConfig({
230
- filterService,
231
- messageLabelService,
232
- mailboxService,
233
- placementMoveService,
234
- });
235
-
236
- const bodySyncService = new BodySyncService(
237
- messageService,
238
- storage,
239
- threadMessageService,
240
- addressService,
241
- envelopeService,
242
- new SyncWarningService(syncWarningRepository),
243
- log,
244
- placementConfig,
245
- filterConfig,
246
- {
247
- quarantineService: new QuarantineService(
248
- quarantineRepository,
249
- mailboxSpecialUseRepository,
250
- workerVersion(),
251
- log,
252
- ),
253
- mailboxId,
254
- uidValidity: mailbox.uidValidity ?? 0,
255
- attempts: receiveCount,
256
- },
257
- { flagQueueService },
258
- // An invitation becomes a card the first time the message is seen,
259
- // alongside the filters that run on the same pass (issue #1033).
260
- { calendarSuggestionService, calendarStore, filterService },
261
- );
262
-
263
- // Guard at the openBox choke point (epic #1281 invariants 3 & 5). The
264
- // served UIDVALIDITY is only knowable by opening the box, which only a
265
- // genuine fetch needs — BodySyncService.syncBodies skips connecting
266
- // entirely when every message already has a stored body (nothing would
267
- // touch a stored UID either way) — so the check fires lazily, the first
268
- // time `openBox` is actually called, instead of forcing an extra open
269
- // on every event.
270
- const getConnectionChecked = async () =>
271
- guardConnectionCursor(
272
- await borrowed.getConnection(),
273
- { mailboxService },
274
- accountId,
275
- mailbox,
276
- );
277
-
278
- // Both the sync attempt AND the retry-exhaustion resolution below run
279
- // against the SAME borrowed connection, so the mailbox stays open
280
- // across the two — the connection is only released once both are done.
281
- const folder = {
282
- mailboxId: mailbox.mailboxId,
283
- fullPath: mailbox.fullPath,
284
- uidValidity: mailbox.uidValidity,
285
- placement: await resolveFolderPlacement(
286
- mailboxSpecialUseService,
287
- accountId,
288
- mailbox.mailboxId,
289
- ),
290
- };
291
-
292
- await (async () => {
293
- const result = await bodySyncService.syncBodies(
294
- messageIds,
295
- accountId,
296
- account.accountConfigId,
297
- folder,
298
- getConnectionChecked,
299
- force,
300
- );
301
-
302
- if (result.failedMessageIds.length === 0) {
303
- return;
304
- }
305
-
306
- if (receiveCount < BODY_SYNC_MAX_ATTEMPTS) {
307
- // Redelivery budget remains: let SQS retry the whole record
308
- // naturally. Messages that already synced skip via the
309
- // already-stored guard on the next attempt, so only the failed
310
- // ones actually redo work. This — not a manual re-enqueue — is
311
- // what lets a genuine processing failure ever reach the body-dlq.
312
- throw buildRetryableFailureError(
313
- result.failedMessageIds,
314
- receiveCount,
315
- );
316
- }
317
-
318
- // Redelivery budget exhausted: a persistent per-message failure is
319
- // never a steady state (epic #1281 invariant 3). Resolve every
320
- // failed id into exactly one of the two terminal outcomes instead of
321
- // letting the record dead-letter with no diagnosis.
322
- const { brokenMessageIds } = await resolveExhaustedBodySyncFailures(
323
- {
324
- messageService,
325
- storageService: storage,
326
- log,
327
- },
328
- {
329
- accountId,
330
- accountConfigId: account.accountConfigId,
331
- mailboxId,
332
- mailboxPath: mailbox.fullPath,
333
- failedMessageIds: result.failedMessageIds,
334
- getConnection: getConnectionChecked,
335
- },
336
- );
337
-
338
- // A broken body is terminal on a record the handler acks, so the
339
- // handler-outcome series never sees it.
340
- if (brokenMessageIds.length > 0) {
341
- recordImapFailure(
342
- "SYNC_MESSAGE_BODY_BROKEN",
343
- "other",
344
- brokenMessageIds.length,
345
- );
346
- }
347
- })()
348
- .catch((error: unknown) => {
349
- // Expected pause (epic #1281 invariant 3), not a fault: ack and
350
- // skip rather than propagating into SQS retry/DLQ. The read stays
351
- // served from whatever is already stored; the fetch resumes once
352
- // the mailbox returns to normal.
353
- if (error instanceof MailboxCursorPausedError) {
354
- log.info(
355
- { accountId, mailboxId, cursorState: error.state },
356
- "UIDVALIDITY changed; mailbox cursor tripped, pausing outbound body sync",
357
- );
358
- return;
359
- }
360
- throw error;
361
- })
362
- .finally(() => borrowed.release());
363
- },
364
- );
365
- };
@@ -1,183 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { after, before, describe, it } from "node:test";
3
- import type { SyncMessagesEvent } from "../events.js";
4
- import { openStore, type World } from "../test-helpers/test-world.js";
5
- import { drainPendingFlagPushes, syncMessages } from "./sync-messages.js";
6
-
7
- let store: Awaited<ReturnType<typeof openStore>>;
8
-
9
- before(async () => {
10
- store = await openStore();
11
- });
12
-
13
- after(() => store.close());
14
-
15
- const syncEvent = (w: World, mailboxId: string): SyncMessagesEvent => ({
16
- type: "SYNC_MESSAGES",
17
- accountId: w.account.accountId,
18
- mailboxId,
19
- eventId: `evt-${mailboxId}`,
20
- timestamp: Date.now(),
21
- });
22
-
23
- const sync = async (w: World, mailboxId: string): Promise<number> => {
24
- await syncMessages(syncEvent(w, mailboxId), w.log, w.connectionFactory);
25
- return w.imap.connections;
26
- };
27
-
28
- describe("which SYNC_MESSAGES events reach the server", () => {
29
- it("a live mailbox does", async () => {
30
- const w = await store.world();
31
- const inbox = await w.folder("INBOX");
32
-
33
- assert.equal(await sync(w, inbox.mailboxId), 1);
34
- });
35
-
36
- // Issue #287: a NotFoundError at the head of the account's FIFO group
37
- // stalled every later event for the account.
38
- it("a mailbox whose row is gone is acked without connecting", async () => {
39
- const w = await store.world();
40
- const old = await w.folder("Old");
41
- await w.client.mailbox.delete(w.account.accountId, old.mailboxId);
42
-
43
- assert.equal(await sync(w, old.mailboxId), 0);
44
- });
45
-
46
- it("a mailbox being deleted is acked without connecting", async () => {
47
- const w = await store.world();
48
- const old = await w.folder("Old");
49
- await w.client.mailboxQueue.deleteMailbox(
50
- old.mailboxId,
51
- w.account.accountId,
52
- );
53
-
54
- assert.equal(await sync(w, old.mailboxId), 0);
55
- });
56
-
57
- it("a mailbox not yet created on the server is acked without connecting", async () => {
58
- const w = await store.world();
59
- const created = await w.client.mailboxQueue.createMailbox(
60
- {
61
- accountId: w.account.accountId,
62
- namespacePrefix: "",
63
- hierarchyDelimiter: "/",
64
- fullPath: "Receipts",
65
- uidValidity: 0,
66
- uidNext: 0,
67
- highestModseq: "0",
68
- messageCount: 0,
69
- unseenCount: 0,
70
- deletedCount: 0,
71
- totalSize: 0,
72
- lastSyncUid: 0,
73
- highWaterMarkUid: 0,
74
- lastMessageSyncAt: 0,
75
- },
76
- w.account.accountId,
77
- );
78
- if (created.outcome !== "Created") throw new Error("path taken");
79
-
80
- assert.equal(await sync(w, created.mailbox.mailboxId), 0);
81
- });
82
- });
83
-
84
- // Issue #339: a delete asked for while the round is in flight lands inside
85
- // the guard, and the round then fails deeper on a folder that is gone.
86
- describe("a folder deleted while its sync is in flight", () => {
87
- it("resolves the event and records no error phase on the account", async () => {
88
- const w = await store.world();
89
- const old = await w.folder("Old");
90
- const connectionFactory = () => {
91
- const factory = w.connectionFactory();
92
- const connection = factory.getConnection();
93
- const openBox = connection.openBox;
94
- let first = true;
95
- connection.openBox = async (path, readOnly) => {
96
- if (first) {
97
- first = false;
98
- await w.client.mailboxQueue.deleteMailbox(
99
- old.mailboxId,
100
- w.account.accountId,
101
- );
102
- w.imap.removeFolder("Old");
103
- }
104
- return openBox(path, readOnly);
105
- };
106
- return { getConnection: () => connection, close: factory.close };
107
- };
108
-
109
- await syncMessages(syncEvent(w, old.mailboxId), w.log, connectionFactory);
110
-
111
- const account = await w.client.account.get(w.account.accountId);
112
- assert.notEqual(account.syncPhase, "error");
113
- });
114
- });
115
-
116
- describe("drainPendingFlagPushes (issue #1273)", () => {
117
- const marker = (w: World, mailboxId: string, messageId: string) =>
118
- w.client.flagPush.put({
119
- messageId,
120
- flagName: "\\Seen",
121
- accountId: w.account.accountId,
122
- accountConfigId: w.account.accountConfigId,
123
- mailboxId,
124
- operation: "add",
125
- });
126
-
127
- it("re-arms the markers stuck before their wake-up and leaves the ones a driver owns", async () => {
128
- const w = await store.world();
129
- const inbox = await w.folder("INBOX");
130
- await marker(w, inbox.mailboxId, "msg-pending");
131
- await marker(w, inbox.mailboxId, "msg-queued");
132
- await w.client.flagPush.updateState("msg-queued", "\\Seen", "queued");
133
- await marker(w, inbox.mailboxId, "msg-processing");
134
- await w.client.flagPush.updateState(
135
- "msg-processing",
136
- "\\Seen",
137
- "processing",
138
- );
139
-
140
- await drainPendingFlagPushes(
141
- w.client.flagPush,
142
- w.account,
143
- inbox.mailboxId,
144
- w.log,
145
- );
146
-
147
- assert.deepEqual(
148
- w
149
- .queued()
150
- .filter((body) => body.type === "FLAG_PUSH")
151
- .map((body) => body.messageId),
152
- ["msg-pending"],
153
- );
154
- });
155
-
156
- it("keeps re-arming the others when one re-arm fails, and says so loudly", async () => {
157
- const w = await store.world();
158
- const inbox = await w.folder("INBOX");
159
- await marker(w, inbox.mailboxId, "will-fail");
160
- await marker(w, inbox.mailboxId, "will-succeed");
161
- const armed: string[] = [];
162
-
163
- await drainPendingFlagPushes(
164
- w.client.flagPush,
165
- w.account,
166
- inbox.mailboxId,
167
- w.log,
168
- async (event) => {
169
- const { messageId } = event as unknown as { messageId: string };
170
- if (messageId === "will-fail") throw new Error("queue down");
171
- armed.push(messageId);
172
- },
173
- );
174
-
175
- assert.deepEqual(armed, ["will-succeed"]);
176
- assert.equal(
177
- w.logs.filter(
178
- (line) => line.fields.alert === "flag_push_drain_rearm_failed",
179
- ).length,
180
- 1,
181
- );
182
- });
183
- });