@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,125 +0,0 @@
1
- #!/usr/bin/env node
2
- import { setTimeout as delay } from "node:timers/promises";
3
- import { finishHalfDeletedAccounts } from "@remit/backend/account-deletion";
4
- import { getClient } from "@remit/backend/client";
5
- import { createLogger } from "@remit/logger";
6
- import { installNodeLogger } from "@remit/logger-lambda/node";
7
- import { backfillSearchText } from "@remit/mailbox-service";
8
- import { clearHeartbeats, createHeartbeat } from "@remit/sqs-client/heartbeat";
9
-
10
- import { createEmitEvent } from "../emit.js";
11
- import { sqsWorkerQueues, workerQueueUrlsFromEnv } from "../queues.js";
12
- import { buildAttachmentSweep } from "./attachment-sweep.js";
13
- import { buildCalendarSubscriptionRefresh } from "./calendar-subscriptions.js";
14
- import { getOfflineIntervalMs, getTickIntervalMs } from "./config.js";
15
- import { runSchedulerLoop } from "./loop.js";
16
- import { runSchedulerTick } from "./run-tick.js";
17
- import { redriveStrandedPlacements } from "./stranded-placement-redrive.js";
18
-
19
- installNodeLogger();
20
-
21
- /**
22
- * The scheduled-sync runner for every deployment that has no EventBridge: the
23
- * self-host stack's `scheduler` service and the local dev compose stack. A
24
- * managed deployment fires `runSchedulerTick` off an EventBridge schedule (see
25
- * handler.ts); this process ticks the same function on a plain loop at the same
26
- * `MAILBOX_SYNC_TICK_INTERVAL_SECONDS` cadence, so there is one scheduling
27
- * implementation and one set of knobs.
28
- *
29
- * A tick failure crashes the process loudly rather than being swallowed —
30
- * compose's `restart: unless-stopped` brings it back for the next tick.
31
- *
32
- * Liveness is a heartbeat file, the same mechanism as a worker's poll loop (D1
33
- * of docs/design/standalone-observability.md); loop.ts carries where in the loop
34
- * it is written and why. Clearing the previous generation's file here is what
35
- * extends that answer across a restart. A completed round is still not the same
36
- * as mail arriving: the accounts that came due are enqueued for workers to
37
- * fetch, and whether that fetch lands is what
38
- * `remit_account_sync_age_seconds` measures.
39
- */
40
-
41
- const log = createLogger();
42
-
43
- const queues = sqsWorkerQueues(workerQueueUrlsFromEnv());
44
-
45
- const tickIntervalMs = getTickIntervalMs();
46
- const offlineIntervalMs = getOfflineIntervalMs();
47
-
48
- // A persistent failure (e.g. Postgres not up yet at container boot) throws
49
- // before the loop ever reaches its own `delay`, so `restart: unless-stopped`
50
- // would otherwise respawn the process immediately — a tight, log-flooding
51
- // crash loop (review #1250). This fixed pause before exiting is not retry
52
- // logic (there is nothing to retry here; the container restart IS the
53
- // retry) — it only paces how fast that restart can happen.
54
- const CRASH_BACKOFF_MS = 5_000;
55
-
56
- log.info(
57
- { tickIntervalMs, offlineIntervalMs },
58
- "Scheduled-sync runner started",
59
- );
60
-
61
- const runLoop = async (): Promise<void> => {
62
- // Neither call on the monitoring file may take the scheduler down with it. An
63
- // unreadable or full /data/heartbeat is a reason to keep enqueuing mail, not
64
- // to stop. A clear that fails leaves the previous generation's file, which
65
- // ages out at the same threshold; a write that fails is the missed beat that
66
- // is itself the signal. Two messages because they are different problems: one
67
- // is a volume this container could not read at boot, the other a write it
68
- // could not make this round.
69
- const onClearError = (error: unknown): void => {
70
- log.error({ error }, "Scheduled-sync heartbeat clear failed");
71
- };
72
- const onBeatError = (error: unknown): void => {
73
- log.error({ error }, "Scheduled-sync heartbeat write failed");
74
- };
75
-
76
- await clearHeartbeats().catch(onClearError);
77
- const client = await getClient();
78
- const { account } = client;
79
- await finishHalfDeletedAccounts({
80
- account,
81
- configImport: client.configImport,
82
- queue: client.queues.accountFanout,
83
- log,
84
- });
85
- const searchTextBackfill = backfillSearchText(
86
- { messageService: client.message, storageService: client.storage },
87
- log,
88
- ).then((result) => {
89
- log.debug({ ...result }, "Search text backfill finished");
90
- });
91
- const schedulerLoop = runSchedulerLoop({
92
- tick: runSchedulerTick,
93
- tickDeps: {
94
- accountService: account,
95
- queue: queues.mailboxes,
96
- log,
97
- tickIntervalMs,
98
- offlineIntervalMs,
99
- sweepAttachments: buildAttachmentSweep(client, log),
100
- refreshCalendarSubscriptions: buildCalendarSubscriptionRefresh(
101
- client,
102
- log,
103
- ),
104
- redriveStrandedPlacements: (stranded, now) =>
105
- redriveStrandedPlacements(
106
- { client, emit: createEmitEvent(queues), log },
107
- stranded,
108
- now,
109
- ),
110
- },
111
- heartbeat: createHeartbeat("tick"),
112
- onHeartbeatError: onBeatError,
113
- tickIntervalMs,
114
- });
115
- await Promise.all([searchTextBackfill, schedulerLoop]);
116
- };
117
-
118
- runLoop()
119
- .catch(async (error) => {
120
- log.error({ error }, "Scheduled-sync tick failed");
121
- await delay(CRASH_BACKOFF_MS);
122
- })
123
- .finally(() => {
124
- process.exit(1);
125
- });
@@ -1,265 +0,0 @@
1
- import type { RemitClient } from "@remit/backend/client";
2
- import type { AccountItem, MessageItem } from "@remit/data-ports";
3
- import { trashMailboxAt } from "@remit/data-ports/folder-role";
4
- import {
5
- deriveCopyMessageId,
6
- deriveMessageId,
7
- isValidMessageId,
8
- } from "@remit/data-ports/id";
9
- import { PATHLESS_MUTATION_EVENT_SCHEMA_VERSION } from "@remit/data-ports/mutation-events";
10
- import { MessageStatus, MessageSyncStatus } from "@remit/domain-enums";
11
- import type { Logger } from "@remit/logger";
12
- import { isCursorRebuildNeeded } from "@remit/mailbox-service";
13
- import type {
14
- MessageCopyEventV3,
15
- MessageDeleteEventV3,
16
- MessageMoveEventV3,
17
- PlacementMovePushEvent,
18
- } from "../events.js";
19
-
20
- /** Well past three attempts on a 60 s visibility timeout. */
21
- export const STRANDED_PLACEMENT_AFTER_MILLIS = 60 * 60 * 1000;
22
-
23
- type Unstamped<T> = Omit<T, "eventId" | "timestamp">;
24
-
25
- export type StrandedPlacementEvent =
26
- | Unstamped<PlacementMovePushEvent>
27
- | Unstamped<MessageDeleteEventV3>
28
- | Unstamped<MessageMoveEventV3>
29
- | Unstamped<MessageCopyEventV3>;
30
-
31
- export interface StrandedPlacementRedriveDeps {
32
- client: Pick<
33
- RemitClient,
34
- | "mailbox"
35
- | "mailboxSpecialUse"
36
- | "message"
37
- | "placementMove"
38
- | "threadMessage"
39
- >;
40
- emit: (event: StrandedPlacementEvent) => Promise<void>;
41
- log: Logger;
42
- }
43
-
44
- const isStrandedCopy = (row: MessageItem): boolean =>
45
- row.status === MessageStatus.moving &&
46
- row.uid === 0 &&
47
- typeof row.originalUid !== "number";
48
-
49
- type CopyRedrive =
50
- | { kind: "redrive"; event: Unstamped<MessageCopyEventV3> }
51
- | { kind: "unresolvable"; reason: string }
52
- | { kind: "wait" };
53
-
54
- /**
55
- * The message a uid-0 copy was made from: its id derives from the Message-ID
56
- * the copy's listing row carries, and the copy's own id derives from it.
57
- */
58
- const copyRedriveFor = async (
59
- client: StrandedPlacementRedriveDeps["client"],
60
- account: AccountItem,
61
- copy: MessageItem,
62
- ): Promise<CopyRedrive> => {
63
- const listing = await client.threadMessage.findByMessageId(
64
- account.accountConfigId,
65
- copy.messageId,
66
- );
67
- if (!listing) return { kind: "unresolvable", reason: "listing row missing" };
68
- const header = listing.messageIdHeader;
69
- if (!header || !isValidMessageId(header)) {
70
- return { kind: "unresolvable", reason: "listing row has no Message-ID" };
71
- }
72
- const sourceId = deriveMessageId(account.accountId, header);
73
- if (deriveCopyMessageId(sourceId, copy.mailboxId) !== copy.messageId) {
74
- return {
75
- kind: "unresolvable",
76
- reason: "copy was not made from the message its Message-ID names",
77
- };
78
- }
79
- const [source] = await client.message.get([sourceId]);
80
- if (!source) return { kind: "unresolvable", reason: "source row missing" };
81
- if (source.status === MessageStatus.deleted) {
82
- return { kind: "unresolvable", reason: "source row deleted" };
83
- }
84
- if (source.status !== MessageStatus.active) return { kind: "wait" };
85
- return {
86
- kind: "redrive",
87
- event: {
88
- type: "MESSAGE_COPY",
89
- schemaVersion: PATHLESS_MUTATION_EVENT_SCHEMA_VERSION,
90
- accountId: account.accountId,
91
- sourceMessageId: source.messageId,
92
- newMessageId: copy.messageId,
93
- sourceMailboxId: source.mailboxId,
94
- destinationMailboxId: copy.mailboxId,
95
- uid: source.uid,
96
- },
97
- };
98
- };
99
-
100
- /** A copy with no source to copy from has nothing to bind and nothing to hide. */
101
- const removeUnresolvableCopy = async (
102
- deps: StrandedPlacementRedriveDeps,
103
- account: AccountItem,
104
- row: MessageItem,
105
- reason: string,
106
- now: number,
107
- ): Promise<boolean> => {
108
- const removed = await deps.client.message.deleteIfPlacement(row.messageId, {
109
- status: MessageStatus.moving,
110
- mailboxId: row.mailboxId,
111
- uid: 0,
112
- });
113
- if (!removed) return false;
114
- deps.log.error(
115
- {
116
- alert: "stranded_copy_unresolvable",
117
- accountId: account.accountId,
118
- messageId: row.messageId,
119
- mailboxId: row.mailboxId,
120
- reason,
121
- untouchedForMs: now - row.updatedAt,
122
- },
123
- "A copy stranded at uid 0 has no source to copy from; the copy row was removed",
124
- );
125
- return true;
126
- };
127
-
128
- const eventFor = async (
129
- client: StrandedPlacementRedriveDeps["client"],
130
- account: AccountItem,
131
- row: MessageItem,
132
- trashMailboxId: string | undefined,
133
- ): Promise<StrandedPlacementEvent | undefined> => {
134
- const { accountId, accountConfigId } = account;
135
- const { messageId } = row;
136
-
137
- if (row.status === MessageStatus.deleting) {
138
- return {
139
- type: "MESSAGE_DELETE",
140
- schemaVersion: PATHLESS_MUTATION_EVENT_SCHEMA_VERSION,
141
- accountId,
142
- messageId,
143
- mailboxId: row.mailboxId,
144
- uid: row.uid,
145
- operation: "permanent_delete",
146
- };
147
- }
148
- if (
149
- typeof row.originalMailboxId !== "string" ||
150
- typeof row.originalUid !== "number"
151
- ) {
152
- return undefined;
153
- }
154
-
155
- const marker = await client.placementMove.find(messageId);
156
- if (marker && marker.destinationMailboxId === row.mailboxId) {
157
- return {
158
- type: "PLACEMENT_MOVE_PUSH",
159
- accountId,
160
- accountConfigId,
161
- messageId,
162
- };
163
- }
164
- if (row.mailboxId === trashMailboxId) {
165
- return {
166
- type: "MESSAGE_DELETE",
167
- schemaVersion: PATHLESS_MUTATION_EVENT_SCHEMA_VERSION,
168
- accountId,
169
- messageId,
170
- mailboxId: row.originalMailboxId,
171
- uid: row.originalUid,
172
- operation: "move_to_trash",
173
- destinationMailboxId: row.mailboxId,
174
- };
175
- }
176
- return {
177
- type: "MESSAGE_MOVE",
178
- schemaVersion: PATHLESS_MUTATION_EVENT_SCHEMA_VERSION,
179
- accountId,
180
- messageId,
181
- sourceMailboxId: row.originalMailboxId,
182
- destinationMailboxId: row.mailboxId,
183
- uid: row.originalUid,
184
- };
185
- };
186
-
187
- /** The claim restarts the row's clock; a folder awaiting a cursor rebuild waits until it is normal. */
188
- export const redriveStrandedPlacements = async (
189
- deps: StrandedPlacementRedriveDeps,
190
- account: AccountItem,
191
- now: number,
192
- ): Promise<number> => {
193
- const { client, emit, log } = deps;
194
- const { accountId } = account;
195
-
196
- const mailboxes = await client.mailbox.listAllByAccount(accountId);
197
- const paused = new Set(
198
- mailboxes
199
- .filter((mailbox) => isCursorRebuildNeeded(mailbox.cursorState))
200
- .map((mailbox) => mailbox.mailboxId),
201
- );
202
- const stranded = (
203
- await client.message.listStrandedPlacements(
204
- mailboxes.map((mailbox) => mailbox.mailboxId),
205
- now - STRANDED_PLACEMENT_AFTER_MILLIS,
206
- )
207
- ).filter(
208
- (row) =>
209
- !paused.has(row.mailboxId) &&
210
- !(row.originalMailboxId && paused.has(row.originalMailboxId)),
211
- );
212
- if (stranded.length === 0) return 0;
213
-
214
- const trash = trashMailboxAt(
215
- await client.mailboxSpecialUse.resolveTrashRole(accountId),
216
- "resolved",
217
- );
218
- const trashMailboxId = trash.allowed ? trash.mailbox.mailboxId : undefined;
219
-
220
- let redriven = 0;
221
- for (const row of stranded) {
222
- const copy = isStrandedCopy(row)
223
- ? await copyRedriveFor(client, account, row)
224
- : undefined;
225
- if (copy?.kind === "wait") continue;
226
- if (copy?.kind === "unresolvable") {
227
- if (await removeUnresolvableCopy(deps, account, row, copy.reason, now)) {
228
- redriven += 1;
229
- }
230
- continue;
231
- }
232
- const event =
233
- copy?.kind === "redrive"
234
- ? copy.event
235
- : await eventFor(client, account, row, trashMailboxId);
236
- if (!event) continue;
237
-
238
- // A copy's earlier COPY may have landed with its answer lost, so the
239
- // re-drive marks it `failed`: the copy handler then asks the destination
240
- // before it copies again.
241
- const claimed = await client.message.transitionPlacement(
242
- row.messageId,
243
- { status: row.status, mailboxId: row.mailboxId, uid: row.uid },
244
- {
245
- syncStatus: copy ? MessageSyncStatus.failed : MessageSyncStatus.pending,
246
- },
247
- );
248
- if (!claimed) continue;
249
-
250
- await emit(event);
251
- redriven += 1;
252
- log.warn(
253
- {
254
- metric: "stranded_placement_redriven",
255
- accountId,
256
- messageId: row.messageId,
257
- event: event.type,
258
- syncStatus: row.syncStatus,
259
- untouchedForMs: now - row.updatedAt,
260
- },
261
- "A move or delete had no event left to settle it; re-driven",
262
- );
263
- }
264
- return redriven;
265
- };