@remit/backend 0.0.143 → 0.0.144
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/dev-server/content-handler.d.ts +38 -0
- package/dist/dev-server/content-handler.js +46 -0
- package/dist/src/api.d.ts +7 -0
- package/dist/src/api.js +144 -0
- package/dist/src/auth.d.ts +30 -0
- package/dist/src/auth.js +73 -0
- package/dist/src/config/msoauth.d.ts +30 -0
- package/dist/src/config/msoauth.js +30 -0
- package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
- package/dist/src/data-backend.js +12 -0
- package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
- package/dist/src/derive/autoMoved.js +49 -0
- package/dist/src/derive/contentSignature.d.ts +41 -0
- package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
- package/dist/src/derive/contentUrl.d.ts +46 -0
- package/dist/src/derive/contentUrl.js +46 -0
- package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
- package/dist/src/derive/deriveMuted.js +7 -0
- package/dist/src/derive/enrichThreadRows.d.ts +118 -0
- package/dist/src/derive/enrichThreadRows.js +151 -0
- package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
- package/dist/src/derive/filterThreadCriteria.js +24 -0
- package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
- package/dist/src/derive/pendingMoveCounts.js +53 -0
- package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
- package/dist/src/derive/senderTrust.js +19 -0
- package/dist/src/error.d.ts +21 -0
- package/dist/src/error.js +85 -0
- package/dist/src/handlers/account-guards.d.ts +36 -0
- package/dist/src/handlers/account-guards.js +107 -0
- package/dist/src/handlers/account-oauth.d.ts +14 -0
- package/dist/src/handlers/account-oauth.js +387 -0
- package/dist/src/handlers/account-overrides.d.ts +53 -0
- package/dist/src/handlers/account-overrides.js +233 -0
- package/dist/src/handlers/account-signature.d.ts +28 -0
- package/dist/src/handlers/account-signature.js +85 -0
- package/dist/src/handlers/account.d.ts +6 -0
- package/dist/src/handlers/account.js +350 -0
- package/dist/src/handlers/address.d.ts +35 -0
- package/dist/src/handlers/address.js +141 -0
- package/dist/src/handlers/calendar-event.d.ts +70 -0
- package/dist/src/handlers/calendar-event.js +425 -0
- package/dist/src/handlers/calendar-feed.d.ts +47 -0
- package/dist/src/handlers/calendar-feed.js +192 -0
- package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
- package/dist/src/handlers/calendar-suggestion.js +212 -0
- package/dist/src/handlers/calendar.d.ts +116 -0
- package/dist/src/handlers/calendar.js +330 -0
- package/dist/src/handlers/config.d.ts +43 -0
- package/dist/src/handlers/config.js +176 -0
- package/dist/src/handlers/ensure-account-config.d.ts +9 -0
- package/dist/src/handlers/ensure-account-config.js +27 -0
- package/dist/src/handlers/filter.d.ts +3 -0
- package/dist/src/handlers/filter.js +103 -0
- package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
- package/dist/src/handlers/folder-role-appointments.js +168 -0
- package/dist/src/handlers/folder-role-labels.d.ts +26 -0
- package/dist/src/handlers/folder-role-labels.js +40 -0
- package/dist/src/handlers/folder-role.d.ts +9 -0
- package/dist/src/handlers/folder-role.js +40 -0
- package/dist/src/handlers/index.d.ts +2 -0
- package/dist/src/handlers/index.js +60 -0
- package/dist/src/handlers/label.d.ts +43 -0
- package/dist/src/handlers/label.js +114 -0
- package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
- package/dist/src/handlers/mailbox-bindings.js +61 -0
- package/dist/src/handlers/mailbox.d.ts +23 -0
- package/dist/src/handlers/mailbox.js +219 -0
- package/dist/src/handlers/me.d.ts +2 -0
- package/dist/src/handlers/me.js +138 -0
- package/dist/src/handlers/message.d.ts +84 -0
- package/dist/src/handlers/message.js +607 -0
- package/dist/src/handlers/organize.d.ts +10 -0
- package/dist/src/handlers/organize.js +112 -0
- package/dist/src/handlers/outbox-attachment.d.ts +23 -0
- package/dist/src/handlers/outbox-attachment.js +81 -0
- package/dist/src/handlers/outbox.d.ts +4 -0
- package/dist/src/handlers/outbox.js +144 -0
- package/dist/src/handlers/sync-progress.d.ts +46 -0
- package/dist/src/handlers/sync-progress.js +63 -0
- package/dist/src/handlers/sync.d.ts +28 -0
- package/dist/src/handlers/sync.js +82 -0
- package/dist/src/handlers/system-update.d.ts +2 -0
- package/dist/src/handlers/system-update.js +172 -0
- package/dist/src/handlers/thread.d.ts +148 -0
- package/dist/src/handlers/thread.js +233 -0
- package/dist/src/handlers/unified-threads.d.ts +247 -0
- package/dist/src/handlers/unified-threads.js +409 -0
- package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
- package/dist/src/handlers/vip-suggestions.js +16 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +37 -0
- package/{src/json.ts → dist/src/json.d.ts} +1 -4
- package/dist/src/json.js +10 -0
- package/dist/src/jwt-auth.d.ts +18 -0
- package/dist/src/jwt-auth.js +97 -0
- package/dist/src/request-context.d.ts +9 -0
- package/dist/src/request-context.js +22 -0
- package/dist/src/request.d.ts +3 -0
- package/dist/src/request.js +7 -0
- package/dist/src/response.d.ts +27 -0
- package/dist/src/response.js +87 -0
- package/dist/src/service/account-deletion.d.ts +22 -0
- package/dist/src/service/account-deletion.js +39 -0
- package/dist/src/service/compose-sqlite.d.ts +2 -0
- package/dist/src/service/compose-sqlite.js +11 -0
- package/dist/src/service/create-remit-client.d.ts +95 -0
- package/dist/src/service/create-remit-client.js +207 -0
- package/dist/src/service/data-client.d.ts +12 -0
- package/dist/src/service/data-client.js +44 -0
- package/dist/src/service/export-identity.d.ts +3 -0
- package/dist/src/service/export-identity.js +35 -0
- package/dist/src/service/fire-and-forget.d.ts +54 -0
- package/dist/src/service/fire-and-forget.js +38 -0
- package/dist/src/service/organize.d.ts +184 -0
- package/dist/src/service/organize.js +384 -0
- package/dist/src/service/refusal.d.ts +4 -0
- package/dist/src/service/refusal.js +20 -0
- package/dist/src/service/sender-category.d.ts +51 -0
- package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
- package/dist/src/service/sqs.d.ts +1 -0
- package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
- package/dist/src/service/trigger-sync.d.ts +51 -0
- package/dist/src/service/trigger-sync.js +39 -0
- package/dist/src/types.d.ts +49 -0
- package/dist/src/types.js +1 -0
- package/package.json +113 -98
- package/.env.test +0 -4
- package/dev-server/content-auth.test.ts +0 -113
- package/dev-server/content-auth.ts +0 -54
- package/dev-server/content-handler.test.ts +0 -105
- package/dev-server/content-handler.ts +0 -79
- package/dev-server/content-path.test.ts +0 -37
- package/dev-server/content-path.ts +0 -27
- package/dev-server/cors.test.ts +0 -48
- package/dev-server/cors.ts +0 -25
- package/dev-server/keep-alive.test.ts +0 -24
- package/dev-server/lambda-helpers.ts +0 -69
- package/dev-server/log-output.test.ts +0 -173
- package/dev-server/oauth-callback-route.test.ts +0 -66
- package/dev-server/relational-health.test.ts +0 -83
- package/dev-server/relational-health.ts +0 -39
- package/dev-server/server.ts +0 -384
- package/dev-server/sync-age.test.ts +0 -91
- package/dev-server/sync-age.ts +0 -55
- package/dev-server/upload-handler.ts +0 -132
- package/dev-server/upload-route.test.ts +0 -196
- package/scripts/backfill-classification.ts +0 -91
- package/scripts/backfill-list-id.ts +0 -83
- package/scripts/config-save.ts +0 -128
- package/src/auth.test.ts +0 -12
- package/src/auth.ts +0 -92
- package/src/config/msoauth.test.ts +0 -48
- package/src/config/msoauth.ts +0 -48
- package/src/data-backend.test.ts +0 -23
- package/src/derive/autoMoved.test.ts +0 -104
- package/src/derive/contentSignature.test.ts +0 -161
- package/src/derive/contentUrl.test.ts +0 -172
- package/src/derive/contentUrl.ts +0 -86
- package/src/derive/deriveMuted.test.ts +0 -34
- package/src/derive/enrichThreadRows.test.ts +0 -360
- package/src/derive/enrichThreadRows.ts +0 -234
- package/src/derive/filterThreadCriteria.test.ts +0 -126
- package/src/derive/filterThreadCriteria.ts +0 -51
- package/src/derive/senderTrust.test.ts +0 -67
- package/src/error.test.ts +0 -201
- package/src/error.ts +0 -123
- package/src/handlers/account-guards.test.ts +0 -67
- package/src/handlers/account-guards.ts +0 -169
- package/src/handlers/account-oauth-callback.test.ts +0 -155
- package/src/handlers/account-oauth-consent.test.ts +0 -336
- package/src/handlers/account-oauth-start.test.ts +0 -191
- package/src/handlers/account-oauth.test.ts +0 -439
- package/src/handlers/account-oauth.ts +0 -635
- package/src/handlers/account-overrides.test.ts +0 -88
- package/src/handlers/account-overrides.ts +0 -323
- package/src/handlers/account-ownership.ts +0 -25
- package/src/handlers/account-signature.ts +0 -117
- package/src/handlers/account-synced-services.test.ts +0 -245
- package/src/handlers/account.ts +0 -552
- package/src/handlers/address.test.ts +0 -381
- package/src/handlers/address.ts +0 -200
- package/src/handlers/calendar-event.test.ts +0 -685
- package/src/handlers/calendar-event.ts +0 -738
- package/src/handlers/calendar-feed-validators.test.ts +0 -228
- package/src/handlers/calendar-feed.test.ts +0 -439
- package/src/handlers/calendar-feed.ts +0 -291
- package/src/handlers/calendar-sqlite-fixture.ts +0 -36
- package/src/handlers/calendar-suggestion.test.ts +0 -1269
- package/src/handlers/calendar-suggestion.ts +0 -425
- package/src/handlers/calendar.test.ts +0 -1080
- package/src/handlers/calendar.ts +0 -517
- package/src/handlers/config.test.ts +0 -245
- package/src/handlers/config.ts +0 -332
- package/src/handlers/ensure-account-config.ts +0 -30
- package/src/handlers/filter.test.ts +0 -644
- package/src/handlers/filter.ts +0 -518
- package/src/handlers/folder-role-appointments.test.ts +0 -438
- package/src/handlers/folder-role-appointments.ts +0 -249
- package/src/handlers/folder-role-labels.ts +0 -63
- package/src/handlers/folder-role.test.ts +0 -209
- package/src/handlers/folder-role.ts +0 -80
- package/src/handlers/index.ts +0 -83
- package/src/handlers/label.test.ts +0 -112
- package/src/handlers/label.ts +0 -210
- package/src/handlers/mailbox-bindings.ts +0 -111
- package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
- package/src/handlers/mailbox-settled-target.test.ts +0 -251
- package/src/handlers/mailbox.test.ts +0 -280
- package/src/handlers/mailbox.ts +0 -581
- package/src/handlers/me.test.ts +0 -81
- package/src/handlers/me.ts +0 -203
- package/src/handlers/message-read-backfill.test.ts +0 -103
- package/src/handlers/message.test.ts +0 -131
- package/src/handlers/message.ts +0 -1131
- package/src/handlers/organize.test.ts +0 -277
- package/src/handlers/organize.ts +0 -197
- package/src/handlers/outbox-attachment.test.ts +0 -765
- package/src/handlers/outbox-attachment.ts +0 -126
- package/src/handlers/outbox-list.test.ts +0 -179
- package/src/handlers/outbox.test.ts +0 -772
- package/src/handlers/outbox.ts +0 -258
- package/src/handlers/search.test.ts +0 -161
- package/src/handlers/search.ts +0 -105
- package/src/handlers/sync-progress.test.ts +0 -205
- package/src/handlers/sync-progress.ts +0 -99
- package/src/handlers/sync.test.ts +0 -168
- package/src/handlers/sync.ts +0 -118
- package/src/handlers/system-update.test.ts +0 -497
- package/src/handlers/system-update.ts +0 -230
- package/src/handlers/thread.test.ts +0 -396
- package/src/handlers/thread.ts +0 -427
- package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
- package/src/handlers/unified-threads.test.ts +0 -785
- package/src/handlers/unified-threads.ts +0 -669
- package/src/index.test.ts +0 -83
- package/src/index.ts +0 -228
- package/src/jwt-auth.test.ts +0 -152
- package/src/jwt-auth.ts +0 -132
- package/src/log-redaction.test.ts +0 -144
- package/src/openapi-spec.test.ts +0 -118
- package/src/request-context.test.ts +0 -61
- package/src/request-context.ts +0 -33
- package/src/request-pipeline.test.ts +0 -88
- package/src/request.ts +0 -10
- package/src/response.test.ts +0 -130
- package/src/response.ts +0 -144
- package/src/service/compose-sqlite.ts +0 -98
- package/src/service/config-import.ts +0 -39
- package/src/service/create-remit-client.ts +0 -522
- package/src/service/data-client.test.ts +0 -97
- package/src/service/data-client.ts +0 -58
- package/src/service/export-identity.ts +0 -39
- package/src/service/filter.ts +0 -40
- package/src/service/fire-and-forget.test.ts +0 -126
- package/src/service/fire-and-forget.ts +0 -79
- package/src/service/localhost-env-config.test.ts +0 -36
- package/src/service/organize-queue.ts +0 -98
- package/src/service/organize.test.ts +0 -1451
- package/src/service/organize.ts +0 -1025
- package/src/service/read-path-filter-config.test.ts +0 -243
- package/src/service/semantic-capability.test.ts +0 -78
- package/src/service/semantic-capability.ts +0 -95
- package/src/service/sender-category.test.ts +0 -235
- package/src/service/trigger-sync.test.ts +0 -205
- package/src/service/trigger-sync.ts +0 -100
- package/src/stdout-capture-fixture.ts +0 -40
- package/src/types.ts +0 -271
- package/tsconfig.json +0 -7
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { logger } from "@remit/logger";
|
|
2
|
+
import { BodySyncQueueService, BodySyncService, createConnection, FlagPushService, FlagQueueService, MailboxQueueService, MessageMoveService, OutboxAttachmentService, OutboxQueueService, PlacementMoveService, SpamReportService, SyncWarningService, } from "@remit/mailbox-service";
|
|
3
|
+
import { buildFilterConfig } from "@remit/mailbox-service/filter-config";
|
|
4
|
+
import { createCachedDataKeyProvider, createKmsDataKeyProvider, createSecretsService, deserializeEncryptedPayload, FAKE_KMS_KEY_ID, } from "@remit/secrets-service";
|
|
5
|
+
import { createStorageService } from "@remit/storage-service/s3";
|
|
6
|
+
import { env } from "expect-env";
|
|
7
|
+
const buildConnectionScope = (accountService, secretsService) => async (accountId) => {
|
|
8
|
+
const account = await accountService.get(accountId);
|
|
9
|
+
if (!account.passwordHash) {
|
|
10
|
+
throw new Error(`Account ${accountId} has no passwordHash — OAuth accounts cannot use this connection path`);
|
|
11
|
+
}
|
|
12
|
+
const password = await secretsService.decrypt(deserializeEncryptedPayload(JSON.parse(account.passwordHash)));
|
|
13
|
+
let connection = null;
|
|
14
|
+
let connectPromise = null;
|
|
15
|
+
const getConnection = async () => {
|
|
16
|
+
if (connectPromise) {
|
|
17
|
+
return connectPromise;
|
|
18
|
+
}
|
|
19
|
+
const conn = createConnection({
|
|
20
|
+
user: account.username,
|
|
21
|
+
credentials: { kind: "password", password },
|
|
22
|
+
host: account.imapHost,
|
|
23
|
+
port: account.imapPort,
|
|
24
|
+
tls: account.imapTls,
|
|
25
|
+
});
|
|
26
|
+
connection = conn;
|
|
27
|
+
connectPromise = conn.connect().then(() => conn);
|
|
28
|
+
return connectPromise;
|
|
29
|
+
};
|
|
30
|
+
const disconnect = async () => {
|
|
31
|
+
if (connection) {
|
|
32
|
+
await connection.disconnect();
|
|
33
|
+
connection = null;
|
|
34
|
+
connectPromise = null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
return { getConnection, disconnect };
|
|
38
|
+
};
|
|
39
|
+
// The body queue lives on the worker infra; the API only knows it locally
|
|
40
|
+
// (dev / e2e set SQS_QUEUE_URL_BODY). When unset the read path still returns a
|
|
41
|
+
// retryable 202 — it just cannot re-arm the cue itself.
|
|
42
|
+
const buildBodySyncQueue = () => {
|
|
43
|
+
const bodyQueueUrl = process.env.SQS_QUEUE_URL_BODY;
|
|
44
|
+
if (!bodyQueueUrl)
|
|
45
|
+
return undefined;
|
|
46
|
+
return new BodySyncQueueService({ sqsQueueUrl: bodyQueueUrl, logger });
|
|
47
|
+
};
|
|
48
|
+
// Everything a RemitClient needs that is chosen from the environment and does
|
|
49
|
+
// not vary by data backend: storage, secrets, the SQS queue URLs, and
|
|
50
|
+
// the optional body-sync queue. Each composition root builds its backend's
|
|
51
|
+
// repositories and merges them with this.
|
|
52
|
+
export const buildSharedDeps = () => {
|
|
53
|
+
const storage = createStorageService();
|
|
54
|
+
const kmsKeyId = process.env.KMS_KEY_ID ?? FAKE_KMS_KEY_ID;
|
|
55
|
+
const dataKeyProvider = createCachedDataKeyProvider(createKmsDataKeyProvider(kmsKeyId));
|
|
56
|
+
const secrets = createSecretsService(dataKeyProvider);
|
|
57
|
+
const sqsQueueUrl = env.SQS_QUEUE_URL;
|
|
58
|
+
const sqsSmtpQueueUrl = process.env.SQS_QUEUE_URL_SMTP ?? sqsQueueUrl;
|
|
59
|
+
return {
|
|
60
|
+
storage,
|
|
61
|
+
secrets,
|
|
62
|
+
sqsQueueUrl,
|
|
63
|
+
sqsSmtpQueueUrl,
|
|
64
|
+
bodySyncQueue: buildBodySyncQueue(),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
// The read-path body backfill (describeMessage / getRawMessage →
|
|
68
|
+
// fetchAndGetBody) materializes a body that was never sync-synced, and must run
|
|
69
|
+
// the account's standing filters the same way the imap-worker's sync path does.
|
|
70
|
+
// Without this, a message a user opens before background body-sync is classified
|
|
71
|
+
// but never filter-evaluated — and once its body is stored the filter-capable
|
|
72
|
+
// sync path skips it, so an explicit standing filter silently never fires (issue
|
|
73
|
+
// #223). Only the explicit user-rule half is wired here; heuristic classifier
|
|
74
|
+
// placement stays on the sync path. It shares `buildFilterConfig` with
|
|
75
|
+
// `sync-message-body.ts`, so a filter fires on either (issue #298). Over the
|
|
76
|
+
// message-management queue's
|
|
77
|
+
// local-first PlacementMoveService; filters stay off when that queue is unset.
|
|
78
|
+
export const buildReadPathFilterConfig = (repositories) => {
|
|
79
|
+
const placementMoveQueueUrl = process.env.SQS_QUEUE_URL_MESSAGE_MGMT;
|
|
80
|
+
return buildFilterConfig({
|
|
81
|
+
filterService: repositories.filter,
|
|
82
|
+
messageLabelService: repositories.messageLabel,
|
|
83
|
+
mailboxService: repositories.mailbox,
|
|
84
|
+
placementMoveService: placementMoveQueueUrl
|
|
85
|
+
? new PlacementMoveService({
|
|
86
|
+
messageService: repositories.message,
|
|
87
|
+
threadMessageService: repositories.threadMessage,
|
|
88
|
+
markerService: repositories.placementMove,
|
|
89
|
+
addressService: repositories.address,
|
|
90
|
+
mailboxSpecialUseService: repositories.mailboxSpecialUse,
|
|
91
|
+
mailboxService: repositories.mailbox,
|
|
92
|
+
sqsQueueUrl: placementMoveQueueUrl,
|
|
93
|
+
})
|
|
94
|
+
: undefined,
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
// Backend-neutral composition root: given repositories (from any data-ports
|
|
98
|
+
// implementation) and the shared services, wire the domain and queue services
|
|
99
|
+
// and assemble a RemitClient. Imports neither ElectroDB nor Drizzle — the
|
|
100
|
+
// caller chooses the backend.
|
|
101
|
+
export const createRemitClient = (deps) => {
|
|
102
|
+
const { repositories, storage, secrets, sqsQueueUrl, sqsSmtpQueueUrl, bodySyncQueue, } = deps;
|
|
103
|
+
const filterConfig = buildReadPathFilterConfig(repositories);
|
|
104
|
+
const flagPushService = new FlagPushService({
|
|
105
|
+
markerService: repositories.flagPush,
|
|
106
|
+
sqsQueueUrl,
|
|
107
|
+
logger,
|
|
108
|
+
});
|
|
109
|
+
// Shared with the returned `flagQueue` below (issue #302): body sync
|
|
110
|
+
// auto-marks-read a message from an unsubscribed sender through the exact
|
|
111
|
+
// same `FlagQueueService.markAsRead` a manual mark-as-read goes through, not
|
|
112
|
+
// a second instance or a second primitive.
|
|
113
|
+
const flagQueueService = new FlagQueueService({
|
|
114
|
+
messageFlagService: repositories.messageFlag,
|
|
115
|
+
messageService: repositories.message,
|
|
116
|
+
threadMessageService: repositories.threadMessage,
|
|
117
|
+
flagPushService,
|
|
118
|
+
logger,
|
|
119
|
+
});
|
|
120
|
+
const messageMoveService = new MessageMoveService({
|
|
121
|
+
messageService: repositories.message,
|
|
122
|
+
mailboxService: repositories.mailbox,
|
|
123
|
+
mailboxSpecialUseService: repositories.mailboxSpecialUse,
|
|
124
|
+
threadMessageService: repositories.threadMessage,
|
|
125
|
+
addressService: repositories.address,
|
|
126
|
+
sqsQueueUrl,
|
|
127
|
+
logger,
|
|
128
|
+
});
|
|
129
|
+
// One instance, shared with the queue service below: the per-draft chain that
|
|
130
|
+
// makes the attachment cap safe under concurrency lives on it, and a discard
|
|
131
|
+
// racing an upload has to take the same lock the upload does.
|
|
132
|
+
const outboxAttachmentService = new OutboxAttachmentService({
|
|
133
|
+
outboxMessageService: repositories.outboxMessage,
|
|
134
|
+
outboxAttachmentService: repositories.outboxAttachment,
|
|
135
|
+
storage,
|
|
136
|
+
});
|
|
137
|
+
const bodySync = new BodySyncService(repositories.message, storage, repositories.threadMessage, repositories.address, repositories.envelope, new SyncWarningService(repositories.syncWarning), logger, undefined, filterConfig, undefined, { flagQueueService },
|
|
138
|
+
// The read-path backfill materializes a body the sync path never got to,
|
|
139
|
+
// so it is a message's first sight as much as the sync pass is — and an
|
|
140
|
+
// invitation the user opens before background sync must still get its
|
|
141
|
+
// card (issue #1033, the same argument as the filter wiring above).
|
|
142
|
+
{
|
|
143
|
+
calendarSuggestionService: repositories.calendarSuggestion,
|
|
144
|
+
calendarStore: repositories.calendarStore,
|
|
145
|
+
filterService: repositories.filter,
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
accountConfig: repositories.accountConfig,
|
|
149
|
+
account: repositories.account,
|
|
150
|
+
accountSetting: repositories.accountSetting,
|
|
151
|
+
address: repositories.address,
|
|
152
|
+
mailbox: repositories.mailbox,
|
|
153
|
+
mailboxSpecialUse: repositories.mailboxSpecialUse,
|
|
154
|
+
mailboxLock: repositories.mailboxLock,
|
|
155
|
+
message: repositories.message,
|
|
156
|
+
messageFlag: repositories.messageFlag,
|
|
157
|
+
outboxMessage: repositories.outboxMessage,
|
|
158
|
+
threadMessage: repositories.threadMessage,
|
|
159
|
+
envelope: repositories.envelope,
|
|
160
|
+
accountExportRequest: repositories.accountExportRequest,
|
|
161
|
+
configImport: repositories.configImport,
|
|
162
|
+
quarantine: repositories.quarantine,
|
|
163
|
+
syncWarning: repositories.syncWarning,
|
|
164
|
+
organizeJobRequest: repositories.organizeJobRequest,
|
|
165
|
+
filter: repositories.filter,
|
|
166
|
+
label: repositories.label,
|
|
167
|
+
messageLabel: repositories.messageLabel,
|
|
168
|
+
senderSignerStanding: repositories.senderSignerStanding,
|
|
169
|
+
calendarCollection: repositories.calendarCollection,
|
|
170
|
+
calendarObject: repositories.calendarObject,
|
|
171
|
+
calendarEventIndex: repositories.calendarEventIndex,
|
|
172
|
+
calendarFeedToken: repositories.calendarFeedToken,
|
|
173
|
+
calendarSuggestion: repositories.calendarSuggestion,
|
|
174
|
+
calendarStore: repositories.calendarStore,
|
|
175
|
+
storage,
|
|
176
|
+
secrets,
|
|
177
|
+
bodySync,
|
|
178
|
+
bodySyncQueue,
|
|
179
|
+
placementMove: repositories.placementMove,
|
|
180
|
+
flagPush: repositories.flagPush,
|
|
181
|
+
flagQueue: flagQueueService,
|
|
182
|
+
mailboxQueue: new MailboxQueueService({
|
|
183
|
+
mailboxService: repositories.mailbox,
|
|
184
|
+
sqsQueueUrl,
|
|
185
|
+
logger,
|
|
186
|
+
}),
|
|
187
|
+
messageMove: messageMoveService,
|
|
188
|
+
outboxAttachment: outboxAttachmentService,
|
|
189
|
+
outboxQueue: new OutboxQueueService({
|
|
190
|
+
outboxMessageService: repositories.outboxMessage,
|
|
191
|
+
outboxAttachmentService,
|
|
192
|
+
accountService: repositories.account,
|
|
193
|
+
sqsSmtpQueueUrl,
|
|
194
|
+
logger,
|
|
195
|
+
}),
|
|
196
|
+
spamReport: new SpamReportService({
|
|
197
|
+
messageService: repositories.message,
|
|
198
|
+
addressService: repositories.address,
|
|
199
|
+
accountService: repositories.account,
|
|
200
|
+
mailboxSpecialUseService: repositories.mailboxSpecialUse,
|
|
201
|
+
messageMoveService,
|
|
202
|
+
flagPushService,
|
|
203
|
+
logger,
|
|
204
|
+
}),
|
|
205
|
+
createConnectionScope: buildConnectionScope(repositories.account, secrets),
|
|
206
|
+
};
|
|
207
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RemitClient } from "./create-remit-client.js";
|
|
2
|
+
export type { ConnectionScope, RemitClient, } from "./create-remit-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Register the client from the composition root. An adapter that lives outside
|
|
5
|
+
* this shared, open-core module — DynamoDB, or a Postgres adapter in the
|
|
6
|
+
* repository that deploys it — is never imported here; its composition root
|
|
7
|
+
* calls this before handling a request.
|
|
8
|
+
*/
|
|
9
|
+
export declare const setClient: (client: RemitClient) => void;
|
|
10
|
+
export declare const getClient: () => Promise<RemitClient>;
|
|
11
|
+
/** Reset the singleton and any injected client — test use only. */
|
|
12
|
+
export declare const _resetForTest: () => void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
let clientPromise = null;
|
|
2
|
+
let injected = null;
|
|
3
|
+
/**
|
|
4
|
+
* Register the client from the composition root. An adapter that lives outside
|
|
5
|
+
* this shared, open-core module — DynamoDB, or a Postgres adapter in the
|
|
6
|
+
* repository that deploys it — is never imported here; its composition root
|
|
7
|
+
* calls this before handling a request.
|
|
8
|
+
*/
|
|
9
|
+
export const setClient = (client) => {
|
|
10
|
+
injected = client;
|
|
11
|
+
clientPromise = null;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The SQLite composition this build contains, reached only by a process that
|
|
15
|
+
* registered no client of its own. The `import()` is dynamic so a bundler
|
|
16
|
+
* following the entry graph of a process that injects its client never reaches
|
|
17
|
+
* `@remit/drizzle-service` or drizzle-orm (both `external` for the Lambda
|
|
18
|
+
* esbuild build).
|
|
19
|
+
*
|
|
20
|
+
* `SQLITE_APP_DB_PATH` is the precondition the composition needs, checked ahead of
|
|
21
|
+
* the import so its absence names `setClient` instead of failing on module
|
|
22
|
+
* resolution inside a bundle, or — worse — opening an empty database and
|
|
23
|
+
* serving an empty mailbox. It is a precondition, not a backend selection.
|
|
24
|
+
*/
|
|
25
|
+
const buildRelationalClient = async () => {
|
|
26
|
+
if (!process.env.SQLITE_APP_DB_PATH) {
|
|
27
|
+
throw new Error("no client registered — register one with setClient() from your composition root");
|
|
28
|
+
}
|
|
29
|
+
const composition = await import("./compose-sqlite.js");
|
|
30
|
+
return composition.buildSqliteClient();
|
|
31
|
+
};
|
|
32
|
+
export const getClient = () => {
|
|
33
|
+
if (!clientPromise) {
|
|
34
|
+
clientPromise = injected
|
|
35
|
+
? Promise.resolve(injected)
|
|
36
|
+
: buildRelationalClient();
|
|
37
|
+
}
|
|
38
|
+
return clientPromise;
|
|
39
|
+
};
|
|
40
|
+
/** Reset the singleton and any injected client — test use only. */
|
|
41
|
+
export const _resetForTest = () => {
|
|
42
|
+
clientPromise = null;
|
|
43
|
+
injected = null;
|
|
44
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const DEFAULT_CONTROL_DIR = "/data/control";
|
|
4
|
+
/**
|
|
5
|
+
* The version a file says it was written by. The updater owns this fact: it
|
|
6
|
+
* reads the tag from `.env` and writes it into `state.json`, which is the only
|
|
7
|
+
* honest source across an update that rewrites that tag underneath a
|
|
8
|
+
* long-lived process. With no state file there is nothing authoritative to
|
|
9
|
+
* stamp, and an export says so rather than inventing a version an importing
|
|
10
|
+
* reader would then trust.
|
|
11
|
+
*/
|
|
12
|
+
const runningVersion = () => {
|
|
13
|
+
const dir = process.env.REMIT_UPDATE_CONTROL_DIR ?? DEFAULT_CONTROL_DIR;
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = readFileSync(join(dir, "state.json"), "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "unknown";
|
|
20
|
+
}
|
|
21
|
+
const state = JSON.parse(raw);
|
|
22
|
+
if (typeof state !== "object" || state === null)
|
|
23
|
+
return "unknown";
|
|
24
|
+
const version = state.currentVersion;
|
|
25
|
+
return typeof version === "string" && version.length > 0
|
|
26
|
+
? version
|
|
27
|
+
: "unknown";
|
|
28
|
+
};
|
|
29
|
+
/** Who wrote a configuration file, and from where. Provenance only. */
|
|
30
|
+
export const exportIdentity = (now = new Date()) => ({
|
|
31
|
+
app: "reader",
|
|
32
|
+
version: runningVersion(),
|
|
33
|
+
exportedAt: now.toISOString(),
|
|
34
|
+
instance: process.env.PUBLIC_ORIGIN ?? "",
|
|
35
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
type StructuredLog = (fields: Record<string, unknown>, message: string) => void;
|
|
2
|
+
export interface FireAndForgetLogger {
|
|
3
|
+
error: StructuredLog;
|
|
4
|
+
}
|
|
5
|
+
export interface FireAndForgetContext {
|
|
6
|
+
/**
|
|
7
|
+
* Stable discriminator for the originating call site (e.g. `"config_load"`,
|
|
8
|
+
* `"account_create"`). Surfaces on the log line as `source` so a CloudWatch
|
|
9
|
+
* metric filter / alarm can attribute a leak to its origin.
|
|
10
|
+
*/
|
|
11
|
+
source: string;
|
|
12
|
+
/**
|
|
13
|
+
* Message logged when the background work rejects. Should describe the work
|
|
14
|
+
* and reassure that the failure is contained (e.g. "...best-effort").
|
|
15
|
+
*/
|
|
16
|
+
message: string;
|
|
17
|
+
/**
|
|
18
|
+
* Identifiers carried onto the structured error line (accountId,
|
|
19
|
+
* accountConfigId, …) so a failure is traceable without a stack dive.
|
|
20
|
+
*/
|
|
21
|
+
ids?: Record<string, unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Alertable discriminator on the structured error line, the field CloudWatch
|
|
24
|
+
* metric filters key on. Defaults to `"sync_trigger_failed"` for the original
|
|
25
|
+
* sync-kick sites; a different fire-and-forget surface (e.g. a flag enqueue)
|
|
26
|
+
* must pass its own so it is not mis-attributed to the sync-trigger alarm.
|
|
27
|
+
*/
|
|
28
|
+
alert?: string;
|
|
29
|
+
logger?: FireAndForgetLogger;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Run unawaited background work that MUST NOT be able to fail the request that
|
|
33
|
+
* spawned it.
|
|
34
|
+
*
|
|
35
|
+
* The dev/Lambda runtime shares one event loop across concurrent requests, so a
|
|
36
|
+
* rejection escaping an unawaited background promise (a `void trigger()`) lands
|
|
37
|
+
* on whatever request happens to be in flight when the deferred work finally
|
|
38
|
+
* fails — turning an unrelated READ into a spurious 500. That is exactly how an
|
|
39
|
+
* unreachable SQS queue 500'd `/mailboxes`, `/threads`, `/outbox` and `/config`.
|
|
40
|
+
*
|
|
41
|
+
* This helper is the single containment point: it awaits the work inside a
|
|
42
|
+
* try/catch, logs every rejection LOUDLY with the alertable structured fields
|
|
43
|
+
* (`alert` — defaulting to `"sync_trigger_failed"`, `source`, the SDK error
|
|
44
|
+
* name/code, the caller ids), and ALWAYS resolves to void. Callers `void
|
|
45
|
+
* fireAndForget(...)` with no
|
|
46
|
+
* chance of an unhandled rejection escaping. Routing all fire-and-forget sites
|
|
47
|
+
* through here keeps the containment from regressing one handler at a time.
|
|
48
|
+
*
|
|
49
|
+
* Use this ONLY for genuine side effects whose failure must not change the
|
|
50
|
+
* response. A failure the caller is responsible for (the real DynamoDB read, an
|
|
51
|
+
* enqueue that IS the request's purpose) must still propagate and 500.
|
|
52
|
+
*/
|
|
53
|
+
export declare const fireAndForget: (work: () => Promise<unknown>, context: FireAndForgetContext) => Promise<void>;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { inspect } from "node:util";
|
|
2
|
+
import { logger } from "@remit/logger";
|
|
3
|
+
/**
|
|
4
|
+
* Run unawaited background work that MUST NOT be able to fail the request that
|
|
5
|
+
* spawned it.
|
|
6
|
+
*
|
|
7
|
+
* The dev/Lambda runtime shares one event loop across concurrent requests, so a
|
|
8
|
+
* rejection escaping an unawaited background promise (a `void trigger()`) lands
|
|
9
|
+
* on whatever request happens to be in flight when the deferred work finally
|
|
10
|
+
* fails — turning an unrelated READ into a spurious 500. That is exactly how an
|
|
11
|
+
* unreachable SQS queue 500'd `/mailboxes`, `/threads`, `/outbox` and `/config`.
|
|
12
|
+
*
|
|
13
|
+
* This helper is the single containment point: it awaits the work inside a
|
|
14
|
+
* try/catch, logs every rejection LOUDLY with the alertable structured fields
|
|
15
|
+
* (`alert` — defaulting to `"sync_trigger_failed"`, `source`, the SDK error
|
|
16
|
+
* name/code, the caller ids), and ALWAYS resolves to void. Callers `void
|
|
17
|
+
* fireAndForget(...)` with no
|
|
18
|
+
* chance of an unhandled rejection escaping. Routing all fire-and-forget sites
|
|
19
|
+
* through here keeps the containment from regressing one handler at a time.
|
|
20
|
+
*
|
|
21
|
+
* Use this ONLY for genuine side effects whose failure must not change the
|
|
22
|
+
* response. A failure the caller is responsible for (the real DynamoDB read, an
|
|
23
|
+
* enqueue that IS the request's purpose) must still propagate and 500.
|
|
24
|
+
*/
|
|
25
|
+
export const fireAndForget = async (work, context) => {
|
|
26
|
+
await work().catch((error) => {
|
|
27
|
+
const log = context.logger ?? logger;
|
|
28
|
+
log.error({
|
|
29
|
+
alert: context.alert ?? "sync_trigger_failed",
|
|
30
|
+
source: context.source,
|
|
31
|
+
...context.ids,
|
|
32
|
+
errorName: error?.name,
|
|
33
|
+
errorCode: error?.Code ??
|
|
34
|
+
error?.code,
|
|
35
|
+
error: inspect(error),
|
|
36
|
+
}, context.message);
|
|
37
|
+
});
|
|
38
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { FilterItem, OrganizeJobRequestItem } from "@remit/data-ports";
|
|
2
|
+
import { type FilterMessage, type LiteralClauseNarrowing, PlacementMoveService } from "@remit/mailbox-service";
|
|
3
|
+
import type { RemitClient } from "./data-client.js";
|
|
4
|
+
/**
|
|
5
|
+
* Hard cap on both the previewed and the applied set. A back-apply is a
|
|
6
|
+
* data-heavy corpus pass (remit-data-heavy: frugal), bounded by counting
|
|
7
|
+
* MATCHES as it pages a store-side narrowed listing, and the final match set
|
|
8
|
+
* never exceeds this. It bounds the answer, never the corpus the question is
|
|
9
|
+
* asked of (#459).
|
|
10
|
+
*/
|
|
11
|
+
export declare const ORGANIZE_MATCH_LIMIT = 500;
|
|
12
|
+
/**
|
|
13
|
+
* The predicate + action for one back-apply pass — the OrganizeInput fields,
|
|
14
|
+
* flattened. The action fields carry absence as the `"None"` sentinel (RFC 034
|
|
15
|
+
* Decision 3.1).
|
|
16
|
+
*/
|
|
17
|
+
export interface OrganizePredicate {
|
|
18
|
+
matchOperator: FilterItem["matchOperator"];
|
|
19
|
+
literalClauses: FilterItem["literalClauses"];
|
|
20
|
+
actionLabelId: string;
|
|
21
|
+
actionMailboxId: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The predicate a back-apply job snapshotted onto its row, read back verbatim so
|
|
25
|
+
* the worker runs exactly what the request asked for.
|
|
26
|
+
*/
|
|
27
|
+
export declare const predicateFromJob: (job: OrganizeJobRequestItem) => OrganizePredicate;
|
|
28
|
+
/**
|
|
29
|
+
* One corpus message projected onto the fields a literal clause matches
|
|
30
|
+
* against, paired with its id. Sourced from the core thread rows.
|
|
31
|
+
*/
|
|
32
|
+
export interface OrganizeCandidate {
|
|
33
|
+
messageId: string;
|
|
34
|
+
message: FilterMessage;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* One page of the narrowed corpus, cursored exactly as the underlying listing
|
|
38
|
+
* is: an absent `continuationToken` means the store has no more rows matching
|
|
39
|
+
* the terms, never that a read window ran out.
|
|
40
|
+
*/
|
|
41
|
+
export interface OrganizeCandidatePage {
|
|
42
|
+
items: OrganizeCandidate[];
|
|
43
|
+
continuationToken?: string;
|
|
44
|
+
}
|
|
45
|
+
/** What to ask the store for, and where to resume. */
|
|
46
|
+
export interface OrganizeCandidateQuery extends LiteralClauseNarrowing {
|
|
47
|
+
limit: number;
|
|
48
|
+
continuationToken?: string;
|
|
49
|
+
mailboxIds?: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Where a pass looks and which standing filter it runs for. A filter
|
|
53
|
+
* back-apply narrows the corpus to the inbox and matches a body-content clause
|
|
54
|
+
* on the stored preview; an "all like these" pass sets neither.
|
|
55
|
+
*/
|
|
56
|
+
export interface OrganizeMatchScope {
|
|
57
|
+
filterId?: string;
|
|
58
|
+
mailboxIds?: readonly string[];
|
|
59
|
+
}
|
|
60
|
+
export interface OrganizeMatchDeps {
|
|
61
|
+
/**
|
|
62
|
+
* One page of the account's messages as literal-match candidates, with the
|
|
63
|
+
* clause terms evaluated by the store. Paged rather than capped, so the
|
|
64
|
+
* caller's bound falls on matches instead of on how far back the read
|
|
65
|
+
* reached (#459).
|
|
66
|
+
*/
|
|
67
|
+
listAccountFilterMessages: (accountConfigId: string, query: OrganizeCandidateQuery) => Promise<OrganizeCandidatePage>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Why the matcher refused a predicate. A refusal is an expected outcome of
|
|
71
|
+
* asking for a rule this deployment cannot evaluate, not a fault: the HTTP
|
|
72
|
+
* boundary words it as a 400 and the back-apply worker fails the job row on it,
|
|
73
|
+
* neither of which may treat it as the infrastructure failure a throw would
|
|
74
|
+
* make it (reader #463).
|
|
75
|
+
*/
|
|
76
|
+
export interface OrganizeRejection {
|
|
77
|
+
reason: "BodyContentUnsupported" | "FilterNotActive" | "FilterMissing" | "FolderMissing";
|
|
78
|
+
message: string;
|
|
79
|
+
}
|
|
80
|
+
/** The matched ids. */
|
|
81
|
+
export interface OrganizeMatched {
|
|
82
|
+
rejected: null;
|
|
83
|
+
messageIds: string[];
|
|
84
|
+
}
|
|
85
|
+
/** The predicate was refused: nothing matched, and nothing may be applied. */
|
|
86
|
+
export interface OrganizeMatchRejected {
|
|
87
|
+
rejected: OrganizeRejection;
|
|
88
|
+
}
|
|
89
|
+
export type OrganizeMatchResult = OrganizeMatched | OrganizeMatchRejected;
|
|
90
|
+
/**
|
|
91
|
+
* A `HasWords` clause matches the message body (RFC 031, `clauseMatches`). The
|
|
92
|
+
* live index-time filter evaluates it against the full parsed body
|
|
93
|
+
* (`body-sync.ts` `toFilterMessage`); the corpus slice carries only
|
|
94
|
+
* `From`/`Subject` at full fidelity and no faithful body, so it cannot serve a
|
|
95
|
+
* body-content clause without silently narrowing the match to whatever preview
|
|
96
|
+
* happened to be stored. A body-content clause on an "all like these" pass is
|
|
97
|
+
* refused instead of returning a wrong set.
|
|
98
|
+
*/
|
|
99
|
+
export declare const BODY_CONTENT_REJECTION_MESSAGE = "Organize cannot match a body-content (HasWords) clause over the mailbox: it reads only each message's preview, not its body";
|
|
100
|
+
/**
|
|
101
|
+
* The refusal a predicate carries on its own, decidable without reading the
|
|
102
|
+
* corpus: a body-content clause is refused up front. `createOrganizeJob` runs
|
|
103
|
+
* this before enqueueing and {@link matchOrganize} runs it too, so preview, the
|
|
104
|
+
* worker and the request boundary all refuse the same rule (reader #463).
|
|
105
|
+
*/
|
|
106
|
+
export declare const organizePredicateRejection: (predicate: OrganizePredicate) => OrganizeRejection | null;
|
|
107
|
+
/**
|
|
108
|
+
* The matcher shared by preview and apply — the previewed set equals the applied
|
|
109
|
+
* set for the same input. Read-only. Returns the matching message ids, bounded.
|
|
110
|
+
*
|
|
111
|
+
* - An ad-hoc predicate with no clause matches nothing: a run-now request
|
|
112
|
+
* carries none, and an empty predicate must never sweep the whole mailbox.
|
|
113
|
+
* - A predicate the corpus read cannot evaluate (a body-content clause on an
|
|
114
|
+
* "all like these" pass, see {@link bodyContentRejection}) comes back as a
|
|
115
|
+
* rejection, so a caller can tell a refused rule from a broken deployment
|
|
116
|
+
* (reader #463).
|
|
117
|
+
*/
|
|
118
|
+
export declare const matchOrganize: (deps: OrganizeMatchDeps, accountConfigId: string, predicate: OrganizePredicate, limit?: number, scope?: OrganizeMatchScope) => Promise<OrganizeMatchResult>;
|
|
119
|
+
/** The matcher deps for the running backend/worker. */
|
|
120
|
+
export declare const buildOrganizeMatchDeps: (client: RemitClient) => OrganizeMatchDeps;
|
|
121
|
+
/**
|
|
122
|
+
* The exclusive-move arm of a back-apply, wired exactly like the body-sync
|
|
123
|
+
* placement mover (`sync-message-body.ts`): the local-first
|
|
124
|
+
* {@link PlacementMoveService} over the shared message-management queue. A move
|
|
125
|
+
* commits locally (marker + ThreadMessage + Message row) and enqueues a
|
|
126
|
+
* `PLACEMENT_MOVE_PUSH` for the reconciler; its marker state engine makes a
|
|
127
|
+
* redelivered job idempotent (mirrors #1297). Returns `undefined` when no
|
|
128
|
+
* message-management queue is wired — the same gate body sync uses to keep the
|
|
129
|
+
* move path off — so a move requested in that environment is counted as failed
|
|
130
|
+
* rather than silently dropped (see {@link applyOrganize}).
|
|
131
|
+
*/
|
|
132
|
+
export declare const buildOrganizeMoveService: (client: RemitClient) => PlacementMoveService | undefined;
|
|
133
|
+
export interface ApplyOrganizeDeps {
|
|
134
|
+
client: RemitClient;
|
|
135
|
+
moveService?: PlacementMoveService;
|
|
136
|
+
}
|
|
137
|
+
export interface ApplyOrganizeResult {
|
|
138
|
+
applied: number;
|
|
139
|
+
failed: number;
|
|
140
|
+
/** Why each failed message failed, as the job reports it. */
|
|
141
|
+
failures: string[];
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Apply the back-apply action to every matched message, reusing the index-time
|
|
145
|
+
* apply plumbing: an idempotent MessageLabel upsert (additive) with
|
|
146
|
+
* `appliedByFilterId` deliberately ABSENT — this path attributes to no filter,
|
|
147
|
+
* exactly like a hand-applied label (RFC 034 Decision 3.3) — and an idempotent
|
|
148
|
+
* folder move (exclusive). One poisoned message never fails the batch; it is
|
|
149
|
+
* counted as failed and the pass continues.
|
|
150
|
+
*
|
|
151
|
+
* Every requested move happens. A standing filter acts at exactly two moments —
|
|
152
|
+
* when a message is first seen, and when the user presses "run rules now" — and
|
|
153
|
+
* holds no claim on that message afterwards, so it never arbitrates against a
|
|
154
|
+
* later user-initiated apply (reader #497). Cross-filter precedence stays where
|
|
155
|
+
* two filters collide over the same message at the same trigger
|
|
156
|
+
* (`FilterPipeline`/`selectMoveWinner`), and `applied` therefore counts moves
|
|
157
|
+
* that actually happened.
|
|
158
|
+
*/
|
|
159
|
+
export declare const applyOrganize: (deps: ApplyOrganizeDeps, accountConfigId: string, messageIds: readonly string[], predicate: OrganizePredicate) => Promise<ApplyOrganizeResult>;
|
|
160
|
+
export interface FilterBackApplyResult {
|
|
161
|
+
rejected: null;
|
|
162
|
+
matched: number;
|
|
163
|
+
applied: number;
|
|
164
|
+
failed: number;
|
|
165
|
+
failures: string[];
|
|
166
|
+
}
|
|
167
|
+
export interface FilterBackApplyDeps {
|
|
168
|
+
client: RemitClient;
|
|
169
|
+
matchDeps: OrganizeMatchDeps;
|
|
170
|
+
moveService?: PlacementMoveService;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Run one standing filter over the inbox as if each matching message had just
|
|
174
|
+
* arrived. Every message the matcher returns is a match: its label always
|
|
175
|
+
* applies, being additive. The index-time {@link FilterPipeline} only orders
|
|
176
|
+
* the exclusive move, so the move is skipped where it names another active
|
|
177
|
+
* filter. The pipeline sees the stored preview rather than the full body, so
|
|
178
|
+
* its verdict on whether this filter matches is not asked; and an evaluate it
|
|
179
|
+
* could not finish names no other filter, so the move goes ahead rather than
|
|
180
|
+
* silently dropping out.
|
|
181
|
+
*/
|
|
182
|
+
export declare const backApplyFilter: (deps: FilterBackApplyDeps, accountConfigId: string, filterId: string) => Promise<FilterBackApplyResult | OrganizeMatchRejected>;
|
|
183
|
+
/** Whether a job row runs a standing filter rather than its own snapshot. */
|
|
184
|
+
export declare const isFilterJob: (job: Pick<OrganizeJobRequestItem, "filterId">) => boolean;
|