@postalsys/email-content 0.0.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/LICENSE +21 -0
- package/README.md +288 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/analyze.d.ts +91 -0
- package/dist/analyze.d.ts.map +1 -0
- package/dist/analyze.js +274 -0
- package/dist/analyze.js.map +1 -0
- package/dist/annotate/client.d.ts +64 -0
- package/dist/annotate/client.d.ts.map +1 -0
- package/dist/annotate/client.js +139 -0
- package/dist/annotate/client.js.map +1 -0
- package/dist/annotate/llm.d.ts +46 -0
- package/dist/annotate/llm.d.ts.map +1 -0
- package/dist/annotate/llm.js +107 -0
- package/dist/annotate/llm.js.map +1 -0
- package/dist/annotate/prompt.d.ts +38 -0
- package/dist/annotate/prompt.d.ts.map +1 -0
- package/dist/annotate/prompt.js +165 -0
- package/dist/annotate/prompt.js.map +1 -0
- package/dist/annotate/store.d.ts +43 -0
- package/dist/annotate/store.d.ts.map +1 -0
- package/dist/annotate/store.js +46 -0
- package/dist/annotate/store.js.map +1 -0
- package/dist/canonical/html.d.ts +33 -0
- package/dist/canonical/html.d.ts.map +1 -0
- package/dist/canonical/html.js +156 -0
- package/dist/canonical/html.js.map +1 -0
- package/dist/canonical/text.d.ts +75 -0
- package/dist/canonical/text.d.ts.map +1 -0
- package/dist/canonical/text.js +217 -0
- package/dist/canonical/text.js.map +1 -0
- package/dist/classify/confidence.d.ts +38 -0
- package/dist/classify/confidence.d.ts.map +1 -0
- package/dist/classify/confidence.js +59 -0
- package/dist/classify/confidence.js.map +1 -0
- package/dist/classify/reconcile.d.ts +52 -0
- package/dist/classify/reconcile.d.ts.map +1 -0
- package/dist/classify/reconcile.js +100 -0
- package/dist/classify/reconcile.js.map +1 -0
- package/dist/classify/rules.d.ts +51 -0
- package/dist/classify/rules.d.ts.map +1 -0
- package/dist/classify/rules.js +648 -0
- package/dist/classify/rules.js.map +1 -0
- package/dist/cli/annotate.d.ts +23 -0
- package/dist/cli/annotate.d.ts.map +1 -0
- package/dist/cli/annotate.js +328 -0
- package/dist/cli/annotate.js.map +1 -0
- package/dist/cli/args.d.ts +17 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +72 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/benchmark.d.ts +13 -0
- package/dist/cli/benchmark.d.ts.map +1 -0
- package/dist/cli/benchmark.js +172 -0
- package/dist/cli/benchmark.js.map +1 -0
- package/dist/cli/corpus.d.ts +9 -0
- package/dist/cli/corpus.d.ts.map +1 -0
- package/dist/cli/corpus.js +152 -0
- package/dist/cli/corpus.js.map +1 -0
- package/dist/cli/disagreements.d.ts +14 -0
- package/dist/cli/disagreements.d.ts.map +1 -0
- package/dist/cli/disagreements.js +133 -0
- package/dist/cli/disagreements.js.map +1 -0
- package/dist/cli/evaluate.d.ts +15 -0
- package/dist/cli/evaluate.d.ts.map +1 -0
- package/dist/cli/evaluate.js +215 -0
- package/dist/cli/evaluate.js.map +1 -0
- package/dist/cli/extract.d.ts +10 -0
- package/dist/cli/extract.d.ts.map +1 -0
- package/dist/cli/extract.js +139 -0
- package/dist/cli/extract.js.map +1 -0
- package/dist/cli/main.d.ts +9 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +147 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/corpus/analyze.d.ts +66 -0
- package/dist/corpus/analyze.d.ts.map +1 -0
- package/dist/corpus/analyze.js +214 -0
- package/dist/corpus/analyze.js.map +1 -0
- package/dist/corpus/migrations/index.d.ts +14 -0
- package/dist/corpus/migrations/index.d.ts.map +1 -0
- package/dist/corpus/migrations/index.js +158 -0
- package/dist/corpus/migrations/index.js.map +1 -0
- package/dist/corpus/sample.d.ts +66 -0
- package/dist/corpus/sample.d.ts.map +1 -0
- package/dist/corpus/sample.js +98 -0
- package/dist/corpus/sample.js.map +1 -0
- package/dist/corpus/scanner.d.ts +79 -0
- package/dist/corpus/scanner.d.ts.map +1 -0
- package/dist/corpus/scanner.js +256 -0
- package/dist/corpus/scanner.js.map +1 -0
- package/dist/corpus/stats.d.ts +76 -0
- package/dist/corpus/stats.d.ts.map +1 -0
- package/dist/corpus/stats.js +240 -0
- package/dist/corpus/stats.js.map +1 -0
- package/dist/corpus/store.d.ts +67 -0
- package/dist/corpus/store.d.ts.map +1 -0
- package/dist/corpus/store.js +196 -0
- package/dist/corpus/store.js.map +1 -0
- package/dist/corpus/thread-index.d.ts +31 -0
- package/dist/corpus/thread-index.d.ts.map +1 -0
- package/dist/corpus/thread-index.js +182 -0
- package/dist/corpus/thread-index.js.map +1 -0
- package/dist/corpus/worker.d.ts +21 -0
- package/dist/corpus/worker.d.ts.map +1 -0
- package/dist/corpus/worker.js +74 -0
- package/dist/corpus/worker.js.map +1 -0
- package/dist/detect/client.d.ts +28 -0
- package/dist/detect/client.d.ts.map +1 -0
- package/dist/detect/client.js +121 -0
- package/dist/detect/client.js.map +1 -0
- package/dist/detect/disclaimer.d.ts +12 -0
- package/dist/detect/disclaimer.d.ts.map +1 -0
- package/dist/detect/disclaimer.js +28 -0
- package/dist/detect/disclaimer.js.map +1 -0
- package/dist/detect/forward-header.d.ts +10 -0
- package/dist/detect/forward-header.d.ts.map +1 -0
- package/dist/detect/forward-header.js +35 -0
- package/dist/detect/forward-header.js.map +1 -0
- package/dist/detect/patterns/disclaimers.json +113 -0
- package/dist/detect/patterns/forward-headers.json +40 -0
- package/dist/detect/patterns/html-clients.json +157 -0
- package/dist/detect/patterns/quoted-headers.json +41 -0
- package/dist/detect/patterns/reply-headers.json +139 -0
- package/dist/detect/patterns/signatures.json +58 -0
- package/dist/detect/patterns.d.ts +43 -0
- package/dist/detect/patterns.d.ts.map +1 -0
- package/dist/detect/patterns.js +125 -0
- package/dist/detect/patterns.js.map +1 -0
- package/dist/detect/reply-header.d.ts +44 -0
- package/dist/detect/reply-header.d.ts.map +1 -0
- package/dist/detect/reply-header.js +134 -0
- package/dist/detect/reply-header.js.map +1 -0
- package/dist/detect/signature.d.ts +28 -0
- package/dist/detect/signature.d.ts.map +1 -0
- package/dist/detect/signature.js +77 -0
- package/dist/detect/signature.js.map +1 -0
- package/dist/detect/traffic.d.ts +26 -0
- package/dist/detect/traffic.d.ts.map +1 -0
- package/dist/detect/traffic.js +98 -0
- package/dist/detect/traffic.js.map +1 -0
- package/dist/eval/metrics.d.ts +87 -0
- package/dist/eval/metrics.d.ts.map +1 -0
- package/dist/eval/metrics.js +185 -0
- package/dist/eval/metrics.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/dist/match/history.d.ts +62 -0
- package/dist/match/history.d.ts.map +1 -0
- package/dist/match/history.js +128 -0
- package/dist/match/history.js.map +1 -0
- package/dist/match/normalize.d.ts +33 -0
- package/dist/match/normalize.d.ts.map +1 -0
- package/dist/match/normalize.js +63 -0
- package/dist/match/normalize.js.map +1 -0
- package/dist/match/shingle.d.ts +44 -0
- package/dist/match/shingle.d.ts.map +1 -0
- package/dist/match/shingle.js +78 -0
- package/dist/match/shingle.js.map +1 -0
- package/dist/mime/charset.d.ts +33 -0
- package/dist/mime/charset.d.ts.map +1 -0
- package/dist/mime/charset.js +118 -0
- package/dist/mime/charset.js.map +1 -0
- package/dist/mime/headers.d.ts +32 -0
- package/dist/mime/headers.d.ts.map +1 -0
- package/dist/mime/headers.js +127 -0
- package/dist/mime/headers.js.map +1 -0
- package/dist/mime/model.d.ts +111 -0
- package/dist/mime/model.d.ts.map +1 -0
- package/dist/mime/model.js +17 -0
- package/dist/mime/model.js.map +1 -0
- package/dist/mime/parse.d.ts +27 -0
- package/dist/mime/parse.d.ts.map +1 -0
- package/dist/mime/parse.js +343 -0
- package/dist/mime/parse.js.map +1 -0
- package/dist/segment/features.d.ts +29 -0
- package/dist/segment/features.d.ts.map +1 -0
- package/dist/segment/features.js +102 -0
- package/dist/segment/features.js.map +1 -0
- package/dist/segment/html-blocks.d.ts +28 -0
- package/dist/segment/html-blocks.d.ts.map +1 -0
- package/dist/segment/html-blocks.js +596 -0
- package/dist/segment/html-blocks.js.map +1 -0
- package/dist/segment/text-blocks.d.ts +30 -0
- package/dist/segment/text-blocks.d.ts.map +1 -0
- package/dist/segment/text-blocks.js +204 -0
- package/dist/segment/text-blocks.js.map +1 -0
- package/dist/segment/types.d.ts +88 -0
- package/dist/segment/types.d.ts.map +1 -0
- package/dist/segment/types.js +11 -0
- package/dist/segment/types.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/util/hash.d.ts +49 -0
- package/dist/util/hash.d.ts.map +1 -0
- package/dist/util/hash.js +0 -0
- package/dist/util/hash.js.map +1 -0
- package/dist/util/lang.d.ts +25 -0
- package/dist/util/lang.d.ts.map +1 -0
- package/dist/util/lang.js +135 -0
- package/dist/util/lang.js.map +1 -0
- package/dist/util/worker-pool.d.ts +51 -0
- package/dist/util/worker-pool.d.ts.map +1 -0
- package/dist/util/worker-pool.js +155 -0
- package/dist/util/worker-pool.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pattern set loader.
|
|
4
|
+
*
|
|
5
|
+
* Patterns live in JSON so they can be extended or overridden without touching code, which matters
|
|
6
|
+
* because the multilingual and client specific surface keeps growing. A broken pattern is skipped
|
|
7
|
+
* with a warning rather than crashing the process: a bad regex in a config file must never take
|
|
8
|
+
* down a corpus run.
|
|
9
|
+
*
|
|
10
|
+
* Set EMAIL_CONTENT_PATTERNS to a directory to override the bundled sets. Files are matched by
|
|
11
|
+
* name, so an override directory may contain only the files it wants to replace.
|
|
12
|
+
*/
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.loadPatterns = loadPatterns;
|
|
18
|
+
exports.resetPatternCache = resetPatternCache;
|
|
19
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
20
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
21
|
+
const BUNDLED_DIR = node_path_1.default.join(__dirname, 'patterns');
|
|
22
|
+
function readJson(dir, file) {
|
|
23
|
+
try {
|
|
24
|
+
const raw = node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), 'utf8');
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function loadFile(file, overrideDir) {
|
|
33
|
+
if (overrideDir) {
|
|
34
|
+
const override = readJson(overrideDir, file);
|
|
35
|
+
if (override) {
|
|
36
|
+
return override;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return readJson(BUNDLED_DIR, file) || {};
|
|
40
|
+
}
|
|
41
|
+
function compileLinePatterns(input, invalid) {
|
|
42
|
+
if (!Array.isArray(input)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const item of input) {
|
|
47
|
+
const id = typeof item.id === 'string' ? item.id : null;
|
|
48
|
+
const source = typeof item.pattern === 'string' ? item.pattern : null;
|
|
49
|
+
if (!id || !source) {
|
|
50
|
+
invalid.push(String(item.id ?? 'unnamed'));
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
out.push({
|
|
55
|
+
id,
|
|
56
|
+
lang: typeof item.lang === 'string' ? item.lang : undefined,
|
|
57
|
+
role: typeof item.role === 'string' ? item.role : undefined,
|
|
58
|
+
client: typeof item.client === 'string' ? item.client : undefined,
|
|
59
|
+
strength: typeof item.strength === 'number' ? item.strength : 0.5,
|
|
60
|
+
regex: new RegExp(source, typeof item.flags === 'string' ? item.flags : '')
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
invalid.push(id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function compileSelectors(input, invalid) {
|
|
70
|
+
if (!Array.isArray(input)) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const item of input) {
|
|
75
|
+
const id = typeof item.id === 'string' ? item.id : null;
|
|
76
|
+
const selector = typeof item.selector === 'string' ? item.selector : null;
|
|
77
|
+
if (!id || !selector) {
|
|
78
|
+
invalid.push(String(item.id ?? 'unnamed'));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
out.push({
|
|
82
|
+
id,
|
|
83
|
+
client: typeof item.client === 'string' ? item.client : '*',
|
|
84
|
+
role: (typeof item.role === 'string' ? item.role : 'hint'),
|
|
85
|
+
strength: typeof item.strength === 'number' ? item.strength : 0.5,
|
|
86
|
+
selector,
|
|
87
|
+
prevalence: typeof item.prevalence === 'number' ? item.prevalence : 0
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
let cached = null;
|
|
93
|
+
/** Load and compile all pattern sets. Cached after the first call. */
|
|
94
|
+
function loadPatterns(overrideDir) {
|
|
95
|
+
if (cached && overrideDir === undefined) {
|
|
96
|
+
return cached;
|
|
97
|
+
}
|
|
98
|
+
const dir = overrideDir === undefined ? process.env.EMAIL_CONTENT_PATTERNS || null : overrideDir;
|
|
99
|
+
const invalid = [];
|
|
100
|
+
const reply = loadFile('reply-headers.json', dir);
|
|
101
|
+
const forward = loadFile('forward-headers.json', dir);
|
|
102
|
+
const quoted = loadFile('quoted-headers.json', dir);
|
|
103
|
+
const signatures = loadFile('signatures.json', dir);
|
|
104
|
+
const disclaimers = loadFile('disclaimers.json', dir);
|
|
105
|
+
const html = loadFile('html-clients.json', dir);
|
|
106
|
+
const sets = {
|
|
107
|
+
replyHeaders: compileLinePatterns(reply.patterns, invalid),
|
|
108
|
+
forwardHeaders: compileLinePatterns(forward.patterns, invalid),
|
|
109
|
+
quotedHeaderFields: compileLinePatterns(quoted.fields, invalid),
|
|
110
|
+
signatureDelimiters: compileLinePatterns(signatures.delimiters, invalid),
|
|
111
|
+
signaturePhrases: compileLinePatterns(signatures.phrases, invalid),
|
|
112
|
+
disclaimerPhrases: compileLinePatterns(disclaimers.phrases, invalid),
|
|
113
|
+
htmlSelectors: compileSelectors(html.selectors, invalid),
|
|
114
|
+
invalid
|
|
115
|
+
};
|
|
116
|
+
if (overrideDir === undefined) {
|
|
117
|
+
cached = sets;
|
|
118
|
+
}
|
|
119
|
+
return sets;
|
|
120
|
+
}
|
|
121
|
+
/** Reset the cache. Used by tests that swap pattern directories. */
|
|
122
|
+
function resetPatternCache() {
|
|
123
|
+
cached = null;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/detect/patterns.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reply attribution detection.
|
|
3
|
+
*
|
|
4
|
+
* Two shapes are handled, and the difference matters:
|
|
5
|
+
*
|
|
6
|
+
* 1. A single attribution line: "On Mon, 13 Jan 2025 at 09:15, Bob wrote:". One line, strong.
|
|
7
|
+
* 2. An embedded copy of the previous message's headers, which Outlook and several webmail
|
|
8
|
+
* clients emit as consecutive From/Sent/To/Subject lines.
|
|
9
|
+
*
|
|
10
|
+
* The second shape needs care. A single "From:" line is not evidence of anything, because ordinary
|
|
11
|
+
* prose starts that way often enough to matter. Only a run of consecutive field lines including a
|
|
12
|
+
* date-like or subject field counts, and even then the strength is capped below the single line
|
|
13
|
+
* attribution, because a person pasting an address block looks identical.
|
|
14
|
+
*/
|
|
15
|
+
import { type Evidence } from '../segment/types';
|
|
16
|
+
export interface ReplyHeaderMatch {
|
|
17
|
+
/** First line index of the anchor, inclusive. */
|
|
18
|
+
startLine: number;
|
|
19
|
+
/** Last line index of the anchor, inclusive. */
|
|
20
|
+
endLine: number;
|
|
21
|
+
evidence: Evidence;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Detect a single line attribution.
|
|
25
|
+
*
|
|
26
|
+
* Lines longer than 300 characters are rejected: a genuine attribution line is short, and a long
|
|
27
|
+
* one is almost always a paragraph that happens to end in "wrote:".
|
|
28
|
+
*/
|
|
29
|
+
export declare function detectAttributionLine(line: string): Evidence | null;
|
|
30
|
+
/**
|
|
31
|
+
* Detect an embedded header block starting at or after `from`.
|
|
32
|
+
*
|
|
33
|
+
* Returns the first match found, scanning forward. The caller supplies de-prefixed line content so
|
|
34
|
+
* that a header block inside a quote is detected the same way as one outside it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function detectQuotedHeaderBlock(lines: readonly string[], from?: number): ReplyHeaderMatch | null;
|
|
37
|
+
/**
|
|
38
|
+
* Detect the Outlook style horizontal rule, a long run of underscores on its own line.
|
|
39
|
+
*
|
|
40
|
+
* Weak on its own. It is only meaningful when an embedded header block follows within a few lines,
|
|
41
|
+
* which the segmenter checks.
|
|
42
|
+
*/
|
|
43
|
+
export declare function detectRuleSeparator(line: string): Evidence | null;
|
|
44
|
+
//# sourceMappingURL=reply-header.d.ts.map
|
|
@@ -0,0 +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;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,SAAI,GAAG,gBAAgB,GAAG,IAAI,CAmDnG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CASjE"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reply attribution detection.
|
|
4
|
+
*
|
|
5
|
+
* Two shapes are handled, and the difference matters:
|
|
6
|
+
*
|
|
7
|
+
* 1. A single attribution line: "On Mon, 13 Jan 2025 at 09:15, Bob wrote:". One line, strong.
|
|
8
|
+
* 2. An embedded copy of the previous message's headers, which Outlook and several webmail
|
|
9
|
+
* clients emit as consecutive From/Sent/To/Subject lines.
|
|
10
|
+
*
|
|
11
|
+
* The second shape needs care. A single "From:" line is not evidence of anything, because ordinary
|
|
12
|
+
* prose starts that way often enough to matter. Only a run of consecutive field lines including a
|
|
13
|
+
* date-like or subject field counts, and even then the strength is capped below the single line
|
|
14
|
+
* attribution, because a person pasting an address block looks identical.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.detectAttributionLine = detectAttributionLine;
|
|
18
|
+
exports.detectQuotedHeaderBlock = detectQuotedHeaderBlock;
|
|
19
|
+
exports.detectRuleSeparator = detectRuleSeparator;
|
|
20
|
+
const patterns_1 = require("./patterns");
|
|
21
|
+
/** Longest run of blank lines tolerated inside an embedded header block. */
|
|
22
|
+
const MAX_GAP = 1;
|
|
23
|
+
/** Minimum distinct fields before a run of header-like lines counts as an anchor. */
|
|
24
|
+
const MIN_FIELDS = 3;
|
|
25
|
+
/**
|
|
26
|
+
* An embedded From: line carries an actual address, in one form or another. Prose that happens to
|
|
27
|
+
* begin with "From:" does not.
|
|
28
|
+
*
|
|
29
|
+
* This check is what separates a real Outlook header block from a sentence like
|
|
30
|
+
* "From: the address that sent the message", which appears in `text-mentions-headers.eml` and in
|
|
31
|
+
* real support mail often enough to matter. Without it, an instruction list describing email
|
|
32
|
+
* headers is classified as reply history and disappears.
|
|
33
|
+
*/
|
|
34
|
+
const ORIGINATOR_VALUE = /(@[\w-]+\.[\w.-]+|<[^>]*@[^>]*>|mailto:|\[[^\]]*@[^\]]*\])/i;
|
|
35
|
+
/** A date field carries something date shaped: a numeric date, a month name, or a clock time. */
|
|
36
|
+
const DATE_VALUE = /(\d{1,4}[./-]\d{1,2}[./-]\d{1,4}|\b\d{1,2}:\d{2}\b|\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|mon|tue|wed|thu|fri|sat|sun)[a-z]*\.?\b|\b(esmaspäev|teisipäev|kolmapäev|neljapäev|reede|laupäev|pühapäev|montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag|lundi|mardi|mercredi|jeudi|vendredi|samedi|dimanche)\b)/i;
|
|
37
|
+
function valueOf(line) {
|
|
38
|
+
const colon = line.indexOf(':');
|
|
39
|
+
return colon === -1 ? '' : line.slice(colon + 1).trim();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Detect a single line attribution.
|
|
43
|
+
*
|
|
44
|
+
* Lines longer than 300 characters are rejected: a genuine attribution line is short, and a long
|
|
45
|
+
* one is almost always a paragraph that happens to end in "wrote:".
|
|
46
|
+
*/
|
|
47
|
+
function detectAttributionLine(line) {
|
|
48
|
+
const trimmed = line.trim();
|
|
49
|
+
if (!trimmed || trimmed.length > 300) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const patterns = (0, patterns_1.loadPatterns)();
|
|
53
|
+
let best = null;
|
|
54
|
+
for (const pattern of patterns.replyHeaders) {
|
|
55
|
+
if (!pattern.regex.test(trimmed)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!best || pattern.strength > best.strength) {
|
|
59
|
+
best = { id: `reply:${pattern.id}`, kind: 'reply-anchor', strength: pattern.strength };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return best;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Detect an embedded header block starting at or after `from`.
|
|
66
|
+
*
|
|
67
|
+
* Returns the first match found, scanning forward. The caller supplies de-prefixed line content so
|
|
68
|
+
* that a header block inside a quote is detected the same way as one outside it.
|
|
69
|
+
*/
|
|
70
|
+
function detectQuotedHeaderBlock(lines, from = 0) {
|
|
71
|
+
const patterns = (0, patterns_1.loadPatterns)();
|
|
72
|
+
for (let i = from; i < lines.length; i++) {
|
|
73
|
+
const roles = new Set();
|
|
74
|
+
let gap = 0;
|
|
75
|
+
let last = -1;
|
|
76
|
+
for (let j = i; j < lines.length; j++) {
|
|
77
|
+
const line = (lines[j] ?? '').trim();
|
|
78
|
+
if (!line) {
|
|
79
|
+
gap++;
|
|
80
|
+
if (gap > MAX_GAP) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const matched = patterns.quotedHeaderFields.find(pattern => pattern.regex.test(line));
|
|
86
|
+
if (!matched) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
gap = 0;
|
|
90
|
+
const role = matched.role || matched.id;
|
|
91
|
+
const value = valueOf(line);
|
|
92
|
+
// Only fields whose value looks like a real header value count towards the threshold.
|
|
93
|
+
// The line still continues the block either way, so a "Subject:" with unusual content
|
|
94
|
+
// does not truncate an otherwise convincing header block.
|
|
95
|
+
if (role === 'originator' ? ORIGINATOR_VALUE.test(value) : role === 'date' ? DATE_VALUE.test(value) : true) {
|
|
96
|
+
roles.add(role);
|
|
97
|
+
}
|
|
98
|
+
last = j;
|
|
99
|
+
}
|
|
100
|
+
// A convincing block names the originator plus at least a date or a subject, so that a
|
|
101
|
+
// pasted "To: ... Cc: ..." address list on its own does not qualify.
|
|
102
|
+
const strong = roles.size >= MIN_FIELDS && roles.has('originator') && (roles.has('date') || roles.has('subject'));
|
|
103
|
+
if (strong && last >= i) {
|
|
104
|
+
return {
|
|
105
|
+
startLine: i,
|
|
106
|
+
endLine: last,
|
|
107
|
+
evidence: {
|
|
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
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Detect the Outlook style horizontal rule, a long run of underscores on its own line.
|
|
120
|
+
*
|
|
121
|
+
* Weak on its own. It is only meaningful when an embedded header block follows within a few lines,
|
|
122
|
+
* which the segmenter checks.
|
|
123
|
+
*/
|
|
124
|
+
function detectRuleSeparator(line) {
|
|
125
|
+
const trimmed = line.trim();
|
|
126
|
+
if (/^_{10,}$/.test(trimmed)) {
|
|
127
|
+
return { id: 'reply:outlook_underscore_rule', kind: 'separator', strength: 0.6 };
|
|
128
|
+
}
|
|
129
|
+
if (/^[*=~]{10,}$/.test(trimmed)) {
|
|
130
|
+
return { id: 'separator:symbol_rule', kind: 'separator', strength: 0.35 };
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=reply-header.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reply-header.js","sourceRoot":"","sources":["../../src/detect/reply-header.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signature detection.
|
|
3
|
+
*
|
|
4
|
+
* Signatures are collapsed, never hidden, so this only needs to be good enough to demote a trailing
|
|
5
|
+
* block. Two rules carry almost all the weight:
|
|
6
|
+
*
|
|
7
|
+
* - The RFC 3676 delimiter, a line containing exactly "-- " (trailing space) or "--". This is the
|
|
8
|
+
* one high confidence signal.
|
|
9
|
+
* - Position plus shape: a short block of short lines at the very end of a message, containing
|
|
10
|
+
* contact-like content.
|
|
11
|
+
*
|
|
12
|
+
* A signature that contains quote-looking text, a legal footer, or a pasted email address must not
|
|
13
|
+
* be promoted into reply history, which is why signature evidence is emitted for the block rather
|
|
14
|
+
* than for individual lines.
|
|
15
|
+
*/
|
|
16
|
+
import { type Evidence } from '../segment/types';
|
|
17
|
+
import { type BlockFeatures } from '../segment/types';
|
|
18
|
+
export declare function detectSignatureDelimiter(line: string): Evidence | null;
|
|
19
|
+
/** Phrase level evidence, such as "Sent from my phone" or a closing greeting. */
|
|
20
|
+
export declare function detectSignaturePhrases(lines: readonly string[]): Evidence[];
|
|
21
|
+
/**
|
|
22
|
+
* Shape evidence for a whole block.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately weak. It exists to nudge a genuine trailing signature over the collapse threshold
|
|
25
|
+
* when combined with a phrase or a delimiter, not to classify on its own.
|
|
26
|
+
*/
|
|
27
|
+
export declare function detectSignatureShape(features: BlockFeatures): Evidence | null;
|
|
28
|
+
//# sourceMappingURL=signature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../src/detect/signature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAQtD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAQtE;AAED,iFAAiF;AACjF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,QAAQ,EAAE,CAmB3E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,aAAa,GAAG,QAAQ,GAAG,IAAI,CAc7E"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Signature detection.
|
|
4
|
+
*
|
|
5
|
+
* Signatures are collapsed, never hidden, so this only needs to be good enough to demote a trailing
|
|
6
|
+
* block. Two rules carry almost all the weight:
|
|
7
|
+
*
|
|
8
|
+
* - The RFC 3676 delimiter, a line containing exactly "-- " (trailing space) or "--". This is the
|
|
9
|
+
* one high confidence signal.
|
|
10
|
+
* - Position plus shape: a short block of short lines at the very end of a message, containing
|
|
11
|
+
* contact-like content.
|
|
12
|
+
*
|
|
13
|
+
* A signature that contains quote-looking text, a legal footer, or a pasted email address must not
|
|
14
|
+
* be promoted into reply history, which is why signature evidence is emitted for the block rather
|
|
15
|
+
* than for individual lines.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.detectSignatureDelimiter = detectSignatureDelimiter;
|
|
19
|
+
exports.detectSignaturePhrases = detectSignaturePhrases;
|
|
20
|
+
exports.detectSignatureShape = detectSignatureShape;
|
|
21
|
+
const patterns_1 = require("./patterns");
|
|
22
|
+
/** Signatures beyond this many lines are almost always something else. */
|
|
23
|
+
const MAX_SIGNATURE_LINES = 12;
|
|
24
|
+
/** A signature block sits in the last part of the message. */
|
|
25
|
+
const MIN_POSITION = 0.5;
|
|
26
|
+
function detectSignatureDelimiter(line) {
|
|
27
|
+
const patterns = (0, patterns_1.loadPatterns)();
|
|
28
|
+
for (const pattern of patterns.signatureDelimiters) {
|
|
29
|
+
if (pattern.regex.test(line)) {
|
|
30
|
+
return { id: `signature:${pattern.id}`, kind: 'signature', strength: pattern.strength };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/** Phrase level evidence, such as "Sent from my phone" or a closing greeting. */
|
|
36
|
+
function detectSignaturePhrases(lines) {
|
|
37
|
+
const patterns = (0, patterns_1.loadPatterns)();
|
|
38
|
+
const found = new Map();
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const trimmed = line.trim();
|
|
41
|
+
if (!trimmed || trimmed.length > 200) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
for (const pattern of patterns.signaturePhrases) {
|
|
45
|
+
if (pattern.regex.test(trimmed)) {
|
|
46
|
+
const id = `signature:${pattern.id}`;
|
|
47
|
+
const existing = found.get(id);
|
|
48
|
+
if (!existing || existing.strength < pattern.strength) {
|
|
49
|
+
found.set(id, { id, kind: 'signature', strength: pattern.strength });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...found.values()];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Shape evidence for a whole block.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately weak. It exists to nudge a genuine trailing signature over the collapse threshold
|
|
60
|
+
* when combined with a phrase or a delimiter, not to classify on its own.
|
|
61
|
+
*/
|
|
62
|
+
function detectSignatureShape(features) {
|
|
63
|
+
if (features.isAllBlank || features.position < MIN_POSITION) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (features.lineCount > MAX_SIGNATURE_LINES || features.wordCount > 80) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const contactish = features.hasEmailAddress || features.hasPhoneNumber || features.hasUrl;
|
|
70
|
+
if (!contactish || features.shortLineRatio < 0.6) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
// Trailing position and contact details, but no delimiter and no phrase. Enough to collapse
|
|
74
|
+
// only in combination with something else.
|
|
75
|
+
return { id: 'signature:shape', kind: 'signature', strength: 0.45 };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=signature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signature.js","sourceRoot":"","sources":["../../src/detect/signature.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;AAYH,4DAQC;AAGD,wDAmBC;AAQD,oDAcC;AA9DD,yCAA0C;AAI1C,0EAA0E;AAC1E,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAE/B,8DAA8D;AAC9D,MAAM,YAAY,GAAG,GAAG,CAAC;AAEzB,SAAgB,wBAAwB,CAAC,IAAY;IACjD,MAAM,QAAQ,GAAG,IAAA,uBAAY,GAAE,CAAC;IAChC,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,mBAAmB,EAAE,CAAC;QACjD,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,OAAO,EAAE,EAAE,EAAE,aAAa,OAAO,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;QAC5F,CAAC;IACL,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,iFAAiF;AACjF,SAAgB,sBAAsB,CAAC,KAAwB;IAC3D,MAAM,QAAQ,GAAG,IAAA,uBAAY,GAAE,CAAC;IAChC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YACnC,SAAS;QACb,CAAC;QACD,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,gBAAgB,EAAE,CAAC;YAC9C,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC9B,MAAM,EAAE,GAAG,aAAa,OAAO,CAAC,EAAE,EAAE,CAAC;gBACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBAC/B,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;oBACpD,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;gBACzE,CAAC;YACL,CAAC;QACL,CAAC;IACL,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;GAKG;AACH,SAAgB,oBAAoB,CAAC,QAAuB;IACxD,IAAI,QAAQ,CAAC,UAAU,IAAI,QAAQ,CAAC,QAAQ,GAAG,YAAY,EAAE,CAAC;QAC1D,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,IAAI,QAAQ,CAAC,SAAS,GAAG,mBAAmB,IAAI,QAAQ,CAAC,SAAS,GAAG,EAAE,EAAE,CAAC;QACtE,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,MAAM,UAAU,GAAG,QAAQ,CAAC,eAAe,IAAI,QAAQ,CAAC,cAAc,IAAI,QAAQ,CAAC,MAAM,CAAC;IAC1F,IAAI,CAAC,UAAU,IAAI,QAAQ,CAAC,cAAc,GAAG,GAAG,EAAE,CAAC;QAC/C,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,4FAA4F;IAC5F,2CAA2C;IAC3C,OAAO,EAAE,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AACxE,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traffic classification: is this message part of a human conversation, or machine generated?
|
|
3
|
+
*
|
|
4
|
+
* This matters more than it looks. Measured on the working corpus, only about 13 percent of
|
|
5
|
+
* messages are human authored, roughly 40 percent are GitHub notifications, and 56 percent carry
|
|
6
|
+
* mailing list or bulk headers. Sampling and evaluation target the human slice, so a reliable
|
|
7
|
+
* split keeps the annotation budget from being spent on templated machine formats.
|
|
8
|
+
*
|
|
9
|
+
* The rules are header based and deliberately ordered from most to least specific. They classify
|
|
10
|
+
* traffic, never content: a notification message is still extracted normally.
|
|
11
|
+
*/
|
|
12
|
+
import { type HeaderEntry, type MessageHeaders } from '../mime/model';
|
|
13
|
+
export type TrafficClass = 'human' | 'mailing-list' | 'bulk' | 'notification' | 'ticketing' | 'auto-reply' | 'unknown';
|
|
14
|
+
export interface TrafficVerdict {
|
|
15
|
+
traffic: TrafficClass;
|
|
16
|
+
/** Ids of the rules that fired, in order. */
|
|
17
|
+
signals: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Classify message traffic.
|
|
21
|
+
*
|
|
22
|
+
* @param headers decoded well known headers
|
|
23
|
+
* @param entries every header line of the outermost message
|
|
24
|
+
*/
|
|
25
|
+
export declare function classifyTraffic(headers: MessageHeaders, entries: readonly HeaderEntry[]): TrafficVerdict;
|
|
26
|
+
//# sourceMappingURL=traffic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traffic.d.ts","sourceRoot":"","sources":["../../src/detect/traffic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAEtE,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,cAAc,GAAG,WAAW,GAAG,YAAY,GAAG,SAAS,CAAC;AAEvH,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,YAAY,CAAC;IACtB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,EAAE,CAAC;CACrB;AAqCD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,cAAc,CA+CxG"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Traffic classification: is this message part of a human conversation, or machine generated?
|
|
4
|
+
*
|
|
5
|
+
* This matters more than it looks. Measured on the working corpus, only about 13 percent of
|
|
6
|
+
* messages are human authored, roughly 40 percent are GitHub notifications, and 56 percent carry
|
|
7
|
+
* mailing list or bulk headers. Sampling and evaluation target the human slice, so a reliable
|
|
8
|
+
* split keeps the annotation budget from being spent on templated machine formats.
|
|
9
|
+
*
|
|
10
|
+
* The rules are header based and deliberately ordered from most to least specific. They classify
|
|
11
|
+
* traffic, never content: a notification message is still extracted normally.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.classifyTraffic = classifyTraffic;
|
|
15
|
+
const NOREPLY = /(^|[.\-_])(no-?reply|do-?not-?reply|donotreply|notifications?|mailer-daemon|postmaster|bounces?|automated)([.\-_@]|$)/i;
|
|
16
|
+
const VENDOR_HEADERS = [
|
|
17
|
+
'x-github-recipient',
|
|
18
|
+
'x-github-sender',
|
|
19
|
+
'x-gitlab-project',
|
|
20
|
+
'x-jira-fingerprint',
|
|
21
|
+
'x-atlassian-mail-generated',
|
|
22
|
+
'x-zendesk-from-account-id',
|
|
23
|
+
'x-freshdesk-account',
|
|
24
|
+
'x-intercom-message-id',
|
|
25
|
+
'x-hubspot-message-id',
|
|
26
|
+
'x-mailgun-sid',
|
|
27
|
+
'x-ses-outgoing',
|
|
28
|
+
'x-sg-eid',
|
|
29
|
+
'x-campaign-id',
|
|
30
|
+
'feedback-id',
|
|
31
|
+
'x-mailchimp-id'
|
|
32
|
+
];
|
|
33
|
+
const TICKETING_HEADERS = new Set([
|
|
34
|
+
'x-github-recipient',
|
|
35
|
+
'x-github-sender',
|
|
36
|
+
'x-gitlab-project',
|
|
37
|
+
'x-jira-fingerprint',
|
|
38
|
+
'x-atlassian-mail-generated',
|
|
39
|
+
'x-zendesk-from-account-id',
|
|
40
|
+
'x-freshdesk-account',
|
|
41
|
+
'x-intercom-message-id'
|
|
42
|
+
]);
|
|
43
|
+
function hasHeader(entries, key) {
|
|
44
|
+
return entries.some(entry => entry.key === key);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Classify message traffic.
|
|
48
|
+
*
|
|
49
|
+
* @param headers decoded well known headers
|
|
50
|
+
* @param entries every header line of the outermost message
|
|
51
|
+
*/
|
|
52
|
+
function classifyTraffic(headers, entries) {
|
|
53
|
+
const signals = [];
|
|
54
|
+
const autoSubmitted = (headers.autoSubmitted || '').toLowerCase();
|
|
55
|
+
if (autoSubmitted && autoSubmitted !== 'no') {
|
|
56
|
+
signals.push(`auto-submitted:${autoSubmitted.split(/[\s;]/)[0]}`);
|
|
57
|
+
}
|
|
58
|
+
if (headers.listId) {
|
|
59
|
+
signals.push('list-id');
|
|
60
|
+
}
|
|
61
|
+
if (headers.listUnsubscribe) {
|
|
62
|
+
signals.push('list-unsubscribe');
|
|
63
|
+
}
|
|
64
|
+
const precedence = (headers.precedence || '').toLowerCase();
|
|
65
|
+
if (precedence === 'bulk' || precedence === 'junk' || precedence === 'list') {
|
|
66
|
+
signals.push(`precedence:${precedence}`);
|
|
67
|
+
}
|
|
68
|
+
const vendor = VENDOR_HEADERS.filter(key => hasHeader(entries, key));
|
|
69
|
+
signals.push(...vendor);
|
|
70
|
+
if (headers.from && NOREPLY.test(headers.from)) {
|
|
71
|
+
signals.push('noreply-from');
|
|
72
|
+
}
|
|
73
|
+
// Most specific first. Ticketing and notification systems often also set list headers, and
|
|
74
|
+
// knowing that a message is a GitHub notification is more useful than knowing it is a list.
|
|
75
|
+
if (vendor.some(key => TICKETING_HEADERS.has(key))) {
|
|
76
|
+
return { traffic: 'ticketing', signals };
|
|
77
|
+
}
|
|
78
|
+
if (autoSubmitted === 'auto-replied' || autoSubmitted === 'auto-generated') {
|
|
79
|
+
return { traffic: 'auto-reply', signals };
|
|
80
|
+
}
|
|
81
|
+
if (headers.listId) {
|
|
82
|
+
return { traffic: 'mailing-list', signals };
|
|
83
|
+
}
|
|
84
|
+
if (headers.listUnsubscribe || vendor.length > 0) {
|
|
85
|
+
return { traffic: 'bulk', signals };
|
|
86
|
+
}
|
|
87
|
+
if (precedence === 'bulk' || precedence === 'junk') {
|
|
88
|
+
return { traffic: 'bulk', signals };
|
|
89
|
+
}
|
|
90
|
+
if (headers.from && NOREPLY.test(headers.from)) {
|
|
91
|
+
return { traffic: 'notification', signals };
|
|
92
|
+
}
|
|
93
|
+
if (!headers.from && !headers.messageId) {
|
|
94
|
+
return { traffic: 'unknown', signals };
|
|
95
|
+
}
|
|
96
|
+
return { traffic: 'human', signals };
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=traffic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traffic.js","sourceRoot":"","sources":["../../src/detect/traffic.ts"],"names":[],"mappings":"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"}
|