@postalsys/email-content 0.0.5 → 0.1.1
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/README.md +205 -17
- package/dist/.tsbuildinfo +1 -1
- package/dist/analyze.d.ts +55 -15
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +275 -61
- package/dist/analyze.js.map +1 -1
- package/dist/canonical/html.d.ts +21 -2
- package/dist/canonical/html.d.ts.map +1 -1
- package/dist/canonical/html.js +213 -14
- package/dist/canonical/html.js.map +1 -1
- package/dist/classify/confidence.d.ts +12 -0
- package/dist/classify/confidence.d.ts.map +1 -1
- package/dist/classify/confidence.js +54 -4
- package/dist/classify/confidence.js.map +1 -1
- package/dist/classify/reconcile.d.ts +25 -17
- package/dist/classify/reconcile.d.ts.map +1 -1
- package/dist/classify/reconcile.js +52 -20
- package/dist/classify/reconcile.js.map +1 -1
- package/dist/classify/rules.d.ts +7 -0
- package/dist/classify/rules.d.ts.map +1 -1
- package/dist/classify/rules.js +58 -0
- package/dist/classify/rules.js.map +1 -1
- package/dist/cli/annotate.d.ts.map +1 -1
- package/dist/cli/annotate.js +6 -3
- package/dist/cli/annotate.js.map +1 -1
- package/dist/cli/args.d.ts +14 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +48 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/benchmark.d.ts +39 -0
- package/dist/cli/benchmark.d.ts.map +1 -1
- package/dist/cli/benchmark.js +179 -66
- package/dist/cli/benchmark.js.map +1 -1
- package/dist/cli/corpus.d.ts.map +1 -1
- package/dist/cli/corpus.js +13 -8
- package/dist/cli/corpus.js.map +1 -1
- package/dist/cli/disagreements.d.ts.map +1 -1
- package/dist/cli/disagreements.js +7 -4
- package/dist/cli/disagreements.js.map +1 -1
- package/dist/cli/evaluate.d.ts.map +1 -1
- package/dist/cli/evaluate.js +34 -10
- package/dist/cli/evaluate.js.map +1 -1
- package/dist/cli/extract.d.ts.map +1 -1
- package/dist/cli/extract.js +8 -3
- package/dist/cli/extract.js.map +1 -1
- package/dist/cli/main.js +8 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/corpus/scanner.d.ts +9 -1
- package/dist/corpus/scanner.d.ts.map +1 -1
- package/dist/corpus/scanner.js +52 -13
- package/dist/corpus/scanner.js.map +1 -1
- package/dist/corpus/store.d.ts +29 -2
- package/dist/corpus/store.d.ts.map +1 -1
- package/dist/corpus/store.js +45 -2
- package/dist/corpus/store.js.map +1 -1
- package/dist/detect/patterns.d.ts +29 -0
- package/dist/detect/patterns.d.ts.map +1 -1
- package/dist/detect/patterns.js +80 -6
- package/dist/detect/patterns.js.map +1 -1
- package/dist/detect/reply-header.d.ts +14 -0
- package/dist/detect/reply-header.d.ts.map +1 -1
- package/dist/detect/reply-header.js +80 -37
- package/dist/detect/reply-header.js.map +1 -1
- package/dist/eval/oracle.d.ts +80 -0
- package/dist/eval/oracle.d.ts.map +1 -0
- package/dist/eval/oracle.js +250 -0
- package/dist/eval/oracle.js.map +1 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +94 -56
- package/dist/index.js.map +1 -1
- package/dist/match/history.d.ts +12 -0
- package/dist/match/history.d.ts.map +1 -1
- package/dist/match/history.js +16 -0
- package/dist/match/history.js.map +1 -1
- package/dist/mime/model.d.ts +28 -3
- package/dist/mime/model.d.ts.map +1 -1
- package/dist/mime/model.js +41 -1
- package/dist/mime/model.js.map +1 -1
- package/dist/mime/parse.d.ts.map +1 -1
- package/dist/mime/parse.js +19 -2
- package/dist/mime/parse.js.map +1 -1
- package/dist/mime/select.d.ts +71 -0
- package/dist/mime/select.d.ts.map +1 -0
- package/dist/mime/select.js +198 -0
- package/dist/mime/select.js.map +1 -0
- package/dist/segment/html-blocks.d.ts +1 -1
- package/dist/segment/html-blocks.d.ts.map +1 -1
- package/dist/segment/html-blocks.js +77 -8
- package/dist/segment/html-blocks.js.map +1 -1
- package/dist/segment/text-blocks.d.ts.map +1 -1
- package/dist/segment/text-blocks.js +18 -1
- package/dist/segment/text-blocks.js.map +1 -1
- package/dist/segment/types.d.ts +14 -0
- package/dist/segment/types.d.ts.map +1 -1
- package/dist/types.d.ts +132 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +37 -3
- package/dist/types.js.map +1 -1
- package/dist/util/worker-pool.d.ts +21 -0
- package/dist/util/worker-pool.d.ts.map +1 -1
- package/dist/util/worker-pool.js +61 -13
- package/dist/util/worker-pool.js.map +1 -1
- package/package.json +4 -2
package/dist/detect/patterns.js
CHANGED
|
@@ -14,29 +14,73 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.PatternAssetError = void 0;
|
|
18
|
+
exports.countPatterns = countPatterns;
|
|
19
|
+
exports.assertPatternsLoaded = assertPatternsLoaded;
|
|
17
20
|
exports.loadPatterns = loadPatterns;
|
|
18
21
|
exports.resetPatternCache = resetPatternCache;
|
|
19
22
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
20
23
|
const node_path_1 = __importDefault(require("node:path"));
|
|
21
24
|
const BUNDLED_DIR = node_path_1.default.join(__dirname, 'patterns');
|
|
25
|
+
/**
|
|
26
|
+
* A classifier asset is missing or corrupt.
|
|
27
|
+
*
|
|
28
|
+
* An installation defect, not malformed email, and therefore deliberately outside the per message
|
|
29
|
+
* fail-soft path. Without the pattern files every detector returns nothing: no reply header is
|
|
30
|
+
* recognised, no quoted history is found, and every message comes back looking like new content.
|
|
31
|
+
* That reads as success, which makes it the worst possible failure mode for a package whose whole
|
|
32
|
+
* job is to know which parts of a message are history.
|
|
33
|
+
*/
|
|
34
|
+
class PatternAssetError extends Error {
|
|
35
|
+
code = 'pattern_assets_unusable';
|
|
36
|
+
file;
|
|
37
|
+
constructor(message, file = null) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'PatternAssetError';
|
|
40
|
+
this.file = file;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.PatternAssetError = PatternAssetError;
|
|
22
44
|
function readJson(dir, file) {
|
|
45
|
+
let raw;
|
|
46
|
+
try {
|
|
47
|
+
raw = node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), 'utf8');
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { ok: false, reason: 'missing' };
|
|
51
|
+
}
|
|
52
|
+
// Present but unreadable is a different fault from absent, and must not be silently treated as
|
|
53
|
+
// an empty pattern set. A truncated file in a published tarball produced exactly that.
|
|
23
54
|
try {
|
|
24
|
-
const raw = node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), 'utf8');
|
|
25
55
|
const parsed = JSON.parse(raw);
|
|
26
|
-
|
|
56
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
57
|
+
return { ok: false, reason: 'invalid' };
|
|
58
|
+
}
|
|
59
|
+
return { ok: true, value: parsed };
|
|
27
60
|
}
|
|
28
61
|
catch {
|
|
29
|
-
return
|
|
62
|
+
return { ok: false, reason: 'invalid' };
|
|
30
63
|
}
|
|
31
64
|
}
|
|
32
65
|
function loadFile(file, overrideDir) {
|
|
33
66
|
if (overrideDir) {
|
|
34
67
|
const override = readJson(overrideDir, file);
|
|
35
|
-
if (override) {
|
|
36
|
-
return override;
|
|
68
|
+
if (override.ok) {
|
|
69
|
+
return override.value;
|
|
37
70
|
}
|
|
71
|
+
if (override.reason === 'invalid') {
|
|
72
|
+
throw new PatternAssetError(`Pattern file ${file} in ${overrideDir} is not valid JSON.`, file);
|
|
73
|
+
}
|
|
74
|
+
// Absent from an override directory is normal: an override may replace only some files.
|
|
75
|
+
}
|
|
76
|
+
const bundled = readJson(BUNDLED_DIR, file);
|
|
77
|
+
if (bundled.ok) {
|
|
78
|
+
return bundled.value;
|
|
79
|
+
}
|
|
80
|
+
if (bundled.reason === 'invalid') {
|
|
81
|
+
throw new PatternAssetError(`Bundled pattern file ${file} is corrupt. Reinstall the package.`, file);
|
|
38
82
|
}
|
|
39
|
-
return
|
|
83
|
+
return {};
|
|
40
84
|
}
|
|
41
85
|
function compileLinePatterns(input, invalid) {
|
|
42
86
|
if (!Array.isArray(input)) {
|
|
@@ -89,6 +133,35 @@ function compileSelectors(input, invalid) {
|
|
|
89
133
|
}
|
|
90
134
|
return out;
|
|
91
135
|
}
|
|
136
|
+
/** Total compiled patterns across every set. */
|
|
137
|
+
function countPatterns(sets) {
|
|
138
|
+
return (sets.replyHeaders.length +
|
|
139
|
+
sets.forwardHeaders.length +
|
|
140
|
+
sets.quotedHeaderFields.length +
|
|
141
|
+
sets.signatureDelimiters.length +
|
|
142
|
+
sets.signaturePhrases.length +
|
|
143
|
+
sets.disclaimerPhrases.length +
|
|
144
|
+
sets.htmlSelectors.length);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Fail loudly when no pattern survived loading.
|
|
148
|
+
*
|
|
149
|
+
* The bundled patterns are read from disk at runtime through path.join(__dirname, 'patterns').
|
|
150
|
+
* @yao-pkg/pkg detects that form and bundles the directory automatically, which is why consumers
|
|
151
|
+
* need no pkg asset configuration, but it is a static analysis heuristic rather than a guarantee.
|
|
152
|
+
* If it ever fails, or the files are missing from a published tarball, every detector quietly
|
|
153
|
+
* returns nothing: no reply header is recognised and reply history stops being detected.
|
|
154
|
+
*
|
|
155
|
+
* The failure direction is at least the safe one, since nothing can be hidden without an anchor.
|
|
156
|
+
* But a build that silently extracts nothing useful should not pass unnoticed.
|
|
157
|
+
*/
|
|
158
|
+
function assertPatternsLoaded(sets, source) {
|
|
159
|
+
if (countPatterns(sets) === 0) {
|
|
160
|
+
throw new PatternAssetError(`No detector patterns could be loaded from ${source}. ` +
|
|
161
|
+
'The pattern JSON files are missing from the installation or the bundle. ' +
|
|
162
|
+
'Without them no reply history can be detected.');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
92
165
|
let cached = null;
|
|
93
166
|
/** Load and compile all pattern sets. Cached after the first call. */
|
|
94
167
|
function loadPatterns(overrideDir) {
|
|
@@ -113,6 +186,7 @@ function loadPatterns(overrideDir) {
|
|
|
113
186
|
htmlSelectors: compileSelectors(html.selectors, invalid),
|
|
114
187
|
invalid
|
|
115
188
|
};
|
|
189
|
+
assertPatternsLoaded(sets, dir ?? BUNDLED_DIR);
|
|
116
190
|
if (overrideDir === undefined) {
|
|
117
191
|
cached = sets;
|
|
118
192
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/detect/patterns.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG
|
|
1
|
+
{"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/detect/patterns.ts"],"names":[],"mappings":"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"}
|
|
@@ -32,6 +32,20 @@ export declare function detectAttributionLine(line: string): Evidence | null;
|
|
|
32
32
|
*
|
|
33
33
|
* Returns the first match found, scanning forward. The caller supplies de-prefixed line content so
|
|
34
34
|
* that a header block inside a quote is detected the same way as one outside it.
|
|
35
|
+
*
|
|
36
|
+
* Single pass, deliberately. The obvious implementation restarts an inner forward scan at every
|
|
37
|
+
* candidate line, which is quadratic: 12,000 consecutive `Cc:` lines never form a strong block but
|
|
38
|
+
* make the inner scan traverse almost the whole remaining input from every outer position, and
|
|
39
|
+
* measured at 4.8 seconds of blocked event loop on that input. Inbound mail is attacker controlled,
|
|
40
|
+
* so that is a denial of service, not a slow path.
|
|
41
|
+
*
|
|
42
|
+
* The single pass is exactly equivalent, not an approximation. Lines partition into *chains*:
|
|
43
|
+
* maximal runs of header-shaped lines joined by blank gaps of at most `MAX_GAP`, terminated by any
|
|
44
|
+
* other line or by a longer blank run. The set of roles collected from a start inside a chain is a
|
|
45
|
+
* subset of the set collected from the chain's own start, and strength is monotone under superset,
|
|
46
|
+
* so if the chain start is not strong no later start in that chain can be, and if it is strong it
|
|
47
|
+
* is also the earliest strong start. Checking each chain once therefore returns the same match the
|
|
48
|
+
* restarting scan did.
|
|
35
49
|
*/
|
|
36
50
|
export declare function detectQuotedHeaderBlock(lines: readonly string[], from?: number): ReplyHeaderMatch | null;
|
|
37
51
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reply-header.d.ts","sourceRoot":"","sources":["../../src/detect/reply-header.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AA4BjD,MAAM,WAAW,gBAAgB;IAC7B,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,QAAQ,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAgBnE;
|
|
1
|
+
{"version":3,"file":"reply-header.d.ts","sourceRoot":"","sources":["../../src/detect/reply-header.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AA4BjD,MAAM,WAAW,gBAAgB;IAC7B,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,QAAQ,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAgBnE;AASD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,SAAI,GAAG,gBAAgB,GAAG,IAAI,CA8EnG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CASjE"}
|
|
@@ -61,57 +61,100 @@ function detectAttributionLine(line) {
|
|
|
61
61
|
}
|
|
62
62
|
return best;
|
|
63
63
|
}
|
|
64
|
+
/** Is this run of header-shaped lines convincing enough to anchor a region? */
|
|
65
|
+
function isStrong(roles) {
|
|
66
|
+
// A convincing block names the originator plus at least a date or a subject, so that a
|
|
67
|
+
// pasted "To: ... Cc: ..." address list on its own does not qualify.
|
|
68
|
+
return roles.size >= MIN_FIELDS && roles.has('originator') && (roles.has('date') || roles.has('subject'));
|
|
69
|
+
}
|
|
64
70
|
/**
|
|
65
71
|
* Detect an embedded header block starting at or after `from`.
|
|
66
72
|
*
|
|
67
73
|
* Returns the first match found, scanning forward. The caller supplies de-prefixed line content so
|
|
68
74
|
* that a header block inside a quote is detected the same way as one outside it.
|
|
75
|
+
*
|
|
76
|
+
* Single pass, deliberately. The obvious implementation restarts an inner forward scan at every
|
|
77
|
+
* candidate line, which is quadratic: 12,000 consecutive `Cc:` lines never form a strong block but
|
|
78
|
+
* make the inner scan traverse almost the whole remaining input from every outer position, and
|
|
79
|
+
* measured at 4.8 seconds of blocked event loop on that input. Inbound mail is attacker controlled,
|
|
80
|
+
* so that is a denial of service, not a slow path.
|
|
81
|
+
*
|
|
82
|
+
* The single pass is exactly equivalent, not an approximation. Lines partition into *chains*:
|
|
83
|
+
* maximal runs of header-shaped lines joined by blank gaps of at most `MAX_GAP`, terminated by any
|
|
84
|
+
* other line or by a longer blank run. The set of roles collected from a start inside a chain is a
|
|
85
|
+
* subset of the set collected from the chain's own start, and strength is monotone under superset,
|
|
86
|
+
* so if the chain start is not strong no later start in that chain can be, and if it is strong it
|
|
87
|
+
* is also the earliest strong start. Checking each chain once therefore returns the same match the
|
|
88
|
+
* restarting scan did.
|
|
69
89
|
*/
|
|
70
90
|
function detectQuotedHeaderBlock(lines, from = 0) {
|
|
71
91
|
const patterns = (0, patterns_1.loadPatterns)();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
const evidence = {
|
|
93
|
+
id: 'reply:quoted_header_block',
|
|
94
|
+
kind: 'reply-anchor',
|
|
95
|
+
// Below a single line attribution on purpose: a person can paste this shape.
|
|
96
|
+
strength: 0.88
|
|
97
|
+
};
|
|
98
|
+
// Start of the chain currently being accumulated, which may be a blank line: the restarting
|
|
99
|
+
// scan reported the outer index it began at, and that index is allowed to sit on the at most
|
|
100
|
+
// MAX_GAP blank lines that lead into the first field.
|
|
101
|
+
let chainStart = -1;
|
|
102
|
+
let roles = new Set();
|
|
103
|
+
let last = -1;
|
|
104
|
+
let gap = 0;
|
|
105
|
+
for (let j = Math.max(0, from); j < lines.length; j++) {
|
|
106
|
+
const line = (lines[j] ?? '').trim();
|
|
107
|
+
if (!line) {
|
|
108
|
+
if (chainStart === -1) {
|
|
109
|
+
chainStart = j;
|
|
110
|
+
gap = 1;
|
|
83
111
|
continue;
|
|
84
112
|
}
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
113
|
+
gap++;
|
|
114
|
+
if (gap <= MAX_GAP) {
|
|
115
|
+
continue;
|
|
88
116
|
}
|
|
89
|
-
gap
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
117
|
+
// The blank run is now too long to be an internal gap. Close the chain, then begin a
|
|
118
|
+
// new tentative one far enough into the blank run that exactly MAX_GAP blanks can still
|
|
119
|
+
// precede a following field line, which is where the restarting scan would have found
|
|
120
|
+
// its next viable start.
|
|
121
|
+
if (isStrong(roles) && last >= chainStart) {
|
|
122
|
+
return { startLine: chainStart, endLine: last, evidence };
|
|
123
|
+
}
|
|
124
|
+
roles = new Set();
|
|
125
|
+
last = -1;
|
|
126
|
+
chainStart = j - MAX_GAP + 1;
|
|
127
|
+
gap = MAX_GAP;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const matched = patterns.quotedHeaderFields.find(pattern => pattern.regex.test(line));
|
|
131
|
+
if (!matched) {
|
|
132
|
+
// Ordinary content terminates the chain outright.
|
|
133
|
+
if (isStrong(roles) && last >= chainStart && chainStart !== -1) {
|
|
134
|
+
return { startLine: chainStart, endLine: last, evidence };
|
|
97
135
|
}
|
|
98
|
-
|
|
136
|
+
roles = new Set();
|
|
137
|
+
last = -1;
|
|
138
|
+
chainStart = -1;
|
|
139
|
+
gap = 0;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (chainStart === -1) {
|
|
143
|
+
chainStart = j;
|
|
99
144
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
id: 'reply:quoted_header_block',
|
|
109
|
-
kind: 'reply-anchor',
|
|
110
|
-
// Below a single line attribution on purpose: a person can paste this shape.
|
|
111
|
-
strength: 0.88
|
|
112
|
-
}
|
|
113
|
-
};
|
|
145
|
+
gap = 0;
|
|
146
|
+
const role = matched.role || matched.id;
|
|
147
|
+
const value = valueOf(line);
|
|
148
|
+
// Only fields whose value looks like a real header value count towards the threshold.
|
|
149
|
+
// The line still continues the block either way, so a "Subject:" with unusual content
|
|
150
|
+
// does not truncate an otherwise convincing header block.
|
|
151
|
+
if (role === 'originator' ? ORIGINATOR_VALUE.test(value) : role === 'date' ? DATE_VALUE.test(value) : true) {
|
|
152
|
+
roles.add(role);
|
|
114
153
|
}
|
|
154
|
+
last = j;
|
|
155
|
+
}
|
|
156
|
+
if (chainStart !== -1 && isStrong(roles) && last >= chainStart) {
|
|
157
|
+
return { startLine: chainStart, endLine: last, evidence };
|
|
115
158
|
}
|
|
116
159
|
return null;
|
|
117
160
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reply-header.js","sourceRoot":"","sources":["../../src/detect/reply-header.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;AA6CH,sDAgBC;
|
|
1
|
+
{"version":3,"file":"reply-header.js","sourceRoot":"","sources":["../../src/detect/reply-header.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Independent safety oracles for the structural benchmark.
|
|
3
|
+
*
|
|
4
|
+
* The benchmark exists to detect the failure this project is built to prevent: a message that had
|
|
5
|
+
* content going in and has none coming out. It cannot do that by asking the extractor, and the
|
|
6
|
+
* previous version did exactly that:
|
|
7
|
+
*
|
|
8
|
+
* const hadContent = visibleChars + hiddenChars > 0;
|
|
9
|
+
*
|
|
10
|
+
* That reads "did the input have content" off the output. When extraction dropped everything the
|
|
11
|
+
* flag went false, the catastrophic counter did not increment, and the benchmark reported zero
|
|
12
|
+
* losses while a reproducible total loss case existed.
|
|
13
|
+
*
|
|
14
|
+
* So the code here shares the *definition* of a body with the extractor, and shares none of its
|
|
15
|
+
* *implementation*. It re-parses the message, walks the flat part list itself, and never calls the
|
|
16
|
+
* selection logic it is meant to check. Re-parsing costs a second MIME pass per message, which is
|
|
17
|
+
* the right trade for a release gate.
|
|
18
|
+
*/
|
|
19
|
+
import { type MimePart, type ParsedMessage } from '../mime/model';
|
|
20
|
+
/** Part ids that sit inside an embedded `message/rfc822`, which is content and not the outer body. */
|
|
21
|
+
export declare function embeddedPartIds(message: ParsedMessage): Set<string>;
|
|
22
|
+
/** A decoded body-bearing leaf, by the written MIME rules rather than by the selection heuristic. */
|
|
23
|
+
export declare function eligibleBodyParts(message: ParsedMessage): MimePart[];
|
|
24
|
+
export interface InputInventory {
|
|
25
|
+
/** Characters of plausibly visible body text across every eligible part. */
|
|
26
|
+
visibleChars: number;
|
|
27
|
+
/** Eligible non attachment body leaves, whether or not they hold visible text. */
|
|
28
|
+
eligibleParts: number;
|
|
29
|
+
/** True when at least one eligible part holds visible text. */
|
|
30
|
+
hasVisibleText: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Count plausible visible body text in a parsed message, before extraction runs.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately over-inclusive: it counts every eligible part, including both members of a
|
|
36
|
+
* `multipart/alternative`, because its only question is whether the message had anything visible to
|
|
37
|
+
* show. It is not, and must not be used as, a demand that every part appear in the output.
|
|
38
|
+
*
|
|
39
|
+
* Script, style, comment and markup text is excluded, so an image-only newsletter or a body that is
|
|
40
|
+
* nothing but a tracking pixel does not count as visible text that extraction then lost.
|
|
41
|
+
*/
|
|
42
|
+
export declare function inventoryVisibleInput(message: ParsedMessage): InputInventory;
|
|
43
|
+
/**
|
|
44
|
+
* A piece of the message that the output has to represent somehow.
|
|
45
|
+
*
|
|
46
|
+
* Per-part coverage would be wrong. A normal `multipart/alternative` carries both a plain and an
|
|
47
|
+
* HTML rendering, and HTML-primary output deliberately contains only HTML-derived segments;
|
|
48
|
+
* demanding both part ids would report most valid mail as lossy. So the unit is a *group*, and any
|
|
49
|
+
* one supported member satisfies it.
|
|
50
|
+
*/
|
|
51
|
+
export interface CoverageGroup {
|
|
52
|
+
/** Stable identifier: the container part for an alternative, the leaf itself otherwise. */
|
|
53
|
+
id: string;
|
|
54
|
+
/** True when the group is one alternative rendering set rather than independent content. */
|
|
55
|
+
alternative: boolean;
|
|
56
|
+
/** Part ids, any one of which represents this group. */
|
|
57
|
+
members: string[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Required coverage groups for a parsed message.
|
|
61
|
+
*
|
|
62
|
+
* Written from the decision table rather than by calling `planBody()`. Sharing the implementation
|
|
63
|
+
* would make the oracle agree with the selection code by construction, which is exactly the
|
|
64
|
+
* property a gate must not have. The two are reviewed against the same table and are expected to
|
|
65
|
+
* agree; when they do not, one of them is wrong and the run says so.
|
|
66
|
+
*/
|
|
67
|
+
export declare function requiredCoverageGroups(message: ParsedMessage): CoverageGroup[];
|
|
68
|
+
export interface CoverageReport {
|
|
69
|
+
required: number;
|
|
70
|
+
covered: number;
|
|
71
|
+
/** Group ids with no member anywhere in the output. Each one is content that vanished. */
|
|
72
|
+
missing: string[];
|
|
73
|
+
/** Alternative members deliberately not emitted. Informational, never a failure. */
|
|
74
|
+
alternativesNotEmitted: string[];
|
|
75
|
+
/** Groups present only in `hiddenSegments`. */
|
|
76
|
+
hiddenOnly: string[];
|
|
77
|
+
}
|
|
78
|
+
/** Compare the required groups against the MIME parts the result actually names. */
|
|
79
|
+
export declare function coverageOf(groups: readonly CoverageGroup[], visibleParts: ReadonlySet<string>, hiddenParts: ReadonlySet<string>): CoverageReport;
|
|
80
|
+
//# sourceMappingURL=oracle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oracle.d.ts","sourceRoot":"","sources":["../../src/eval/oracle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAElE,sGAAsG;AACtG,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,GAAG,CAAC,MAAM,CAAC,CAiBnE;AAED,qGAAqG;AACrG,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,GAAG,QAAQ,EAAE,CASpE;AAED,MAAM,WAAW,cAAc;IAC3B,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,aAAa,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,cAAc,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,cAAc,CAO5E;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC1B,2FAA2F;IAC3F,EAAE,EAAE,MAAM,CAAC;IACX,4FAA4F;IAC5F,WAAW,EAAE,OAAO,CAAC;IACrB,wDAAwD;IACxD,OAAO,EAAE,MAAM,EAAE,CAAC;CACrB;AAyGD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,EAAE,CAwD9E;AAED,MAAM,WAAW,cAAc;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,oFAAoF;IACpF,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,+CAA+C;IAC/C,UAAU,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,oFAAoF;AACpF,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,EAAE,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,cAAc,CAsBhJ"}
|