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