@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,384 @@
|
|
|
1
|
+
import { NotFoundError } from "@remit/data-ports/errors";
|
|
2
|
+
import { FilterClauseField, FilterDisabledReason, FilterState, } from "@remit/domain-enums";
|
|
3
|
+
import { logger } from "@remit/logger";
|
|
4
|
+
import { FilterPipeline, literalClausesMatch, literalClauseTerms, NO_ACTION, PlacementMoveService, } from "@remit/mailbox-service";
|
|
5
|
+
import { buildFilterConfig } from "@remit/mailbox-service/filter-config";
|
|
6
|
+
/**
|
|
7
|
+
* Hard cap on both the previewed and the applied set. A back-apply is a
|
|
8
|
+
* data-heavy corpus pass (remit-data-heavy: frugal), bounded by counting
|
|
9
|
+
* MATCHES as it pages a store-side narrowed listing, and the final match set
|
|
10
|
+
* never exceeds this. It bounds the answer, never the corpus the question is
|
|
11
|
+
* asked of (#459).
|
|
12
|
+
*/
|
|
13
|
+
export const ORGANIZE_MATCH_LIMIT = 500;
|
|
14
|
+
/**
|
|
15
|
+
* The predicate a back-apply job snapshotted onto its row, read back verbatim so
|
|
16
|
+
* the worker runs exactly what the request asked for.
|
|
17
|
+
*/
|
|
18
|
+
export const predicateFromJob = (job) => ({
|
|
19
|
+
matchOperator: job.matchOperator,
|
|
20
|
+
literalClauses: job.literalClauses,
|
|
21
|
+
actionLabelId: job.actionLabelId,
|
|
22
|
+
actionMailboxId: job.actionMailboxId,
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* A `HasWords` clause matches the message body (RFC 031, `clauseMatches`). The
|
|
26
|
+
* live index-time filter evaluates it against the full parsed body
|
|
27
|
+
* (`body-sync.ts` `toFilterMessage`); the corpus slice carries only
|
|
28
|
+
* `From`/`Subject` at full fidelity and no faithful body, so it cannot serve a
|
|
29
|
+
* body-content clause without silently narrowing the match to whatever preview
|
|
30
|
+
* happened to be stored. A body-content clause on an "all like these" pass is
|
|
31
|
+
* refused instead of returning a wrong set.
|
|
32
|
+
*/
|
|
33
|
+
export 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";
|
|
34
|
+
const bodyContentRejection = (clauses) => clauses.some((clause) => clause.field === FilterClauseField.HasWords)
|
|
35
|
+
? {
|
|
36
|
+
reason: "BodyContentUnsupported",
|
|
37
|
+
message: BODY_CONTENT_REJECTION_MESSAGE,
|
|
38
|
+
}
|
|
39
|
+
: null;
|
|
40
|
+
/**
|
|
41
|
+
* The refusal a predicate carries on its own, decidable without reading the
|
|
42
|
+
* corpus: a body-content clause is refused up front. `createOrganizeJob` runs
|
|
43
|
+
* this before enqueueing and {@link matchOrganize} runs it too, so preview, the
|
|
44
|
+
* worker and the request boundary all refuse the same rule (reader #463).
|
|
45
|
+
*/
|
|
46
|
+
export const organizePredicateRejection = (predicate) => bodyContentRejection(predicate.literalClauses);
|
|
47
|
+
/**
|
|
48
|
+
* The store selects on the clause terms, this refines what it returned and
|
|
49
|
+
* stops at `limit` MATCHES. Serves `From`/`Subject` clauses at
|
|
50
|
+
* full fidelity from the core thread rows; body-content (`HasWords`) clauses are
|
|
51
|
+
* refused before this runs (see {@link bodyContentRejection}).
|
|
52
|
+
*
|
|
53
|
+
* The terms are a narrowing, not the verdict ({@link literalClauseTerms}), so
|
|
54
|
+
* every page is still run through {@link literalClausesMatch} — and paging
|
|
55
|
+
* continues until the cap fills or the store runs out, which is what makes the
|
|
56
|
+
* bound a bound on results rather than on the newest `limit` rows (#459).
|
|
57
|
+
* Deduped by message id: the same mail filed in two folders is one match.
|
|
58
|
+
*/
|
|
59
|
+
const matchLiteral = async (deps, accountConfigId, predicate, limit, scope) => {
|
|
60
|
+
const clauses = predicate.literalClauses;
|
|
61
|
+
const narrowing = literalClauseTerms(clauses, predicate.matchOperator);
|
|
62
|
+
if (!narrowing)
|
|
63
|
+
return [];
|
|
64
|
+
const matched = [];
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
let continuationToken;
|
|
67
|
+
do {
|
|
68
|
+
const page = await deps.listAccountFilterMessages(accountConfigId, {
|
|
69
|
+
...narrowing,
|
|
70
|
+
limit,
|
|
71
|
+
continuationToken,
|
|
72
|
+
mailboxIds: scope.mailboxIds,
|
|
73
|
+
});
|
|
74
|
+
for (const { messageId, message } of page.items) {
|
|
75
|
+
if (seen.has(messageId))
|
|
76
|
+
continue;
|
|
77
|
+
seen.add(messageId);
|
|
78
|
+
if (!literalClausesMatch(clauses, predicate.matchOperator, message)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
matched.push(messageId);
|
|
82
|
+
if (matched.length >= limit)
|
|
83
|
+
return matched;
|
|
84
|
+
}
|
|
85
|
+
continuationToken = page.continuationToken;
|
|
86
|
+
} while (continuationToken);
|
|
87
|
+
return matched;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The matcher shared by preview and apply — the previewed set equals the applied
|
|
91
|
+
* set for the same input. Read-only. Returns the matching message ids, bounded.
|
|
92
|
+
*
|
|
93
|
+
* - An ad-hoc predicate with no clause matches nothing: a run-now request
|
|
94
|
+
* carries none, and an empty predicate must never sweep the whole mailbox.
|
|
95
|
+
* - A predicate the corpus read cannot evaluate (a body-content clause on an
|
|
96
|
+
* "all like these" pass, see {@link bodyContentRejection}) comes back as a
|
|
97
|
+
* rejection, so a caller can tell a refused rule from a broken deployment
|
|
98
|
+
* (reader #463).
|
|
99
|
+
*/
|
|
100
|
+
export const matchOrganize = async (deps, accountConfigId, predicate, limit = ORGANIZE_MATCH_LIMIT, scope = {}) => {
|
|
101
|
+
if (predicate.literalClauses.length === 0) {
|
|
102
|
+
return { rejected: null, messageIds: [] };
|
|
103
|
+
}
|
|
104
|
+
const rejection = scope.filterId
|
|
105
|
+
? null
|
|
106
|
+
: organizePredicateRejection(predicate);
|
|
107
|
+
if (rejection)
|
|
108
|
+
return { rejected: rejection };
|
|
109
|
+
const messageIds = await matchLiteral(deps, accountConfigId, predicate, limit, scope);
|
|
110
|
+
return { rejected: null, messageIds };
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* The corpus read for a back-apply: one page of the account's messages,
|
|
114
|
+
* newest-first across every mailbox, with the clause terms evaluated in the
|
|
115
|
+
* query.
|
|
116
|
+
*
|
|
117
|
+
* `From`/`Subject`/`listId` come from the row verbatim (full fidelity), so a
|
|
118
|
+
* `From`, `Subject`, `FromDomain` or `ListId` clause matches here exactly as it
|
|
119
|
+
* does at index time. `text` is the row's stored body preview (`snippet`),
|
|
120
|
+
* the same text search matches free words against. An "all like these" pass
|
|
121
|
+
* refuses a body-content (`HasWords`) clause before reading it (see
|
|
122
|
+
* {@link bodyContentRejection}); a filter back-apply matches it on the
|
|
123
|
+
* preview, the mail search showed the user when the rule was made (#1354).
|
|
124
|
+
*/
|
|
125
|
+
const listAccountFilterMessagesFromClient = (client) => async (accountConfigId, query) => {
|
|
126
|
+
const page = await client.threadMessage.listByFieldTerms(accountConfigId, query.terms, {
|
|
127
|
+
operator: query.operator,
|
|
128
|
+
limit: query.limit,
|
|
129
|
+
continuationToken: query.continuationToken,
|
|
130
|
+
excludeDeleted: true,
|
|
131
|
+
mailboxIds: query.mailboxIds,
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
items: page.items.map((row) => ({
|
|
135
|
+
messageId: row.messageId,
|
|
136
|
+
message: {
|
|
137
|
+
from: row.fromEmail ?? "",
|
|
138
|
+
fromName: row.fromName ?? "",
|
|
139
|
+
subject: row.subject ?? "",
|
|
140
|
+
text: row.snippet ?? "",
|
|
141
|
+
listId: row.listId ?? "",
|
|
142
|
+
},
|
|
143
|
+
})),
|
|
144
|
+
continuationToken: page.continuationToken,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
/** The matcher deps for the running backend/worker. */
|
|
148
|
+
export const buildOrganizeMatchDeps = (client) => ({
|
|
149
|
+
listAccountFilterMessages: listAccountFilterMessagesFromClient(client),
|
|
150
|
+
});
|
|
151
|
+
/**
|
|
152
|
+
* The exclusive-move arm of a back-apply, wired exactly like the body-sync
|
|
153
|
+
* placement mover (`sync-message-body.ts`): the local-first
|
|
154
|
+
* {@link PlacementMoveService} over the shared message-management queue. A move
|
|
155
|
+
* commits locally (marker + ThreadMessage + Message row) and enqueues a
|
|
156
|
+
* `PLACEMENT_MOVE_PUSH` for the reconciler; its marker state engine makes a
|
|
157
|
+
* redelivered job idempotent (mirrors #1297). Returns `undefined` when no
|
|
158
|
+
* message-management queue is wired — the same gate body sync uses to keep the
|
|
159
|
+
* move path off — so a move requested in that environment is counted as failed
|
|
160
|
+
* rather than silently dropped (see {@link applyOrganize}).
|
|
161
|
+
*/
|
|
162
|
+
export const buildOrganizeMoveService = (client) => {
|
|
163
|
+
const queueUrl = process.env.SQS_QUEUE_URL_MESSAGE_MGMT;
|
|
164
|
+
if (!queueUrl)
|
|
165
|
+
return undefined;
|
|
166
|
+
return new PlacementMoveService({
|
|
167
|
+
messageService: client.message,
|
|
168
|
+
threadMessageService: client.threadMessage,
|
|
169
|
+
markerService: client.placementMove,
|
|
170
|
+
addressService: client.address,
|
|
171
|
+
mailboxSpecialUseService: client.mailboxSpecialUse,
|
|
172
|
+
mailboxService: client.mailbox,
|
|
173
|
+
sqsQueueUrl: queueUrl,
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Apply the back-apply action to every matched message, reusing the index-time
|
|
178
|
+
* apply plumbing: an idempotent MessageLabel upsert (additive) with
|
|
179
|
+
* `appliedByFilterId` deliberately ABSENT — this path attributes to no filter,
|
|
180
|
+
* exactly like a hand-applied label (RFC 034 Decision 3.3) — and an idempotent
|
|
181
|
+
* folder move (exclusive). One poisoned message never fails the batch; it is
|
|
182
|
+
* counted as failed and the pass continues.
|
|
183
|
+
*
|
|
184
|
+
* Every requested move happens. A standing filter acts at exactly two moments —
|
|
185
|
+
* when a message is first seen, and when the user presses "run rules now" — and
|
|
186
|
+
* holds no claim on that message afterwards, so it never arbitrates against a
|
|
187
|
+
* later user-initiated apply (reader #497). Cross-filter precedence stays where
|
|
188
|
+
* two filters collide over the same message at the same trigger
|
|
189
|
+
* (`FilterPipeline`/`selectMoveWinner`), and `applied` therefore counts moves
|
|
190
|
+
* that actually happened.
|
|
191
|
+
*/
|
|
192
|
+
export const applyOrganize = async (deps, accountConfigId, messageIds, predicate) => {
|
|
193
|
+
const { client, moveService } = deps;
|
|
194
|
+
const applyLabel = predicate.actionLabelId !== NO_ACTION && predicate.actionLabelId !== "";
|
|
195
|
+
const applyMove = predicate.actionMailboxId !== NO_ACTION && predicate.actionMailboxId !== "";
|
|
196
|
+
const applyToMessage = async (messageId) => {
|
|
197
|
+
if (applyMove) {
|
|
198
|
+
if (!moveService) {
|
|
199
|
+
// An exclusive move was requested but this caller wired no move
|
|
200
|
+
// service. Never silently pretend it applied — surface it as a
|
|
201
|
+
// failed message so the job's failedCount reflects reality.
|
|
202
|
+
throw new Error("Organize move action requested but no move service is wired");
|
|
203
|
+
}
|
|
204
|
+
const message = await client.message.get(messageId);
|
|
205
|
+
const accountId = await client.mailbox.resolveAccountId(message.mailboxId);
|
|
206
|
+
if (!accountId) {
|
|
207
|
+
throw new Error(`Cannot resolve owning account for mailbox ${message.mailboxId}`);
|
|
208
|
+
}
|
|
209
|
+
const outcome = await moveService.moveMessage(accountConfigId, messageId, predicate.actionMailboxId, accountId);
|
|
210
|
+
if (outcome === "refused")
|
|
211
|
+
return { kind: "skipped" };
|
|
212
|
+
}
|
|
213
|
+
if (applyLabel) {
|
|
214
|
+
await client.messageLabel.apply({
|
|
215
|
+
messageId,
|
|
216
|
+
labelId: predicate.actionLabelId,
|
|
217
|
+
accountConfigId,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return { kind: "applied" };
|
|
221
|
+
};
|
|
222
|
+
const result = {
|
|
223
|
+
applied: 0,
|
|
224
|
+
failed: 0,
|
|
225
|
+
failures: [],
|
|
226
|
+
};
|
|
227
|
+
for (const messageId of messageIds) {
|
|
228
|
+
// Per-message isolation: one poisoned message is counted, its reason kept
|
|
229
|
+
// for the job, and the pass continues.
|
|
230
|
+
const outcome = await applyToMessage(messageId).catch((error) => ({
|
|
231
|
+
kind: "failed",
|
|
232
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
233
|
+
}));
|
|
234
|
+
if (outcome.kind === "skipped")
|
|
235
|
+
continue;
|
|
236
|
+
if (outcome.kind === "applied") {
|
|
237
|
+
result.applied += 1;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
result.failed += 1;
|
|
241
|
+
result.failures.push(outcome.reason);
|
|
242
|
+
logger.warn({ accountConfigId, messageId, reason: outcome.reason }, "Organize could not apply its action to a message");
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
};
|
|
246
|
+
const FILTER_NOT_ACTIVE_MESSAGE = "This filter is turned off or has expired. Turn it on to run it over the inbox.";
|
|
247
|
+
const FOLDER_MISSING_MESSAGE = "The folder this filter moves mail into no longer exists, so the filter was turned off. Pick another folder in the rule, then turn it on.";
|
|
248
|
+
const FILTER_MISSING_MESSAGE = "This filter was deleted before it could run over the inbox.";
|
|
249
|
+
const rejectFilter = (reason, message) => ({ rejected: { reason, message } });
|
|
250
|
+
/** A standing filter's predicate and action, read the way index time reads them. */
|
|
251
|
+
const predicateFromFilter = (filter) => ({
|
|
252
|
+
matchOperator: filter.matchOperator,
|
|
253
|
+
literalClauses: filter.literalClauses,
|
|
254
|
+
actionLabelId: filter.actionLabelId,
|
|
255
|
+
actionMailboxId: filter.actionMailboxId,
|
|
256
|
+
});
|
|
257
|
+
/**
|
|
258
|
+
* Read the filter a job names: refused when it was deleted or is not Active,
|
|
259
|
+
* since index time evaluates neither.
|
|
260
|
+
*/
|
|
261
|
+
const loadFilter = async (client, accountConfigId, filterId) => {
|
|
262
|
+
const stored = await client.filter
|
|
263
|
+
.get(accountConfigId, filterId)
|
|
264
|
+
.catch((error) => {
|
|
265
|
+
if (error instanceof NotFoundError)
|
|
266
|
+
return null;
|
|
267
|
+
throw error;
|
|
268
|
+
});
|
|
269
|
+
if (!stored)
|
|
270
|
+
return rejectFilter("FilterMissing", FILTER_MISSING_MESSAGE);
|
|
271
|
+
const filter = await client.filter.refreshExpiry(stored);
|
|
272
|
+
if (filter.state !== FilterState.Active) {
|
|
273
|
+
return rejectFilter("FilterNotActive", FILTER_NOT_ACTIVE_MESSAGE);
|
|
274
|
+
}
|
|
275
|
+
return { rejected: null, filter, predicate: predicateFromFilter(filter) };
|
|
276
|
+
};
|
|
277
|
+
/** Every inbox under the config — the folders a filter back-apply runs over. */
|
|
278
|
+
const inboxMailboxIds = async (client, accountConfigId) => {
|
|
279
|
+
const accounts = await client.account.listMailSourcesByAccountConfig(accountConfigId);
|
|
280
|
+
const inboxes = await Promise.all(accounts.map((account) => client.mailboxSpecialUse.findInboxMailbox(account.accountId)));
|
|
281
|
+
return inboxes.flatMap((inbox) => (inbox ? [inbox.mailboxId] : []));
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* The message as the index-time pipeline reads it, from its thread row. The
|
|
285
|
+
* body is the stored preview, so a `HasWords` clause matches what search
|
|
286
|
+
* matched. A message deleted since the match has no row.
|
|
287
|
+
*/
|
|
288
|
+
const filterMessageOf = async (client, accountConfigId, messageId) => {
|
|
289
|
+
const row = await client.threadMessage
|
|
290
|
+
.getByMessageId(accountConfigId, messageId)
|
|
291
|
+
.catch((error) => {
|
|
292
|
+
if (error instanceof NotFoundError)
|
|
293
|
+
return null;
|
|
294
|
+
throw error;
|
|
295
|
+
});
|
|
296
|
+
if (!row)
|
|
297
|
+
return null;
|
|
298
|
+
return {
|
|
299
|
+
from: row.fromEmail ?? "",
|
|
300
|
+
fromName: row.fromName ?? "",
|
|
301
|
+
subject: row.subject ?? "",
|
|
302
|
+
text: row.snippet ?? "",
|
|
303
|
+
listId: row.listId ?? "",
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
/**
|
|
307
|
+
* Run one standing filter over the inbox as if each matching message had just
|
|
308
|
+
* arrived. Every message the matcher returns is a match: its label always
|
|
309
|
+
* applies, being additive. The index-time {@link FilterPipeline} only orders
|
|
310
|
+
* the exclusive move, so the move is skipped where it names another active
|
|
311
|
+
* filter. The pipeline sees the stored preview rather than the full body, so
|
|
312
|
+
* its verdict on whether this filter matches is not asked; and an evaluate it
|
|
313
|
+
* could not finish names no other filter, so the move goes ahead rather than
|
|
314
|
+
* silently dropping out.
|
|
315
|
+
*/
|
|
316
|
+
export const backApplyFilter = async (deps, accountConfigId, filterId) => {
|
|
317
|
+
const { client, matchDeps, moveService } = deps;
|
|
318
|
+
const loaded = await loadFilter(client, accountConfigId, filterId);
|
|
319
|
+
if (loaded.rejected)
|
|
320
|
+
return loaded;
|
|
321
|
+
const config = buildFilterConfig({
|
|
322
|
+
filterService: client.filter,
|
|
323
|
+
messageLabelService: client.messageLabel,
|
|
324
|
+
mailboxService: client.mailbox,
|
|
325
|
+
placementMoveService: moveService,
|
|
326
|
+
});
|
|
327
|
+
if (!config) {
|
|
328
|
+
throw new Error("Filter back-apply needs the message-management queue to move mail");
|
|
329
|
+
}
|
|
330
|
+
const pipeline = new FilterPipeline(config, logger);
|
|
331
|
+
const mailboxIds = await inboxMailboxIds(client, accountConfigId);
|
|
332
|
+
const nothing = {
|
|
333
|
+
rejected: null,
|
|
334
|
+
matched: 0,
|
|
335
|
+
applied: 0,
|
|
336
|
+
failed: 0,
|
|
337
|
+
failures: [],
|
|
338
|
+
};
|
|
339
|
+
if (mailboxIds.length === 0)
|
|
340
|
+
return nothing;
|
|
341
|
+
const match = await matchOrganize(matchDeps, accountConfigId, loaded.predicate, ORGANIZE_MATCH_LIMIT, { filterId, mailboxIds });
|
|
342
|
+
if (match.rejected)
|
|
343
|
+
return match;
|
|
344
|
+
const { filter, predicate } = loaded;
|
|
345
|
+
const folderAccountId = filter.actionMailboxId === NO_ACTION
|
|
346
|
+
? undefined
|
|
347
|
+
: await client.mailbox.resolveAccountId(filter.actionMailboxId);
|
|
348
|
+
if (folderAccountId === null) {
|
|
349
|
+
await client.filter.update(accountConfigId, filterId, {
|
|
350
|
+
state: FilterState.Disabled,
|
|
351
|
+
disabledReason: FilterDisabledReason.FolderMissing,
|
|
352
|
+
});
|
|
353
|
+
return rejectFilter("FolderMissing", FOLDER_MISSING_MESSAGE);
|
|
354
|
+
}
|
|
355
|
+
const result = {
|
|
356
|
+
...nothing,
|
|
357
|
+
matched: match.messageIds.length,
|
|
358
|
+
};
|
|
359
|
+
for (const messageId of match.messageIds) {
|
|
360
|
+
const message = await filterMessageOf(client, accountConfigId, messageId);
|
|
361
|
+
if (!message)
|
|
362
|
+
continue;
|
|
363
|
+
const accountId = await client.mailbox.resolveAccountId((await client.message.get(messageId)).mailboxId);
|
|
364
|
+
if (!accountId)
|
|
365
|
+
continue;
|
|
366
|
+
if (folderAccountId !== undefined && folderAccountId !== accountId) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const decision = await pipeline.evaluate(accountConfigId, accountId, messageId, message);
|
|
370
|
+
const move = (decision.move?.filterId ?? filterId) === filterId;
|
|
371
|
+
if (!move && filter.actionLabelId === NO_ACTION)
|
|
372
|
+
continue;
|
|
373
|
+
const outcome = await applyOrganize({ client, moveService }, accountConfigId, [messageId], {
|
|
374
|
+
...predicate,
|
|
375
|
+
actionMailboxId: move ? filter.actionMailboxId : NO_ACTION,
|
|
376
|
+
});
|
|
377
|
+
result.applied += outcome.applied;
|
|
378
|
+
result.failed += outcome.failed;
|
|
379
|
+
result.failures.push(...outcome.failures);
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
};
|
|
383
|
+
/** Whether a job row runs a standing filter rather than its own snapshot. */
|
|
384
|
+
export const isFilterJob = (job) => job.filterId !== NO_ACTION;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BadRequestError, ConflictError, ForbiddenError, MailboxNotSettledError, NotFoundError, } from "@remit/data-ports/errors";
|
|
2
|
+
export const refusalError = (refusal) => {
|
|
3
|
+
switch (refusal.kind) {
|
|
4
|
+
case "Invalid":
|
|
5
|
+
return new BadRequestError(refusal.message);
|
|
6
|
+
case "Forbidden":
|
|
7
|
+
return new ForbiddenError(refusal.message);
|
|
8
|
+
case "NotFound":
|
|
9
|
+
return new NotFoundError(refusal.message);
|
|
10
|
+
case "Conflict":
|
|
11
|
+
return new ConflictError(refusal.message);
|
|
12
|
+
case "NotSettled":
|
|
13
|
+
return new MailboxNotSettledError(refusal.message, refusal.mailboxId, refusal.syncStatus);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
export const unwrap = (outcome) => {
|
|
17
|
+
if (outcome.ok)
|
|
18
|
+
return outcome.value;
|
|
19
|
+
throw refusalError(outcome.refusal);
|
|
20
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { MessageCategoryValue } from "@remit/mailbox-service";
|
|
2
|
+
import type { RemitClient } from "./data-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* How much of a sender's already-classified mail one override reaches: the
|
|
5
|
+
* newest 500 of their messages held in the local store. A back-apply is a
|
|
6
|
+
* bounded recent batch, never the sender's whole history — the store-side
|
|
7
|
+
* narrowing pages until this many MATCHES, so a quiet sender's mail is reached
|
|
8
|
+
* however old it is, and a bulk sender's oldest mail is deliberately left as
|
|
9
|
+
* the classifier decided it. The same constant the organize back-apply bounds
|
|
10
|
+
* its match set with (`ORGANIZE_MATCH_LIMIT`), for the same reason.
|
|
11
|
+
*/
|
|
12
|
+
export declare const SENDER_CATEGORY_BACKAPPLY_LIMIT = 500;
|
|
13
|
+
export interface SenderCategoryBackApplyDeps {
|
|
14
|
+
client: Pick<RemitClient, "message" | "threadMessage">;
|
|
15
|
+
}
|
|
16
|
+
export interface SenderCategoryBackApplyResult {
|
|
17
|
+
matched: number;
|
|
18
|
+
applied: number;
|
|
19
|
+
failed: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Back-apply a sender's `Address.flags.category` override to mail that was
|
|
23
|
+
* already classified before the override was set (#415). Issue #299 gave the
|
|
24
|
+
* override full effect on the sender's NEXT message only; this is the
|
|
25
|
+
* retroactive half, deliberately bounded to
|
|
26
|
+
* {@link SENDER_CATEGORY_BACKAPPLY_LIMIT}.
|
|
27
|
+
*
|
|
28
|
+
* This is the one path that intentionally rewrites an already-decided
|
|
29
|
+
* `Message.category`, so it does not — and must not — reuse body sync's
|
|
30
|
+
* `hasDecidedCategory` write-once guard. Its consistency story is that
|
|
31
|
+
* `category` is a key attribute of no provisioned index: the DynamoDB LSI that
|
|
32
|
+
* would have sorted on it was never provisioned and cannot be (#516), and the
|
|
33
|
+
* filtered-mailbox read path is served by the SQL-only
|
|
34
|
+
* `tm_by_mailbox_category_date` index, where an update simply moves the row.
|
|
35
|
+
* What a rewrite can still break is the Message/ThreadMessage pair disagreeing
|
|
36
|
+
* — the badge is served from the thread row and so is the category filter — so
|
|
37
|
+
* both are written per message, thread rows first, and a redelivered job
|
|
38
|
+
* re-runs the same idempotent writes.
|
|
39
|
+
*
|
|
40
|
+
* Nothing here touches placement: `category` is not an input to
|
|
41
|
+
* `classifyPlacement`, so an override re-labels mail where it already sits and
|
|
42
|
+
* files nothing. No remote mail-server state changes, which is why this is not
|
|
43
|
+
* an IMAP mutation and does not take the mutator pattern.
|
|
44
|
+
*
|
|
45
|
+
* Per-message isolation, exactly as `applyOrganize` has it: one poisoned
|
|
46
|
+
* message is counted as failed and the pass continues, so a single missing
|
|
47
|
+
* thread row never costs the other 499 their re-label. Unlike a job row, this
|
|
48
|
+
* count is not the end of the story — the caller is expected to leave a pass
|
|
49
|
+
* with failures unacknowledged so redelivery converges the rest.
|
|
50
|
+
*/
|
|
51
|
+
export declare const backApplySenderCategory: (deps: SenderCategoryBackApplyDeps, accountConfigId: string, normalizedEmail: string, category: MessageCategoryValue, limit?: number) => Promise<SenderCategoryBackApplyResult>;
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import type { MessageCategoryValue } from "@remit/mailbox-service";
|
|
2
1
|
import { denormalizeMessageCategory } from "@remit/mailbox-service";
|
|
3
|
-
import type { RemitClient } from "./data-client.js";
|
|
4
|
-
|
|
5
2
|
/**
|
|
6
3
|
* How much of a sender's already-classified mail one override reaches: the
|
|
7
4
|
* newest 500 of their messages held in the local store. A back-apply is a
|
|
@@ -12,17 +9,6 @@ import type { RemitClient } from "./data-client.js";
|
|
|
12
9
|
* its match set with (`ORGANIZE_MATCH_LIMIT`), for the same reason.
|
|
13
10
|
*/
|
|
14
11
|
export const SENDER_CATEGORY_BACKAPPLY_LIMIT = 500;
|
|
15
|
-
|
|
16
|
-
export interface SenderCategoryBackApplyDeps {
|
|
17
|
-
client: Pick<RemitClient, "message" | "threadMessage">;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface SenderCategoryBackApplyResult {
|
|
21
|
-
matched: number;
|
|
22
|
-
applied: number;
|
|
23
|
-
failed: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
12
|
/**
|
|
27
13
|
* The sender's newest messages in the local store, bounded at `limit`, newest
|
|
28
14
|
* first and deduplicated by messageId.
|
|
@@ -34,36 +20,27 @@ export interface SenderCategoryBackApplyResult {
|
|
|
34
20
|
* out, which is what makes the bound a bound on the sender's messages rather
|
|
35
21
|
* than on how far back the read reached (#459).
|
|
36
22
|
*/
|
|
37
|
-
const listSenderMessageIds = async (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
seen.add(row.messageId);
|
|
58
|
-
messageIds.push(row.messageId);
|
|
59
|
-
if (messageIds.length >= limit) return messageIds;
|
|
60
|
-
}
|
|
61
|
-
continuationToken = page.continuationToken;
|
|
62
|
-
} while (continuationToken);
|
|
63
|
-
|
|
64
|
-
return messageIds;
|
|
23
|
+
const listSenderMessageIds = async (deps, accountConfigId, normalizedEmail, limit) => {
|
|
24
|
+
const wanted = normalizedEmail.toLowerCase();
|
|
25
|
+
const messageIds = [];
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
let continuationToken;
|
|
28
|
+
do {
|
|
29
|
+
const page = await deps.client.threadMessage.listByFieldTerms(accountConfigId, [{ field: "sender", contains: wanted }], { limit, continuationToken, excludeDeleted: true });
|
|
30
|
+
for (const row of page.items) {
|
|
31
|
+
if (row.fromEmail?.toLowerCase() !== wanted)
|
|
32
|
+
continue;
|
|
33
|
+
if (seen.has(row.messageId))
|
|
34
|
+
continue;
|
|
35
|
+
seen.add(row.messageId);
|
|
36
|
+
messageIds.push(row.messageId);
|
|
37
|
+
if (messageIds.length >= limit)
|
|
38
|
+
return messageIds;
|
|
39
|
+
}
|
|
40
|
+
continuationToken = page.continuationToken;
|
|
41
|
+
} while (continuationToken);
|
|
42
|
+
return messageIds;
|
|
65
43
|
};
|
|
66
|
-
|
|
67
44
|
/**
|
|
68
45
|
* Back-apply a sender's `Address.flags.category` override to mail that was
|
|
69
46
|
* already classified before the override was set (#415). Issue #299 gave the
|
|
@@ -94,42 +71,24 @@ const listSenderMessageIds = async (
|
|
|
94
71
|
* count is not the end of the story — the caller is expected to leave a pass
|
|
95
72
|
* with failures unacknowledged so redelivery converges the rest.
|
|
96
73
|
*/
|
|
97
|
-
export const backApplySenderCategory = async (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
);
|
|
118
|
-
await deps.client.message.update(messageId, { category });
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
let applied = 0;
|
|
122
|
-
let failed = 0;
|
|
123
|
-
for (const messageId of messageIds) {
|
|
124
|
-
const ok = await relabel(messageId)
|
|
125
|
-
.then(() => true)
|
|
126
|
-
.catch(() => false);
|
|
127
|
-
if (ok) {
|
|
128
|
-
applied += 1;
|
|
129
|
-
} else {
|
|
130
|
-
failed += 1;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return { matched: messageIds.length, applied, failed };
|
|
74
|
+
export const backApplySenderCategory = async (deps, accountConfigId, normalizedEmail, category, limit = SENDER_CATEGORY_BACKAPPLY_LIMIT) => {
|
|
75
|
+
const messageIds = await listSenderMessageIds(deps, accountConfigId, normalizedEmail, limit);
|
|
76
|
+
const relabel = async (messageId) => {
|
|
77
|
+
await denormalizeMessageCategory({ threadMessageService: deps.client.threadMessage }, accountConfigId, messageId, { category });
|
|
78
|
+
await deps.client.message.update(messageId, { category });
|
|
79
|
+
};
|
|
80
|
+
let applied = 0;
|
|
81
|
+
let failed = 0;
|
|
82
|
+
for (const messageId of messageIds) {
|
|
83
|
+
const ok = await relabel(messageId)
|
|
84
|
+
.then(() => true)
|
|
85
|
+
.catch(() => false);
|
|
86
|
+
if (ok) {
|
|
87
|
+
applied += 1;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
failed += 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { matched: messageIds.length, applied, failed };
|
|
135
94
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const sqsClient: import("@aws-sdk/client-sqs").SQSClient;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { SendMessageCommand, type SQSClient } from "@aws-sdk/client-sqs";
|
|
2
|
+
interface TriggerAccountSyncInput {
|
|
3
|
+
sqsClient: SQSClient;
|
|
4
|
+
queueUrl: string;
|
|
5
|
+
accountId: string;
|
|
6
|
+
/**
|
|
7
|
+
* Set by POST /sync, whose callers ask for a sync of one named account: the
|
|
8
|
+
* refresh control, pull-to-refresh, and the web client's automatic poll
|
|
9
|
+
* (`useStaleAccountSync`) — a timer, not a person. It travels on the event
|
|
10
|
+
* and makes the worker's fan-out sync every mailbox even if one just ran.
|
|
11
|
+
*
|
|
12
|
+
* Everything else triggers a sync as a side effect of doing something else
|
|
13
|
+
* (config load, OAuth connect, account create, the scheduled tick), leaves
|
|
14
|
+
* this unset, and takes the freshness gate — see `mailboxNeedsSync` in
|
|
15
|
+
* imap-worker's sync-mailboxes handler.
|
|
16
|
+
*
|
|
17
|
+
* How often a timer may do that is bounded on the client, by the poll's own
|
|
18
|
+
* floor (`MIN_POLL_INTERVAL_MS` in the client hook, 30s) rather than by the
|
|
19
|
+
* gate: `mailboxPollIntervalSeconds` can lengthen the interval, never
|
|
20
|
+
* shorten it past that.
|
|
21
|
+
*/
|
|
22
|
+
explicitRequest?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Override the FIFO `MessageDeduplicationId`. Defaults to the event's own
|
|
25
|
+
* id, so the queue suppresses a re-send of one event and nothing else —
|
|
26
|
+
* every manual call site (POST /sync, OAuth connect, config load,
|
|
27
|
+
* pull-to-refresh, the client's online-poll) always enqueues. A shared,
|
|
28
|
+
* time-bucketed id instead turned SQS's 5-minute window into a rate limiter
|
|
29
|
+
* and silently discarded a sync the user asked for whenever one had run
|
|
30
|
+
* recently (issue #37). What a trigger costs is bounded in the worker's
|
|
31
|
+
* fan-out (`mailboxNeedsSync`), by skipping mailboxes not worth
|
|
32
|
+
* re-enumerating — never by dropping the trigger.
|
|
33
|
+
*
|
|
34
|
+
* The scheduled-sync tick (#1247) passes `buildScheduledSyncDedupId()`,
|
|
35
|
+
* which is bucketed by the tick's own cadence: there it collapses a
|
|
36
|
+
* re-invocation of a single tick, never two ticks or a manual trigger.
|
|
37
|
+
*/
|
|
38
|
+
dedupId?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the scheduler's own dedup namespace, bucketed by tick interval so
|
|
42
|
+
* consecutive ticks each get a fresh id (never colliding with each other)
|
|
43
|
+
* while still deduping a genuine double-invocation of the same tick (e.g. a
|
|
44
|
+
* retried Lambda) — see `dedupId` above.
|
|
45
|
+
*/
|
|
46
|
+
export declare const buildScheduledSyncDedupId: (accountId: string, now: number, bucketMs: number) => string;
|
|
47
|
+
export declare const buildSyncMailboxesCommand: (input: Omit<TriggerAccountSyncInput, "sqsClient">) => SendMessageCommand;
|
|
48
|
+
export declare const triggerAccountSync: (input: TriggerAccountSyncInput) => Promise<{
|
|
49
|
+
eventId: string;
|
|
50
|
+
}>;
|
|
51
|
+
export {};
|