@remit/mailbox-service 0.0.112 → 0.0.114
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-credentials.d.ts +83 -0
- package/dist/account-credentials.js +104 -0
- package/dist/attribute-mapper.d.ts +36 -0
- package/dist/attribute-mapper.js +119 -0
- package/dist/body-parse.d.ts +28 -0
- package/{src/body-parse.ts → dist/body-parse.js} +18 -26
- package/dist/body-part-mapper.d.ts +92 -0
- package/dist/body-part-mapper.js +301 -0
- package/dist/body-sync-queue.d.ts +59 -0
- package/dist/body-sync-queue.js +56 -0
- package/dist/body-sync-terminal.d.ts +58 -0
- package/dist/body-sync-terminal.js +95 -0
- package/dist/body-sync.d.ts +351 -0
- package/dist/body-sync.js +1204 -0
- package/dist/calendar-parts.d.ts +23 -0
- package/dist/calendar-parts.js +18 -0
- package/dist/classification-backfill.d.ts +91 -0
- package/dist/classification-backfill.js +180 -0
- package/dist/classify-message.d.ts +31 -0
- package/dist/classify-message.js +89 -0
- package/dist/connection-factory.d.ts +54 -0
- package/dist/connection-factory.js +54 -0
- package/dist/connection-test.d.ts +41 -0
- package/dist/connection-test.js +152 -0
- package/dist/external-move.d.ts +94 -0
- package/dist/external-move.js +173 -0
- package/dist/filter-operations.d.ts +42 -0
- package/dist/filter-operations.js +211 -0
- package/dist/filters/filter-config.d.ts +14 -0
- package/dist/filters/filter-config.js +10 -0
- package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
- package/dist/filters/list-id.js +26 -0
- package/dist/filters/match.d.ts +69 -0
- package/dist/filters/match.js +165 -0
- package/dist/filters/pipeline.d.ts +64 -0
- package/dist/filters/pipeline.js +100 -0
- package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +16 -72
- package/dist/flag-push-terminal.js +63 -0
- package/dist/flag-push.d.ts +69 -0
- package/dist/flag-push.js +111 -0
- package/dist/flag-queue.d.ts +175 -0
- package/dist/flag-queue.js +291 -0
- package/dist/folder-operations.d.ts +39 -0
- package/dist/folder-operations.js +112 -0
- package/dist/folder-placement.d.ts +13 -0
- package/dist/folder-placement.js +23 -0
- package/dist/heuristics/authenticationResults.d.ts +10 -0
- package/dist/heuristics/authenticationResults.js +187 -0
- package/dist/heuristics/classifyByHeaders.d.ts +82 -0
- package/dist/heuristics/classifyByHeaders.js +365 -0
- package/dist/heuristics/classifyPlacement.d.ts +67 -0
- package/dist/heuristics/classifyPlacement.js +153 -0
- package/dist/heuristics/machineSenders.d.ts +21 -0
- package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
- package/dist/heuristics/senderMismatch.d.ts +81 -0
- package/dist/heuristics/senderMismatch.js +192 -0
- package/dist/heuristics/socialDomains.d.ts +14 -0
- package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
- package/dist/heuristics/transactionalDomains.d.ts +18 -0
- package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
- package/dist/imapflow-connection.d.ts +377 -0
- package/dist/imapflow-connection.js +1147 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +65 -0
- package/dist/list-id-backfill.d.ts +83 -0
- package/dist/list-id-backfill.js +132 -0
- package/dist/mailbox-cursor-rebuild.d.ts +74 -0
- package/dist/mailbox-cursor-rebuild.js +71 -0
- package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
- package/dist/mailbox-cursor.js +102 -0
- package/dist/mailbox-intent.d.ts +41 -0
- package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +38 -64
- package/dist/mailbox-management.d.ts +240 -0
- package/dist/mailbox-management.js +576 -0
- package/dist/mailbox-presence.d.ts +52 -0
- package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
- package/dist/mailbox-queue.d.ts +129 -0
- package/dist/mailbox-queue.js +216 -0
- package/dist/mailbox-sync.d.ts +93 -0
- package/dist/mailbox-sync.js +490 -0
- package/dist/mailbox-upstream.d.ts +38 -0
- package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
- package/dist/message-category.d.ts +51 -0
- package/{src/message-category.ts → dist/message-category.js} +28 -71
- package/dist/message-move.d.ts +285 -0
- package/dist/message-move.js +858 -0
- package/dist/message-parser.d.ts +14 -0
- package/dist/message-parser.js +17 -0
- package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
- package/dist/message-presence.js +29 -0
- package/dist/message-read.d.ts +51 -0
- package/dist/message-read.js +31 -0
- package/dist/message-sync.d.ts +345 -0
- package/dist/message-sync.js +1358 -0
- package/dist/mime-walker.d.ts +102 -0
- package/dist/mime-walker.js +198 -0
- package/dist/organize-job.d.ts +20 -0
- package/dist/organize-job.js +37 -0
- package/dist/outbox-attachment-content.d.ts +37 -0
- package/dist/outbox-attachment-content.js +50 -0
- package/dist/outbox-attachment-filename.d.ts +19 -0
- package/dist/outbox-attachment-filename.js +64 -0
- package/dist/outbox-attachment.d.ts +148 -0
- package/dist/outbox-attachment.js +273 -0
- package/dist/outbox-queue.d.ts +73 -0
- package/dist/outbox-queue.js +280 -0
- package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
- package/dist/outbox-status.js +18 -0
- package/dist/ownership.d.ts +24 -0
- package/dist/ownership.js +83 -0
- package/dist/placement-move-terminal.d.ts +67 -0
- package/dist/placement-move-terminal.js +88 -0
- package/dist/placement-move.d.ts +83 -0
- package/dist/placement-move.js +216 -0
- package/{src/placement-settled.ts → dist/placement-settled.d.ts} +13 -68
- package/dist/placement-settled.js +75 -0
- package/dist/quarantine.d.ts +95 -0
- package/dist/quarantine.js +131 -0
- package/dist/refusal.d.ts +31 -0
- package/dist/refusal.js +3 -0
- package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -97
- package/dist/restore-source-placement.js +79 -0
- package/dist/search-text-backfill.d.ts +15 -0
- package/dist/search-text-backfill.js +33 -0
- package/dist/snippet.d.ts +61 -0
- package/dist/snippet.js +129 -0
- package/dist/spam-report.d.ts +86 -0
- package/dist/spam-report.js +241 -0
- package/dist/sync-warning.d.ts +21 -0
- package/dist/sync-warning.js +26 -0
- package/dist/sync-watermarks.d.ts +138 -0
- package/dist/sync-watermarks.js +205 -0
- package/dist/test-helpers/fake-imap.d.ts +151 -0
- package/dist/test-helpers/fake-imap.js +508 -0
- package/dist/test-helpers/folder-roles.d.ts +34 -0
- package/dist/test-helpers/folder-roles.js +47 -0
- package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
- package/dist/test-helpers/mime-tree-generator.js +329 -0
- package/dist/test-helpers/test-store.d.ts +69 -0
- package/dist/test-helpers/test-store.js +193 -0
- package/dist/text/normalizer.d.ts +20 -0
- package/dist/text/normalizer.js +102 -0
- package/dist/thread-message-rows.d.ts +69 -0
- package/dist/thread-message-rows.js +40 -0
- package/dist/thread-root.d.ts +7 -0
- package/dist/thread-root.js +25 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.js +19 -0
- package/package.json +86 -88
- package/src/account-credentials.ts +0 -210
- package/src/attribute-mapper.test.ts +0 -104
- package/src/attribute-mapper.ts +0 -148
- package/src/body-parse.test.ts +0 -55
- package/src/body-part-mapper.property.test.ts +0 -354
- package/src/body-part-mapper.ts +0 -410
- package/src/body-sync-queue.test.ts +0 -64
- package/src/body-sync-queue.ts +0 -131
- package/src/body-sync-terminal.test.ts +0 -148
- package/src/body-sync-terminal.ts +0 -174
- package/src/body-sync.test.ts +0 -925
- package/src/body-sync.ts +0 -1891
- package/src/calendar-parts.ts +0 -41
- package/src/classification-backfill.test.ts +0 -285
- package/src/classification-backfill.ts +0 -363
- package/src/classify-message.test.ts +0 -142
- package/src/classify-message.ts +0 -116
- package/src/connection-factory.ts +0 -102
- package/src/connection-test.ts +0 -208
- package/src/credentials.test.ts +0 -181
- package/src/external-move.test.ts +0 -262
- package/src/external-move.ts +0 -285
- package/src/filter-operations.test.ts +0 -319
- package/src/filter-operations.ts +0 -377
- package/src/filter-update.test.ts +0 -313
- package/src/filters/filter-config.test.ts +0 -87
- package/src/filters/filter-config.ts +0 -34
- package/src/filters/list-id.test.ts +0 -50
- package/src/filters/match.test.ts +0 -392
- package/src/filters/match.ts +0 -225
- package/src/filters/pipeline.test.ts +0 -199
- package/src/filters/pipeline.ts +0 -170
- package/src/flag-push.test.ts +0 -112
- package/src/flag-push.ts +0 -198
- package/src/flag-queue.test.ts +0 -197
- package/src/flag-queue.ts +0 -509
- package/src/folder-operations.test.ts +0 -273
- package/src/folder-operations.ts +0 -217
- package/src/folder-placement.ts +0 -56
- package/src/forwarded-message.test.ts +0 -122
- package/src/heuristics/authenticationResults.test.ts +0 -189
- package/src/heuristics/authenticationResults.ts +0 -212
- package/src/heuristics/classifyByHeaders.test.ts +0 -889
- package/src/heuristics/classifyByHeaders.ts +0 -467
- package/src/heuristics/classifyPlacement.test.ts +0 -366
- package/src/heuristics/classifyPlacement.ts +0 -211
- package/src/heuristics/classifyRealMail.test.ts +0 -242
- package/src/heuristics/machineSenders.test.ts +0 -101
- package/src/heuristics/senderMismatch.test.ts +0 -346
- package/src/heuristics/senderMismatch.ts +0 -259
- package/src/imapflow-connect-retry.test.ts +0 -97
- package/src/imapflow-connection.test.ts +0 -503
- package/src/imapflow-connection.ts +0 -1476
- package/src/index.ts +0 -397
- package/src/list-id-backfill.test.ts +0 -207
- package/src/list-id-backfill.ts +0 -274
- package/src/mailbox-cursor-rebuild.test.ts +0 -171
- package/src/mailbox-cursor-rebuild.ts +0 -146
- package/src/mailbox-cursor.test.ts +0 -141
- package/src/mailbox-delete-intent.test.ts +0 -119
- package/src/mailbox-management.test.ts +0 -243
- package/src/mailbox-management.ts +0 -838
- package/src/mailbox-presence.test.ts +0 -74
- package/src/mailbox-queue.ts +0 -397
- package/src/mailbox-rename-intent.test.ts +0 -220
- package/src/mailbox-rename-subtree.test.ts +0 -287
- package/src/mailbox-sync.test.ts +0 -286
- package/src/mailbox-sync.ts +0 -724
- package/src/message-move.test.ts +0 -726
- package/src/message-move.ts +0 -1518
- package/src/message-parser.ts +0 -26
- package/src/message-presence.test.ts +0 -50
- package/src/message-read.ts +0 -87
- package/src/message-save-retry.test.ts +0 -90
- package/src/message-sync-flags.test.ts +0 -167
- package/src/message-sync.test.ts +0 -762
- package/src/message-sync.ts +0 -2133
- package/src/mime-walker.test.ts +0 -311
- package/src/mime-walker.ts +0 -306
- package/src/organize-job.ts +0 -93
- package/src/outbox-attachment-content.test.ts +0 -153
- package/src/outbox-attachment-content.ts +0 -104
- package/src/outbox-attachment-filename.test.ts +0 -132
- package/src/outbox-attachment-filename.ts +0 -76
- package/src/outbox-attachment.test.ts +0 -626
- package/src/outbox-attachment.ts +0 -536
- package/src/outbox-failed-recovery.test.ts +0 -299
- package/src/outbox-queue.test.ts +0 -315
- package/src/outbox-queue.ts +0 -525
- package/src/ownership.ts +0 -140
- package/src/placement-move-terminal.ts +0 -145
- package/src/placement-move.ts +0 -363
- package/src/placement-settled.test.ts +0 -147
- package/src/placement.test.ts +0 -404
- package/src/quarantine.test.ts +0 -133
- package/src/quarantine.ts +0 -229
- package/src/refusal.ts +0 -29
- package/src/snippet.test.ts +0 -213
- package/src/snippet.ts +0 -152
- package/src/spam-report.test.ts +0 -329
- package/src/spam-report.ts +0 -357
- package/src/sync-warning.test.ts +0 -213
- package/src/sync-warning.ts +0 -56
- package/src/sync-watermarks.test.ts +0 -409
- package/src/sync-watermarks.ts +0 -319
- package/src/system-flag-wire-format.test.ts +0 -77
- package/src/test-helpers/fake-imap.ts +0 -690
- package/src/test-helpers/folder-roles.ts +0 -66
- package/src/test-helpers/mime-tree-generator.ts +0 -461
- package/src/test-helpers/test-queue.ts +0 -56
- package/src/test-helpers/test-store.ts +0 -299
- package/src/text/normalizer.test.ts +0 -223
- package/src/text/normalizer.ts +0 -140
- package/src/thread-message-rows.ts +0 -77
- package/src/thread-root.test.ts +0 -29
- package/src/thread-root.ts +0 -51
- package/src/types/mailparser-augment.d.ts +0 -23
- package/src/types/natural-porter-stemmer.d.ts +0 -50
- package/src/types/stopword.d.ts +0 -69
- package/src/types.ts +0 -371
- package/tsconfig.json +0 -8
package/src/filters/match.ts
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FILTER_NO_ACTION,
|
|
3
|
-
type FilterItem,
|
|
4
|
-
type ThreadMessageFieldTerm,
|
|
5
|
-
} from "@remit/data-ports";
|
|
6
|
-
import { FilterClauseField, FilterMatchOperator } from "@remit/domain-enums";
|
|
7
|
-
import { getDomain } from "tldts";
|
|
8
|
-
import { normalizeListId } from "./list-id.js";
|
|
9
|
-
|
|
10
|
-
type FilterClause = FilterItem["literalClauses"][number];
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The `"None"` sentinel a filter's action fields carry when that action is
|
|
14
|
-
* absent (RFC 034 Decision 3.1) — `actionLabelId`/`actionMailboxId` are never
|
|
15
|
-
* empty/optional strings, so a missing action is this exact value, never `""`.
|
|
16
|
-
*/
|
|
17
|
-
export const NO_ACTION = FILTER_NO_ACTION;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The message fields a filter evaluates against — the literal-clause targets
|
|
21
|
-
* (from / subject / body / list). A plain value object so the matcher stays independent of the `ParsedMail` shape and is
|
|
22
|
-
* trivially constructed in a test.
|
|
23
|
-
*/
|
|
24
|
-
export interface FilterMessage {
|
|
25
|
-
from: string;
|
|
26
|
-
fromName: string;
|
|
27
|
-
subject: string;
|
|
28
|
-
text: string;
|
|
29
|
-
/** Normalized `List-Id` header value (see `normalizeListId`); `""` when absent. */
|
|
30
|
-
listId: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const includesFold = (haystack: string, needle: string): boolean =>
|
|
34
|
-
haystack.toLowerCase().includes(needle.toLowerCase());
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Whether every whitespace-separated word of `value` appears somewhere in the
|
|
38
|
-
* sender address, sender name, subject or body — the shape free-text search
|
|
39
|
-
* matches with (`buildSearchConditions`), so a filter made from a search keeps
|
|
40
|
-
* the mail that search showed.
|
|
41
|
-
*/
|
|
42
|
-
const hasEveryWord = (value: string, msg: FilterMessage): boolean => {
|
|
43
|
-
const fields = [msg.from, msg.fromName, msg.subject, msg.text];
|
|
44
|
-
return value
|
|
45
|
-
.split(/\s+/)
|
|
46
|
-
.filter(Boolean)
|
|
47
|
-
.every((word) => fields.some((field) => includesFold(field, word)));
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const hostOf = (address: string): string => {
|
|
51
|
-
const at = address.lastIndexOf("@");
|
|
52
|
-
return at >= 0 ? address.slice(at + 1) : address;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* The registrable, public-suffix-aware domain of an email address or host, or
|
|
57
|
-
* `null` when none resolves. `getDomain` folds case and applies the ICANN
|
|
58
|
-
* suffix list, so `github.com.evil.example` yields `evil.example`, never
|
|
59
|
-
* `github.com` — a `FromDomain` clause cannot be spoofed by a crafted subdomain.
|
|
60
|
-
*/
|
|
61
|
-
const registrableDomain = (addressOrHost: string): string | null =>
|
|
62
|
-
getDomain(hostOf(addressOrHost.trim()));
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Whether one literal clause matches the message. From matches against the
|
|
66
|
-
* sender address and display name; Subject against the subject; HasWords
|
|
67
|
-
* requires every word somewhere in sender, subject or body; ListId against the exact normalized `List-Id`; FromDomain
|
|
68
|
-
* against the sender's registrable domain. An empty clause value never matches.
|
|
69
|
-
*/
|
|
70
|
-
export const clauseMatches = (
|
|
71
|
-
clause: FilterClause,
|
|
72
|
-
msg: FilterMessage,
|
|
73
|
-
): boolean => {
|
|
74
|
-
const value = clause.value.trim();
|
|
75
|
-
if (value === "") return false;
|
|
76
|
-
switch (clause.field) {
|
|
77
|
-
case FilterClauseField.From:
|
|
78
|
-
return includesFold(msg.from, value) || includesFold(msg.fromName, value);
|
|
79
|
-
case FilterClauseField.Subject:
|
|
80
|
-
return includesFold(msg.subject, value);
|
|
81
|
-
case FilterClauseField.HasWords:
|
|
82
|
-
return hasEveryWord(value, msg);
|
|
83
|
-
case FilterClauseField.ListId: {
|
|
84
|
-
const target = normalizeListId(value);
|
|
85
|
-
return target !== "" && normalizeListId(msg.listId) === target;
|
|
86
|
-
}
|
|
87
|
-
case FilterClauseField.FromDomain: {
|
|
88
|
-
const target = registrableDomain(value);
|
|
89
|
-
if (target === null) return false;
|
|
90
|
-
return registrableDomain(msg.from) === target;
|
|
91
|
-
}
|
|
92
|
-
default:
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Whether the literal clauses match under their combine operator. Empty clauses
|
|
99
|
-
* carry no literal constraint and pass vacuously; a caller that needs a
|
|
100
|
-
* predicate checks for clauses itself.
|
|
101
|
-
*/
|
|
102
|
-
export const literalClausesMatch = (
|
|
103
|
-
clauses: readonly FilterClause[],
|
|
104
|
-
operator: FilterItem["matchOperator"],
|
|
105
|
-
msg: FilterMessage,
|
|
106
|
-
): boolean => {
|
|
107
|
-
if (clauses.length === 0) return true;
|
|
108
|
-
return operator === FilterMatchOperator.Or
|
|
109
|
-
? clauses.some((clause) => clauseMatches(clause, msg))
|
|
110
|
-
: clauses.every((clause) => clauseMatches(clause, msg));
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* What a store can be asked for on behalf of one clause: a column term, or the
|
|
115
|
-
* two answers a term cannot express — a clause nothing can satisfy, and a
|
|
116
|
-
* clause selecting on something no ThreadMessage column carries.
|
|
117
|
-
*/
|
|
118
|
-
type ClauseNarrowing =
|
|
119
|
-
| { kind: "Term"; term: ThreadMessageFieldTerm }
|
|
120
|
-
| { kind: "NeverMatches" }
|
|
121
|
-
| { kind: "Unnarrowable" };
|
|
122
|
-
|
|
123
|
-
const clauseNarrowing = (clause: FilterClause): ClauseNarrowing => {
|
|
124
|
-
const value = clause.value.trim();
|
|
125
|
-
if (value === "") return { kind: "NeverMatches" };
|
|
126
|
-
switch (clause.field) {
|
|
127
|
-
case FilterClauseField.From:
|
|
128
|
-
return { kind: "Term", term: { field: "sender", contains: value } };
|
|
129
|
-
case FilterClauseField.Subject:
|
|
130
|
-
return { kind: "Term", term: { field: "subject", contains: value } };
|
|
131
|
-
case FilterClauseField.ListId: {
|
|
132
|
-
const target = normalizeListId(value);
|
|
133
|
-
if (target === "") return { kind: "NeverMatches" };
|
|
134
|
-
return { kind: "Term", term: { field: "listId", contains: target } };
|
|
135
|
-
}
|
|
136
|
-
case FilterClauseField.FromDomain: {
|
|
137
|
-
const target = registrableDomain(value);
|
|
138
|
-
if (target === null) return { kind: "NeverMatches" };
|
|
139
|
-
return { kind: "Term", term: { field: "sender", contains: target } };
|
|
140
|
-
}
|
|
141
|
-
default:
|
|
142
|
-
return { kind: "Unnarrowable" };
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* The terms to query with, and how to combine them. Empty terms narrow nothing
|
|
148
|
-
* — the caller reads the whole corpus and refines it.
|
|
149
|
-
*/
|
|
150
|
-
export interface LiteralClauseNarrowing {
|
|
151
|
-
terms: ThreadMessageFieldTerm[];
|
|
152
|
-
operator: "and" | "or";
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* The store-side half of {@link literalClausesMatch}: the column terms a query
|
|
157
|
-
* can evaluate on behalf of these clauses, or `null` when they can match
|
|
158
|
-
* nothing at all and there is no query worth running.
|
|
159
|
-
*
|
|
160
|
-
* A term is deliberately WIDER than the clause it stands for — a `FromDomain`
|
|
161
|
-
* clause narrows to the registrable domain appearing anywhere in the sender,
|
|
162
|
-
* so `github.com.evil.example` still comes back — and a `HasWords` clause
|
|
163
|
-
* cannot be narrowed at all, since no row column carries the body. The rows a
|
|
164
|
-
* term selects are candidates; `literalClausesMatch` still decides. Pushing the
|
|
165
|
-
* terms down is what makes the caller's bound a bound on RESULTS: filtering a
|
|
166
|
-
* date-ordered page instead answers "matches among the newest N messages"
|
|
167
|
-
* (#459).
|
|
168
|
-
*/
|
|
169
|
-
export const literalClauseTerms = (
|
|
170
|
-
clauses: readonly FilterClause[],
|
|
171
|
-
operator: FilterItem["matchOperator"],
|
|
172
|
-
): LiteralClauseNarrowing | null => {
|
|
173
|
-
const combine = operator === FilterMatchOperator.Or ? "or" : "and";
|
|
174
|
-
if (clauses.length === 0) return { terms: [], operator: combine };
|
|
175
|
-
const narrowings = clauses.map(clauseNarrowing);
|
|
176
|
-
const terms = narrowings.flatMap((narrowing) =>
|
|
177
|
-
narrowing.kind === "Term" ? [narrowing.term] : [],
|
|
178
|
-
);
|
|
179
|
-
if (combine === "and") {
|
|
180
|
-
if (narrowings.some((narrowing) => narrowing.kind === "NeverMatches")) {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
return { terms, operator: "and" };
|
|
184
|
-
}
|
|
185
|
-
if (narrowings.every((narrowing) => narrowing.kind === "NeverMatches")) {
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
if (narrowings.some((narrowing) => narrowing.kind === "Unnarrowable")) {
|
|
189
|
-
return { terms: [], operator: "or" };
|
|
190
|
-
}
|
|
191
|
-
return { terms, operator: "or" };
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* The move a message ends in when several filters matched: the filter whose
|
|
196
|
-
* predicate or action was most recently *changed* wins (RFC 034 Decision 3.2),
|
|
197
|
-
* tie-broken on `filterId` for the unreachable identical-timestamp case.
|
|
198
|
-
* `actionChangedAt` — not `ruleChangedAt` — is the signal: `ruleChangedAt` also
|
|
199
|
-
* bumps on a scope/expiry-only edit (reader #266), which changes a filter's
|
|
200
|
-
* lifecycle, not what it matches or does, and must not reorder exclusive-move
|
|
201
|
-
* precedence (reader #384). Nor is it `updatedAt`, so a cosmetic rename never
|
|
202
|
-
* flips an exclusive move either.
|
|
203
|
-
*/
|
|
204
|
-
export const selectMoveWinner = (
|
|
205
|
-
candidates: readonly FilterItem[],
|
|
206
|
-
): FilterItem | undefined => {
|
|
207
|
-
let winner: FilterItem | undefined;
|
|
208
|
-
for (const candidate of candidates) {
|
|
209
|
-
if (!winner) {
|
|
210
|
-
winner = candidate;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
if (candidate.actionChangedAt > winner.actionChangedAt) {
|
|
214
|
-
winner = candidate;
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
if (
|
|
218
|
-
candidate.actionChangedAt === winner.actionChangedAt &&
|
|
219
|
-
candidate.filterId > winner.filterId
|
|
220
|
-
) {
|
|
221
|
-
winner = candidate;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return winner;
|
|
225
|
-
};
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, beforeEach, describe, it, mock } from "node:test";
|
|
3
|
-
import type { AccountItem, CreateFilterInput } from "@remit/data-ports";
|
|
4
|
-
import {
|
|
5
|
-
AccountAuthType,
|
|
6
|
-
ConnectionState,
|
|
7
|
-
FilterClauseField,
|
|
8
|
-
FilterMatchOperator,
|
|
9
|
-
FilterScope,
|
|
10
|
-
} from "@remit/domain-enums";
|
|
11
|
-
import { PlacementMoveService } from "../placement-move.js";
|
|
12
|
-
import {
|
|
13
|
-
openStore,
|
|
14
|
-
type Store,
|
|
15
|
-
seedAccount,
|
|
16
|
-
seedMailbox,
|
|
17
|
-
} from "../test-helpers/test-store.js";
|
|
18
|
-
import type { FilterMessage } from "./match.js";
|
|
19
|
-
import { FilterPipeline } from "./pipeline.js";
|
|
20
|
-
|
|
21
|
-
const ACCOUNT = "cfg-1";
|
|
22
|
-
|
|
23
|
-
let store: Store;
|
|
24
|
-
let account: AccountItem;
|
|
25
|
-
let archive: string;
|
|
26
|
-
let projects: string;
|
|
27
|
-
|
|
28
|
-
beforeEach(async () => {
|
|
29
|
-
store = await openStore();
|
|
30
|
-
account = await seedAccount(store, { accountConfigId: ACCOUNT });
|
|
31
|
-
archive = (await seedMailbox(store, account, "Archive")).mailboxId;
|
|
32
|
-
projects = (await seedMailbox(store, account, "Projects")).mailboxId;
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterEach(async () => {
|
|
36
|
-
mock.timers.reset();
|
|
37
|
-
await store.close();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const fromGithub: FilterMessage = {
|
|
41
|
-
from: "npm@github.com",
|
|
42
|
-
fromName: "npm",
|
|
43
|
-
subject: "A new version of left-pad is available",
|
|
44
|
-
text: "body",
|
|
45
|
-
listId: "",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const receipt: FilterMessage = {
|
|
49
|
-
from: "billing@stripe.com",
|
|
50
|
-
fromName: "Stripe",
|
|
51
|
-
subject: "Your receipt",
|
|
52
|
-
text: "Thanks for your payment",
|
|
53
|
-
listId: "",
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const senders = (actionMailboxId: string) =>
|
|
57
|
-
store.filter.create({
|
|
58
|
-
accountConfigId: ACCOUNT,
|
|
59
|
-
name: "rule",
|
|
60
|
-
scope: FilterScope.Standing,
|
|
61
|
-
matchOperator: FilterMatchOperator.Or,
|
|
62
|
-
literalClauses: [
|
|
63
|
-
{ field: FilterClauseField.From, value: "npm@github.com" },
|
|
64
|
-
{ field: FilterClauseField.From, value: "notifications@github.com" },
|
|
65
|
-
],
|
|
66
|
-
actionMailboxId,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const pipeline = () =>
|
|
70
|
-
new FilterPipeline(
|
|
71
|
-
{
|
|
72
|
-
filterService: store.filter,
|
|
73
|
-
messageLabelService: store.messageLabel,
|
|
74
|
-
mailboxService: store.mailbox,
|
|
75
|
-
placementMoveService: new PlacementMoveService({
|
|
76
|
-
messageService: store.message,
|
|
77
|
-
threadMessageService: store.threadMessage,
|
|
78
|
-
markerService: store.placementMove,
|
|
79
|
-
addressService: store.address,
|
|
80
|
-
mailboxSpecialUseService: store.mailboxSpecialUse,
|
|
81
|
-
mailboxService: store.mailbox,
|
|
82
|
-
sqsQueueUrl: "https://sqs.test/message-mgmt",
|
|
83
|
-
}),
|
|
84
|
-
},
|
|
85
|
-
{ info: () => {}, error: () => {} },
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
describe("FilterPipeline over literal filters", () => {
|
|
89
|
-
it("resolves the move for a matching sender", async () => {
|
|
90
|
-
const filter = await senders(archive);
|
|
91
|
-
|
|
92
|
-
const decision = await pipeline().evaluate(
|
|
93
|
-
ACCOUNT,
|
|
94
|
-
account.accountId,
|
|
95
|
-
"m-1",
|
|
96
|
-
fromGithub,
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
assert.deepEqual(decision.move, {
|
|
100
|
-
destinationMailboxId: archive,
|
|
101
|
-
filterId: filter.filterId,
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("resolves nothing for a sender no clause names", async () => {
|
|
106
|
-
await senders(archive);
|
|
107
|
-
|
|
108
|
-
const decision = await pipeline().evaluate(
|
|
109
|
-
ACCOUNT,
|
|
110
|
-
account.accountId,
|
|
111
|
-
"m-2",
|
|
112
|
-
{
|
|
113
|
-
...fromGithub,
|
|
114
|
-
from: "hello@stripe.com",
|
|
115
|
-
},
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
assert.equal(decision.move, undefined);
|
|
119
|
-
assert.deepEqual(decision.labels, []);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("gives a contested message to the more recently changed action", async () => {
|
|
123
|
-
mock.timers.enable({ apis: ["Date"], now: 1_000_000 });
|
|
124
|
-
await senders(archive);
|
|
125
|
-
mock.timers.setTime(9_000_000);
|
|
126
|
-
const rival = await senders(projects);
|
|
127
|
-
|
|
128
|
-
const decision = await pipeline().evaluate(
|
|
129
|
-
ACCOUNT,
|
|
130
|
-
account.accountId,
|
|
131
|
-
"m-3",
|
|
132
|
-
fromGithub,
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
assert.deepEqual(decision.move, {
|
|
136
|
-
destinationMailboxId: projects,
|
|
137
|
-
filterId: rival.filterId,
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe("FilterPipeline scopes a filter to its folder's account", () => {
|
|
143
|
-
it("leaves another account's mail alone, as if the filter had not matched", async () => {
|
|
144
|
-
const sibling = await store.account.create({
|
|
145
|
-
accountConfigId: ACCOUNT,
|
|
146
|
-
username: "me-too@example.com",
|
|
147
|
-
email: "me-too@example.com",
|
|
148
|
-
authType: AccountAuthType.Password,
|
|
149
|
-
passwordHash: '{"ciphertext":"x"}',
|
|
150
|
-
imapHost: "imap.example.com",
|
|
151
|
-
imapPort: 993,
|
|
152
|
-
imapTls: true,
|
|
153
|
-
imapStartTls: false,
|
|
154
|
-
isActive: true,
|
|
155
|
-
connectionState: ConnectionState.Authenticated,
|
|
156
|
-
});
|
|
157
|
-
const travel = await seedMailbox(store, sibling, "Travel");
|
|
158
|
-
const filter = await senders(travel.mailboxId);
|
|
159
|
-
|
|
160
|
-
const mine = await pipeline().evaluate(
|
|
161
|
-
ACCOUNT,
|
|
162
|
-
account.accountId,
|
|
163
|
-
"m-1",
|
|
164
|
-
fromGithub,
|
|
165
|
-
);
|
|
166
|
-
const theirs = await pipeline().evaluate(
|
|
167
|
-
ACCOUNT,
|
|
168
|
-
sibling.accountId,
|
|
169
|
-
"m-2",
|
|
170
|
-
fromGithub,
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
assert.deepEqual(mine, { labels: [] });
|
|
174
|
-
assert.deepEqual(theirs.move, {
|
|
175
|
-
destinationMailboxId: travel.mailboxId,
|
|
176
|
-
filterId: filter.filterId,
|
|
177
|
-
});
|
|
178
|
-
assert.equal(
|
|
179
|
-
(await store.filter.get(ACCOUNT, filter.filterId)).state,
|
|
180
|
-
"Active",
|
|
181
|
-
);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("turns the filter off when no account holds its folder any more", async () => {
|
|
185
|
-
const filter = await senders("mbx-gone");
|
|
186
|
-
|
|
187
|
-
const decision = await pipeline().evaluate(
|
|
188
|
-
ACCOUNT,
|
|
189
|
-
account.accountId,
|
|
190
|
-
"m-1",
|
|
191
|
-
fromGithub,
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
assert.deepEqual(decision, { labels: [] });
|
|
195
|
-
const stored = await store.filter.get(ACCOUNT, filter.filterId);
|
|
196
|
-
assert.equal(stored.state, "Disabled");
|
|
197
|
-
assert.equal(stored.disabledReason, "FolderMissing");
|
|
198
|
-
});
|
|
199
|
-
});
|
package/src/filters/pipeline.ts
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { inspect } from "node:util";
|
|
2
|
-
import type {
|
|
3
|
-
FilterItem,
|
|
4
|
-
IFilterRepository,
|
|
5
|
-
IMailboxRepository,
|
|
6
|
-
IMessageLabelRepository,
|
|
7
|
-
} from "@remit/data-ports";
|
|
8
|
-
import { FilterDisabledReason, FilterState } from "@remit/domain-enums";
|
|
9
|
-
import type { PlacementMoveService } from "../placement-move.js";
|
|
10
|
-
import {
|
|
11
|
-
type FilterMessage,
|
|
12
|
-
literalClausesMatch,
|
|
13
|
-
NO_ACTION,
|
|
14
|
-
selectMoveWinner,
|
|
15
|
-
} from "./match.js";
|
|
16
|
-
|
|
17
|
-
export interface FilterLogger {
|
|
18
|
-
info(obj: Record<string, unknown>, msg: string): void;
|
|
19
|
-
debug?(obj: Record<string, unknown>, msg: string): void;
|
|
20
|
-
warn?(obj: Record<string, unknown>, msg: string): void;
|
|
21
|
-
error?(obj: Record<string, unknown>, msg: string): void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Dependencies for the index-time filter pass (RFC 034). Optional on
|
|
26
|
-
* {@link BodySyncService} the same way {@link PlacementConfig} is — a deployment
|
|
27
|
-
* without it simply runs no filter matching.
|
|
28
|
-
*/
|
|
29
|
-
export interface FilterConfig {
|
|
30
|
-
filterService: IFilterRepository;
|
|
31
|
-
messageLabelService: IMessageLabelRepository;
|
|
32
|
-
/** Names the account a filter's folder belongs to. */
|
|
33
|
-
mailboxService: Pick<IMailboxRepository, "resolveAccountId">;
|
|
34
|
-
placementMoveService: PlacementMoveService;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* What the matched filters resolve to for one message: every matching filter's
|
|
39
|
-
* label (additive — RFC 034 Decision 3.1), and at most one move (exclusive — the
|
|
40
|
-
* most-recently-changed matching filter, Decision 3.2).
|
|
41
|
-
*/
|
|
42
|
-
export interface FilterDecision {
|
|
43
|
-
labels: Array<{ labelId: string; filterId: string }>;
|
|
44
|
-
move?: { destinationMailboxId: string; filterId: string };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const EMPTY_DECISION: FilterDecision = { labels: [] };
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Evaluates a synced message against the account's active filters and resolves
|
|
51
|
-
* the actions to apply (RFC 034). Reads only — listing filters and refreshing
|
|
52
|
-
* lazy expiry; the caller applies the
|
|
53
|
-
* resulting {@link FilterDecision}.
|
|
54
|
-
*
|
|
55
|
-
* `evaluate` isolates every failure in this read/decision phase from body sync,
|
|
56
|
-
* matching the placement-resolution precedent (#1246): a schema-drifted query
|
|
57
|
-
* must never fail the surrounding message store, since the body is
|
|
58
|
-
* already durable by the time this runs. The apply of the decision — label
|
|
59
|
-
* writes and the move enqueue — is the caller's responsibility and is *not*
|
|
60
|
-
* swallowed there, so a genuine DDB/SQS write failure still propagates and
|
|
61
|
-
* requeues the message.
|
|
62
|
-
*/
|
|
63
|
-
export class FilterPipeline {
|
|
64
|
-
constructor(
|
|
65
|
-
private readonly config: FilterConfig,
|
|
66
|
-
private readonly log: FilterLogger,
|
|
67
|
-
) {}
|
|
68
|
-
|
|
69
|
-
async evaluate(
|
|
70
|
-
accountConfigId: string,
|
|
71
|
-
accountId: string,
|
|
72
|
-
messageId: string,
|
|
73
|
-
msg: FilterMessage,
|
|
74
|
-
): Promise<FilterDecision> {
|
|
75
|
-
return this.match(accountConfigId, accountId, msg).catch(
|
|
76
|
-
(error: unknown) => {
|
|
77
|
-
this.log.error?.(
|
|
78
|
-
{
|
|
79
|
-
alert: "body_sync_filter_match_failed",
|
|
80
|
-
messageId,
|
|
81
|
-
accountConfigId,
|
|
82
|
-
errorName: (error as { name?: string })?.name,
|
|
83
|
-
error: inspect(error),
|
|
84
|
-
},
|
|
85
|
-
"Filter matching failed; body already stored, continuing without filter actions (best-effort, non-fatal)",
|
|
86
|
-
);
|
|
87
|
-
return EMPTY_DECISION;
|
|
88
|
-
},
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* A filter sits on the account config and sees every account's mail, but its
|
|
94
|
-
* folder is one account's: mail of another account is not this filter's to
|
|
95
|
-
* act on, exactly as if it had not matched. A folder no account holds any
|
|
96
|
-
* more turns the filter off, with the reason the filter list shows.
|
|
97
|
-
*/
|
|
98
|
-
private async appliesTo(
|
|
99
|
-
accountConfigId: string,
|
|
100
|
-
accountId: string,
|
|
101
|
-
filter: FilterItem,
|
|
102
|
-
): Promise<boolean> {
|
|
103
|
-
if (filter.actionMailboxId === NO_ACTION) return true;
|
|
104
|
-
const owner = await this.config.mailboxService.resolveAccountId(
|
|
105
|
-
filter.actionMailboxId,
|
|
106
|
-
);
|
|
107
|
-
if (owner !== null) return owner === accountId;
|
|
108
|
-
await this.config.filterService.update(accountConfigId, filter.filterId, {
|
|
109
|
-
state: FilterState.Disabled,
|
|
110
|
-
disabledReason: FilterDisabledReason.FolderMissing,
|
|
111
|
-
});
|
|
112
|
-
this.log.warn?.(
|
|
113
|
-
{ filterId: filter.filterId, accountConfigId },
|
|
114
|
-
"Filter turned off: the folder it moves mail into no longer exists",
|
|
115
|
-
);
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private async match(
|
|
120
|
-
accountConfigId: string,
|
|
121
|
-
accountId: string,
|
|
122
|
-
msg: FilterMessage,
|
|
123
|
-
): Promise<FilterDecision> {
|
|
124
|
-
const active = await this.config.filterService.listByAccountAndState(
|
|
125
|
-
accountConfigId,
|
|
126
|
-
FilterState.Active,
|
|
127
|
-
);
|
|
128
|
-
if (active.length === 0) return EMPTY_DECISION;
|
|
129
|
-
|
|
130
|
-
const matched: FilterItem[] = [];
|
|
131
|
-
for (const filter of active) {
|
|
132
|
-
// Lazy expiry (RFC 034 Decision 1.2): reading a Temporary filter past
|
|
133
|
-
// its expiresAt patches it to Expired on this read and drops it from
|
|
134
|
-
// evaluation, independent of whether the TTL delete has run.
|
|
135
|
-
const usable = await this.config.filterService.refreshExpiry(filter);
|
|
136
|
-
if (usable.state !== FilterState.Active) continue;
|
|
137
|
-
if (!(await this.appliesTo(accountConfigId, accountId, usable))) continue;
|
|
138
|
-
if (filterMatches(usable, msg)) matched.push(usable);
|
|
139
|
-
}
|
|
140
|
-
if (matched.length === 0) return EMPTY_DECISION;
|
|
141
|
-
|
|
142
|
-
const labels: FilterDecision["labels"] = [];
|
|
143
|
-
const seenLabels = new Set<string>();
|
|
144
|
-
for (const filter of matched) {
|
|
145
|
-
if (filter.actionLabelId === NO_ACTION) continue;
|
|
146
|
-
if (seenLabels.has(filter.actionLabelId)) continue;
|
|
147
|
-
seenLabels.add(filter.actionLabelId);
|
|
148
|
-
labels.push({ labelId: filter.actionLabelId, filterId: filter.filterId });
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const winner = selectMoveWinner(
|
|
152
|
-
matched.filter((filter) => filter.actionMailboxId !== NO_ACTION),
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
labels,
|
|
157
|
-
...(winner
|
|
158
|
-
? {
|
|
159
|
-
move: {
|
|
160
|
-
destinationMailboxId: winner.actionMailboxId,
|
|
161
|
-
filterId: winner.filterId,
|
|
162
|
-
},
|
|
163
|
-
}
|
|
164
|
-
: {}),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const filterMatches = (filter: FilterItem, msg: FilterMessage): boolean =>
|
|
170
|
-
literalClausesMatch(filter.literalClauses, filter.matchOperator, msg);
|
package/src/flag-push.test.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
-
import { MessageSystemFlag } from "@remit/domain-enums";
|
|
4
|
-
import { FlagPushService } from "./flag-push.js";
|
|
5
|
-
import {
|
|
6
|
-
captureQueue,
|
|
7
|
-
openStore,
|
|
8
|
-
type QueueCapture,
|
|
9
|
-
type Store,
|
|
10
|
-
} from "./test-helpers/test-store.js";
|
|
11
|
-
|
|
12
|
-
const target = {
|
|
13
|
-
accountId: "acc-1",
|
|
14
|
-
accountConfigId: "cfg-1",
|
|
15
|
-
messageId: "msg-1",
|
|
16
|
-
mailboxId: "mbx-1",
|
|
17
|
-
flagName: MessageSystemFlag.Seen,
|
|
18
|
-
operation: "add" as const,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
let store: Store;
|
|
22
|
-
let capture: QueueCapture;
|
|
23
|
-
let errors: Array<Record<string, unknown>>;
|
|
24
|
-
|
|
25
|
-
beforeEach(async () => {
|
|
26
|
-
store = await openStore();
|
|
27
|
-
capture = captureQueue();
|
|
28
|
-
errors = [];
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
afterEach(async () => {
|
|
32
|
-
capture.restore();
|
|
33
|
-
await store.close();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const queueAnswers = (answer: () => Promise<unknown>) => capture.answer(answer);
|
|
37
|
-
|
|
38
|
-
const flips = (queueUrl = "http://localhost:9324/000/remit-flags") =>
|
|
39
|
-
new FlagPushService({
|
|
40
|
-
markerService: store.flagPush,
|
|
41
|
-
sqsQueueUrl: queueUrl,
|
|
42
|
-
logger: {
|
|
43
|
-
info: () => {},
|
|
44
|
-
error: (fields: Record<string, unknown>) => errors.push(fields),
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const marker = () => store.flagPush.find(target.messageId, target.flagName);
|
|
49
|
-
|
|
50
|
-
describe("FlagPushService.flip", () => {
|
|
51
|
-
it("records the owed push and marks it queued once the hint is sent", async () => {
|
|
52
|
-
queueAnswers(async () => ({}));
|
|
53
|
-
|
|
54
|
-
await flips().flip(target);
|
|
55
|
-
|
|
56
|
-
const recorded = await marker();
|
|
57
|
-
assert.equal(recorded?.operation, "add");
|
|
58
|
-
assert.equal(recorded?.mailboxId, target.mailboxId);
|
|
59
|
-
assert.equal(recorded?.state, "queued");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("names the message and flag in the hint, never the operation or a uid", async () => {
|
|
63
|
-
queueAnswers(async () => ({}));
|
|
64
|
-
|
|
65
|
-
await flips().flip(target);
|
|
66
|
-
|
|
67
|
-
const event = JSON.parse(capture.inputs()[0].MessageBody ?? "{}");
|
|
68
|
-
assert.equal(event.type, "FLAG_PUSH");
|
|
69
|
-
assert.equal(event.messageId, target.messageId);
|
|
70
|
-
assert.equal(event.flagName, target.flagName);
|
|
71
|
-
assert.equal("operation" in event, false);
|
|
72
|
-
assert.equal("uid" in event, false);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("groups a FIFO hint by account and leaves a standard queue ungrouped", async () => {
|
|
76
|
-
queueAnswers(async () => ({}));
|
|
77
|
-
|
|
78
|
-
await flips("http://localhost:9324/000/remit-flags.fifo").flip(target);
|
|
79
|
-
await flips("http://localhost:9324/000/remit-flags").flip(target);
|
|
80
|
-
|
|
81
|
-
assert.equal(capture.inputs()[0].MessageGroupId, target.accountId);
|
|
82
|
-
assert.equal(capture.inputs()[1].MessageGroupId, undefined);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("keeps the push owed and alarms, without rejecting, when the queue is down", async () => {
|
|
86
|
-
queueAnswers(async () => {
|
|
87
|
-
throw Object.assign(new Error("connect ECONNREFUSED"), {
|
|
88
|
-
code: "ECONNREFUSED",
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
await flips().flip(target);
|
|
93
|
-
|
|
94
|
-
assert.equal((await marker())?.state, "pending");
|
|
95
|
-
const alarm = errors.find(
|
|
96
|
-
(entry) => entry.alert === "flag_push_hint_enqueue_failed",
|
|
97
|
-
);
|
|
98
|
-
assert.equal(alarm?.errorCode, "ECONNREFUSED");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("raises no alarm when the worker drained the push before it was marked queued", async () => {
|
|
102
|
-
queueAnswers(async () => {
|
|
103
|
-
await store.flagPush.delete(target.messageId, target.flagName);
|
|
104
|
-
return {};
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
await flips().flip(target);
|
|
108
|
-
|
|
109
|
-
assert.equal(await marker(), null);
|
|
110
|
-
assert.deepEqual(errors, []);
|
|
111
|
-
});
|
|
112
|
-
});
|