@remit/imap-worker 0.0.90 → 0.0.92

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