@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.
- package/dist/account-check.d.ts +27 -0
- package/dist/account-check.js +70 -0
- package/dist/body-sync-gate.d.ts +12 -0
- package/dist/body-sync-gate.js +53 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +193 -0
- package/dist/config-env.d.ts +5 -0
- package/dist/config-env.js +37 -0
- package/dist/connection-pool.d.ts +22 -0
- package/dist/connection-pool.js +97 -0
- package/dist/connection-scope.d.ts +8 -0
- package/dist/connection-scope.js +10 -0
- package/dist/context.d.ts +46 -0
- package/dist/context.js +15 -0
- package/dist/e2e-processor-shim.d.ts +2 -0
- package/dist/e2e-processor-shim.js +181 -0
- package/dist/emit.d.ts +15 -0
- package/dist/emit.js +76 -0
- package/dist/events.d.ts +203 -0
- package/dist/events.js +1 -0
- package/dist/failure-kind.d.ts +10 -0
- package/{src/failure-kind.ts → dist/failure-kind.js} +6 -6
- package/dist/handlers/append-sent-message.d.ts +3 -0
- package/dist/handlers/append-sent-message.js +152 -0
- package/dist/handlers/delete-account-objects.d.ts +3 -0
- package/dist/handlers/delete-account-objects.js +35 -0
- package/dist/handlers/empty-trash.d.ts +14 -0
- package/dist/handlers/empty-trash.js +217 -0
- package/dist/handlers/flag-push.d.ts +38 -0
- package/dist/handlers/flag-push.js +195 -0
- package/dist/handlers/imported-folder.d.ts +4 -0
- package/dist/handlers/imported-folder.js +23 -0
- package/dist/handlers/mailbox-management.d.ts +6 -0
- package/dist/handlers/mailbox-management.js +294 -0
- package/dist/handlers/mailbox-sync-order.d.ts +21 -0
- package/dist/handlers/mailbox-sync-order.js +50 -0
- package/dist/handlers/message-copy.d.ts +40 -0
- package/dist/handlers/message-copy.js +371 -0
- package/dist/handlers/message-delete-terminal.d.ts +60 -0
- package/dist/handlers/message-delete-terminal.js +77 -0
- package/dist/handlers/message-delete.d.ts +57 -0
- package/dist/handlers/message-delete.js +520 -0
- package/{src/handlers/message-move-terminal.ts → dist/handlers/message-move-terminal.d.ts} +18 -105
- package/dist/handlers/message-move-terminal.js +94 -0
- package/dist/handlers/message-move.d.ts +146 -0
- package/dist/handlers/message-move.js +490 -0
- package/dist/handlers/placement-move-push.d.ts +62 -0
- package/dist/handlers/placement-move-push.js +307 -0
- package/dist/handlers/probe-message-id.d.ts +2 -0
- package/dist/handlers/probe-message-id.js +6 -0
- package/dist/handlers/sync-mailboxes.d.ts +45 -0
- package/dist/handlers/sync-mailboxes.js +233 -0
- package/dist/handlers/sync-message-body.d.ts +23 -0
- package/dist/handlers/sync-message-body.js +182 -0
- package/dist/handlers/sync-messages.d.ts +29 -0
- package/dist/handlers/sync-messages.js +285 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +67 -0
- package/dist/lambda-context.d.ts +8 -0
- package/dist/lambda-context.js +33 -0
- package/dist/mailbox-row.d.ts +9 -0
- package/dist/mailbox-row.js +6 -0
- package/dist/poller.d.ts +1 -0
- package/dist/poller.js +51 -0
- package/dist/process.d.ts +6 -0
- package/dist/process.js +3 -0
- package/dist/processor.d.ts +3 -0
- package/dist/processor.js +44 -0
- package/dist/queues.d.ts +10 -0
- package/dist/queues.js +1 -0
- package/dist/s3-account-objects.d.ts +3 -0
- package/dist/s3-account-objects.js +26 -0
- package/dist/scheduler/attachment-sweep.d.ts +11 -0
- package/dist/scheduler/attachment-sweep.js +23 -0
- package/dist/scheduler/calendar-subscriptions.d.ts +26 -0
- package/dist/scheduler/calendar-subscriptions.js +35 -0
- package/dist/scheduler/config.d.ts +27 -0
- package/{src/scheduler/config.ts → dist/scheduler/config.js} +7 -26
- package/{src/scheduler/decide-due.ts → dist/scheduler/decide-due.d.ts} +3 -12
- package/dist/scheduler/decide-due.js +17 -0
- package/dist/scheduler/handler.d.ts +21 -0
- package/{src/scheduler/handler.ts → dist/scheduler/handler.js} +24 -36
- package/{src/scheduler/loop.ts → dist/scheduler/loop.d.ts} +7 -23
- package/dist/scheduler/loop.js +8 -0
- package/dist/scheduler/run-tick.d.ts +62 -0
- package/dist/scheduler/run-tick.js +164 -0
- package/dist/scheduler/runner.d.ts +2 -0
- package/{src/scheduler/runner.ts → dist/scheduler/runner.js} +58 -84
- package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
- package/dist/scheduler/stranded-placement-redrive.js +172 -0
- package/dist/sqs-queues.d.ts +4 -0
- package/dist/sqs-queues.js +22 -0
- package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
- package/dist/with-oauth-lifecycle-deps.js +17 -0
- package/dist/with-oauth-lifecycle.d.ts +56 -0
- package/dist/with-oauth-lifecycle.js +92 -0
- package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
- package/dist/worker-version.js +12 -0
- package/package.json +75 -69
- package/src/account-check.test.ts +0 -149
- package/src/account-check.ts +0 -101
- package/src/body-sync-gate.test.ts +0 -185
- package/src/body-sync-gate.ts +0 -86
- package/src/cli.ts +0 -224
- package/src/connection-scope.test.ts +0 -266
- package/src/connection-scope.ts +0 -314
- package/src/credential-less-sync-fence.test.ts +0 -152
- package/src/e2e-processor-shim.ts +0 -232
- package/src/emit.test.ts +0 -127
- package/src/emit.ts +0 -117
- package/src/events.ts +0 -252
- package/src/failure-kind.test.ts +0 -42
- package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
- package/src/handlers/append-sent-message.ts +0 -264
- package/src/handlers/delete-account-objects.test.ts +0 -128
- package/src/handlers/delete-account-objects.ts +0 -94
- package/src/handlers/empty-trash.sqlite.test.ts +0 -209
- package/src/handlers/empty-trash.ts +0 -352
- package/src/handlers/flag-push.ts +0 -285
- package/src/handlers/imported-folder.test.ts +0 -63
- package/src/handlers/imported-folder.ts +0 -38
- package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
- package/src/handlers/mailbox-management.ts +0 -486
- package/src/handlers/mailbox-sync-order.test.ts +0 -93
- package/src/handlers/mailbox-sync-order.ts +0 -65
- package/src/handlers/message-copy.sqlite.test.ts +0 -298
- package/src/handlers/message-copy.ts +0 -566
- package/src/handlers/message-delete-terminal.ts +0 -147
- package/src/handlers/message-delete.sqlite.test.ts +0 -291
- package/src/handlers/message-delete.ts +0 -885
- package/src/handlers/message-move.sqlite.test.ts +0 -271
- package/src/handlers/message-move.test.ts +0 -216
- package/src/handlers/message-move.ts +0 -752
- package/src/handlers/placement-move-push.test.ts +0 -137
- package/src/handlers/placement-move-push.ts +0 -475
- package/src/handlers/placement-push.sqlite.test.ts +0 -298
- package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
- package/src/handlers/probe-message-id.ts +0 -15
- package/src/handlers/sync-mailboxes.test.ts +0 -183
- package/src/handlers/sync-mailboxes.ts +0 -421
- package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
- package/src/handlers/sync-message-body.test.ts +0 -150
- package/src/handlers/sync-message-body.ts +0 -365
- package/src/handlers/sync-messages.sqlite.test.ts +0 -183
- package/src/handlers/sync-messages.ts +0 -525
- package/src/index.test.ts +0 -22
- package/src/index.ts +0 -86
- package/src/mail-sync-selection.sqlite.test.ts +0 -234
- package/src/mailbox-row.ts +0 -20
- package/src/pending-sync.sqlite.test.ts +0 -166
- package/src/poller.ts +0 -55
- package/src/processor.ts +0 -60
- package/src/queues.ts +0 -49
- package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -75
- package/src/scheduler/attachment-sweep.ts +0 -46
- package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
- package/src/scheduler/calendar-subscriptions.ts +0 -103
- package/src/scheduler/config.test.ts +0 -40
- package/src/scheduler/decide-due.test.ts +0 -44
- package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
- package/src/scheduler/loop.test.ts +0 -143
- package/src/scheduler/run-tick.sqlite.test.ts +0 -191
- package/src/scheduler/run-tick.ts +0 -281
- package/src/scheduler/stranded-placement-redrive.ts +0 -265
- package/src/stranded-placement.sqlite.test.ts +0 -794
- package/src/test-helpers/test-world.ts +0 -427
- package/src/thread-join.sqlite.test.ts +0 -106
- package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
- package/src/with-oauth-lifecycle-deps.ts +0 -71
- package/src/with-oauth-lifecycle.ts +0 -158
- 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
|
-
});
|