@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,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Header extraction helpers.
|
|
4
|
+
*
|
|
5
|
+
* Nothing in the pipeline may depend on a header being present. Every accessor returns null rather
|
|
6
|
+
* than throwing, and every decode is wrapped so that a malformed encoded word cannot abort a parse.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.decodeWordsSafe = decodeWordsSafe;
|
|
13
|
+
exports.parseHeaderValueSafe = parseHeaderValueSafe;
|
|
14
|
+
exports.toHeaderEntries = toHeaderEntries;
|
|
15
|
+
exports.normaliseMessageId = normaliseMessageId;
|
|
16
|
+
exports.parseReferences = parseReferences;
|
|
17
|
+
exports.extractMessageHeaders = extractMessageHeaders;
|
|
18
|
+
const libmime_1 = __importDefault(require("libmime"));
|
|
19
|
+
/** Decode MIME encoded words, falling back to the raw value on any failure. */
|
|
20
|
+
function decodeWordsSafe(value) {
|
|
21
|
+
try {
|
|
22
|
+
return libmime_1.default.decodeWords(value);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Parse a structured header value such as Content-Type into a value plus parameters. */
|
|
29
|
+
function parseHeaderValueSafe(value) {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = libmime_1.default.parseHeaderValue(value);
|
|
32
|
+
return { value: String(parsed.value || ''), params: parsed.params || {} };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { value, params: {} };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Convert mailsplit's raw header lines into normalised entries.
|
|
40
|
+
*
|
|
41
|
+
* mailsplit returns `{ key, line }` where `line` still contains the field name and any folded
|
|
42
|
+
* continuations joined with CRLF. Lines with no colon at all are kept with an empty key so that
|
|
43
|
+
* malformed input is still visible to diagnostics.
|
|
44
|
+
*/
|
|
45
|
+
function toHeaderEntries(list) {
|
|
46
|
+
const entries = [];
|
|
47
|
+
for (const item of list) {
|
|
48
|
+
const line = String(item.line || '');
|
|
49
|
+
const colon = line.indexOf(':');
|
|
50
|
+
const key = String(item.key || '').toLowerCase();
|
|
51
|
+
const rawValue = colon === -1 ? '' : line.slice(colon + 1);
|
|
52
|
+
// Unfold: a folded continuation is CRLF followed by whitespace.
|
|
53
|
+
const value = rawValue.replace(/\r?\n[ \t]+/g, ' ').trim();
|
|
54
|
+
entries.push({ key, value, decoded: decodeWordsSafe(value) });
|
|
55
|
+
}
|
|
56
|
+
return entries;
|
|
57
|
+
}
|
|
58
|
+
function first(entries, key) {
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (entry.key === key) {
|
|
61
|
+
return entry;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function firstValue(entries, key) {
|
|
67
|
+
const entry = first(entries, key);
|
|
68
|
+
return entry ? entry.value || null : null;
|
|
69
|
+
}
|
|
70
|
+
function firstDecoded(entries, key) {
|
|
71
|
+
const entry = first(entries, key);
|
|
72
|
+
return entry ? entry.decoded || null : null;
|
|
73
|
+
}
|
|
74
|
+
/** Strip angle brackets and surrounding whitespace from a Message-ID style value. */
|
|
75
|
+
function normaliseMessageId(value) {
|
|
76
|
+
if (!value) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const match = value.match(/<([^>]+)>/);
|
|
80
|
+
const id = (match?.[1] ?? value).trim().toLowerCase();
|
|
81
|
+
return id || null;
|
|
82
|
+
}
|
|
83
|
+
/** Extract every angle bracketed id from a References style header. */
|
|
84
|
+
function parseReferences(value) {
|
|
85
|
+
if (!value) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const matches = value.match(/<[^>\s]+>/g);
|
|
89
|
+
if (matches) {
|
|
90
|
+
return matches.map(item => item.slice(1, -1).trim().toLowerCase()).filter(Boolean);
|
|
91
|
+
}
|
|
92
|
+
// Some clients emit bare, whitespace separated ids.
|
|
93
|
+
return value
|
|
94
|
+
.split(/\s+/)
|
|
95
|
+
.map(item => item.trim().toLowerCase())
|
|
96
|
+
.filter(item => item.includes('@'));
|
|
97
|
+
}
|
|
98
|
+
function parseDateMs(value) {
|
|
99
|
+
if (!value) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const parsed = Date.parse(value);
|
|
103
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
104
|
+
}
|
|
105
|
+
/** Collect the headers the analysis pipeline uses. All fields are optional by design. */
|
|
106
|
+
function extractMessageHeaders(entries) {
|
|
107
|
+
const dateRaw = firstValue(entries, 'date');
|
|
108
|
+
return {
|
|
109
|
+
messageId: normaliseMessageId(firstValue(entries, 'message-id')),
|
|
110
|
+
inReplyTo: normaliseMessageId(firstValue(entries, 'in-reply-to')),
|
|
111
|
+
references: parseReferences(firstValue(entries, 'references')),
|
|
112
|
+
from: firstDecoded(entries, 'from'),
|
|
113
|
+
to: firstDecoded(entries, 'to'),
|
|
114
|
+
cc: firstDecoded(entries, 'cc'),
|
|
115
|
+
date: dateRaw,
|
|
116
|
+
dateMs: parseDateMs(dateRaw),
|
|
117
|
+
subject: firstDecoded(entries, 'subject'),
|
|
118
|
+
contentType: firstValue(entries, 'content-type'),
|
|
119
|
+
userAgent: firstValue(entries, 'user-agent'),
|
|
120
|
+
xMailer: firstValue(entries, 'x-mailer'),
|
|
121
|
+
listId: firstValue(entries, 'list-id'),
|
|
122
|
+
listUnsubscribe: firstValue(entries, 'list-unsubscribe'),
|
|
123
|
+
precedence: firstValue(entries, 'precedence'),
|
|
124
|
+
autoSubmitted: firstValue(entries, 'auto-submitted')
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/mime/headers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalised internal representation of a parsed message.
|
|
3
|
+
*
|
|
4
|
+
* The MIME tree is preserved as a tree. Nothing is flattened into a single html/text pair, because
|
|
5
|
+
* the extractor needs to know which text part is the alternative sibling of which html part, and
|
|
6
|
+
* needs to be able to point at the exact bytes a segment came from.
|
|
7
|
+
*/
|
|
8
|
+
import { type Warning } from '../types';
|
|
9
|
+
export interface ByteRange {
|
|
10
|
+
start: number;
|
|
11
|
+
end: number;
|
|
12
|
+
}
|
|
13
|
+
export interface HeaderEntry {
|
|
14
|
+
/** Lowercased field name. */
|
|
15
|
+
key: string;
|
|
16
|
+
/** Raw value as it appeared, unfolded but not decoded. */
|
|
17
|
+
value: string;
|
|
18
|
+
/** MIME encoded words decoded, best effort. Falls back to the raw value. */
|
|
19
|
+
decoded: string;
|
|
20
|
+
}
|
|
21
|
+
/** Result of decoding a text part, with enough detail to judge how much to trust it. */
|
|
22
|
+
export interface DecodedText {
|
|
23
|
+
/** Decoded, charset converted, flowed unfolded text. */
|
|
24
|
+
text: string;
|
|
25
|
+
/** Charset actually used, which may differ from the declared one. */
|
|
26
|
+
charsetUsed: string;
|
|
27
|
+
/** Charset as declared in the Content-Type header, if any. */
|
|
28
|
+
charsetDeclared: string | null;
|
|
29
|
+
/** Count of U+FFFD replacement characters produced by the decode. */
|
|
30
|
+
replacementChars: number;
|
|
31
|
+
/** True when the part exceeded the configured size cap and was cut short. */
|
|
32
|
+
truncated: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface MimePart {
|
|
35
|
+
/** IMAP style part path, for example "1.2". The root of a single part message is "TEXT". */
|
|
36
|
+
partId: string;
|
|
37
|
+
partNr: (number | string)[];
|
|
38
|
+
/** Lowercased content type, for example "text/html". Never empty, defaults per RFC 2046. */
|
|
39
|
+
contentType: string;
|
|
40
|
+
contentTypeParams: Record<string, string>;
|
|
41
|
+
charset: string | null;
|
|
42
|
+
encoding: string | null;
|
|
43
|
+
disposition: 'attachment' | 'inline' | null;
|
|
44
|
+
filename: string | null;
|
|
45
|
+
/** Multipart subtype ("alternative", "mixed", "related") or null for leaves. */
|
|
46
|
+
multipart: string | null;
|
|
47
|
+
/** True for a message/rfc822 container. */
|
|
48
|
+
rfc822: boolean;
|
|
49
|
+
/** True when this node is the root of an embedded message. */
|
|
50
|
+
embeddedRoot: boolean;
|
|
51
|
+
flowed: boolean;
|
|
52
|
+
delSp: boolean;
|
|
53
|
+
parentId: string | null;
|
|
54
|
+
childIds: string[];
|
|
55
|
+
/** Byte range of the raw header block within the original message. */
|
|
56
|
+
headerRange: ByteRange;
|
|
57
|
+
/** Byte range of the raw, still encoded body within the original message. Null for containers. */
|
|
58
|
+
bodyRange: ByteRange | null;
|
|
59
|
+
/** Raw encoded body size in bytes. */
|
|
60
|
+
rawBodySize: number;
|
|
61
|
+
/** All header lines of this part. */
|
|
62
|
+
headers: HeaderEntry[];
|
|
63
|
+
/** Decoded content, populated for text/plain and text/html only. */
|
|
64
|
+
decoded: DecodedText | null;
|
|
65
|
+
}
|
|
66
|
+
/** Headers the analysis cares about, decoded and normalised. Any of them may be missing. */
|
|
67
|
+
export interface MessageHeaders {
|
|
68
|
+
messageId: string | null;
|
|
69
|
+
inReplyTo: string | null;
|
|
70
|
+
references: string[];
|
|
71
|
+
from: string | null;
|
|
72
|
+
to: string | null;
|
|
73
|
+
cc: string | null;
|
|
74
|
+
date: string | null;
|
|
75
|
+
/** Parsed date as epoch milliseconds, or null when absent or unparseable. */
|
|
76
|
+
dateMs: number | null;
|
|
77
|
+
subject: string | null;
|
|
78
|
+
contentType: string | null;
|
|
79
|
+
userAgent: string | null;
|
|
80
|
+
xMailer: string | null;
|
|
81
|
+
listId: string | null;
|
|
82
|
+
listUnsubscribe: string | null;
|
|
83
|
+
precedence: string | null;
|
|
84
|
+
autoSubmitted: string | null;
|
|
85
|
+
}
|
|
86
|
+
export interface ParsedMessage {
|
|
87
|
+
/** All parts keyed by partId, in document order. */
|
|
88
|
+
parts: MimePart[];
|
|
89
|
+
partsById: Map<string, MimePart>;
|
|
90
|
+
rootId: string;
|
|
91
|
+
headers: MessageHeaders;
|
|
92
|
+
/** Every header line of the outermost message, for diagnostics and client fingerprinting. */
|
|
93
|
+
rootHeaders: HeaderEntry[];
|
|
94
|
+
warnings: Warning[];
|
|
95
|
+
/** Total size of the raw message in bytes. */
|
|
96
|
+
size: number;
|
|
97
|
+
/** Compact structural signature, for example "m/alternative(P,H)". */
|
|
98
|
+
structure: string;
|
|
99
|
+
}
|
|
100
|
+
export interface ParseOptions {
|
|
101
|
+
/** Maximum decoded bytes retained per text part. Default 1 MiB. */
|
|
102
|
+
maxTextPartBytes?: number;
|
|
103
|
+
/** Maximum number of parts before the walk stops recording. Default 500. */
|
|
104
|
+
maxParts?: number;
|
|
105
|
+
/** Maximum raw header block size per part, passed to mailsplit. Default 1 MiB. */
|
|
106
|
+
maxHeadSize?: number;
|
|
107
|
+
/** Treat message/rfc822 as an opaque leaf instead of descending into it. Default false. */
|
|
108
|
+
ignoreEmbedded?: boolean;
|
|
109
|
+
}
|
|
110
|
+
export declare const DEFAULT_PARSE_OPTIONS: Required<ParseOptions>;
|
|
111
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/mime/model.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Normalised internal representation of a parsed message.
|
|
4
|
+
*
|
|
5
|
+
* The MIME tree is preserved as a tree. Nothing is flattened into a single html/text pair, because
|
|
6
|
+
* the extractor needs to know which text part is the alternative sibling of which html part, and
|
|
7
|
+
* needs to be able to point at the exact bytes a segment came from.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.DEFAULT_PARSE_OPTIONS = void 0;
|
|
11
|
+
exports.DEFAULT_PARSE_OPTIONS = {
|
|
12
|
+
maxTextPartBytes: 1024 * 1024,
|
|
13
|
+
maxParts: 500,
|
|
14
|
+
maxHeadSize: 1024 * 1024,
|
|
15
|
+
ignoreEmbedded: false
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/mime/model.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAgHU,QAAA,qBAAqB,GAA2B;IACzD,gBAAgB,EAAE,IAAI,GAAG,IAAI;IAC7B,QAAQ,EAAE,GAAG;IACb,WAAW,EAAE,IAAI,GAAG,IAAI;IACxB,cAAc,EAAE,KAAK;CACxB,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 1: MIME parsing.
|
|
3
|
+
*
|
|
4
|
+
* Built on @zone-eu/mailsplit rather than a full parser, because mailsplit is the only local option
|
|
5
|
+
* that walks the tree non-destructively: it emits raw header bytes and raw body chunks in document
|
|
6
|
+
* order, byte for byte, and gives every node an IMAP style part number. That is what makes the
|
|
7
|
+
* traceability requirement satisfiable. postal-mime and mailparser both collapse the tree into a
|
|
8
|
+
* single html/text pair and discard byte spans.
|
|
9
|
+
*
|
|
10
|
+
* Byte offsets are not exposed as a field by mailsplit, so this module reconstructs them by
|
|
11
|
+
* accumulating chunk lengths in emission order. mailsplit's own round trip test asserts that the
|
|
12
|
+
* concatenation of all chunks equals the input, which is what makes the running sum exact.
|
|
13
|
+
*
|
|
14
|
+
* Nothing in here throws for malformed input. Problems are recorded as warnings and the walk
|
|
15
|
+
* continues, because a corpus run must never stop on one bad message.
|
|
16
|
+
*/
|
|
17
|
+
import { type MimePart, type ParseOptions, type ParsedMessage } from './model';
|
|
18
|
+
/** Compact structural signature such as "m/alternative(P,H)", used for corpus statistics. */
|
|
19
|
+
export declare function structureSignature(parts: readonly MimePart[], partsById: ReadonlyMap<string, MimePart>, rootId: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Parse a raw RFC 5322 message into the normalised internal representation.
|
|
22
|
+
*
|
|
23
|
+
* Never rejects on malformed input. The only rejection path is an internal stream error, and even
|
|
24
|
+
* that is converted into a parsed message carrying a `mime_parse_error` warning.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseMessage(raw: Buffer, options?: ParseOptions): Promise<ParsedMessage>;
|
|
27
|
+
//# sourceMappingURL=parse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/mime/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH,OAAO,EAA2C,KAAK,QAAQ,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AA4HxH,6FAA6F;AAC7F,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,EAAE,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CA0B/H;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CAgMlG"}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Stage 1: MIME parsing.
|
|
4
|
+
*
|
|
5
|
+
* Built on @zone-eu/mailsplit rather than a full parser, because mailsplit is the only local option
|
|
6
|
+
* that walks the tree non-destructively: it emits raw header bytes and raw body chunks in document
|
|
7
|
+
* order, byte for byte, and gives every node an IMAP style part number. That is what makes the
|
|
8
|
+
* traceability requirement satisfiable. postal-mime and mailparser both collapse the tree into a
|
|
9
|
+
* single html/text pair and discard byte spans.
|
|
10
|
+
*
|
|
11
|
+
* Byte offsets are not exposed as a field by mailsplit, so this module reconstructs them by
|
|
12
|
+
* accumulating chunk lengths in emission order. mailsplit's own round trip test asserts that the
|
|
13
|
+
* concatenation of all chunks equals the input, which is what makes the running sum exact.
|
|
14
|
+
*
|
|
15
|
+
* Nothing in here throws for malformed input. Problems are recorded as warnings and the walk
|
|
16
|
+
* continues, because a corpus run must never stop on one bad message.
|
|
17
|
+
*/
|
|
18
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
19
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
20
|
+
};
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.structureSignature = structureSignature;
|
|
23
|
+
exports.parseMessage = parseMessage;
|
|
24
|
+
const mailsplit_1 = require("@zone-eu/mailsplit");
|
|
25
|
+
const libmime_1 = __importDefault(require("libmime"));
|
|
26
|
+
const charset_1 = require("./charset");
|
|
27
|
+
const headers_1 = require("./headers");
|
|
28
|
+
const model_1 = require("./model");
|
|
29
|
+
/** Content types whose bodies are decoded and retained. Everything else is measured and dropped. */
|
|
30
|
+
const TEXT_TYPES = new Set(['text/plain', 'text/html']);
|
|
31
|
+
function partIdOf(partNr) {
|
|
32
|
+
if (!partNr || partNr.length === 0) {
|
|
33
|
+
return 'TEXT';
|
|
34
|
+
}
|
|
35
|
+
return partNr.join('.');
|
|
36
|
+
}
|
|
37
|
+
function normaliseDisposition(value) {
|
|
38
|
+
if (!value) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const lowered = String(value).toLowerCase();
|
|
42
|
+
if (lowered.startsWith('attachment')) {
|
|
43
|
+
return 'attachment';
|
|
44
|
+
}
|
|
45
|
+
if (lowered.startsWith('inline')) {
|
|
46
|
+
return 'inline';
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/** Decode one text part: transfer encoding, then charset, then format=flowed. */
|
|
51
|
+
async function decodeTextPart(pending, maxBytes, warnings) {
|
|
52
|
+
const { part, node } = pending;
|
|
53
|
+
const raw = Buffer.concat(pending.bodyChunks, pending.bodyBytes);
|
|
54
|
+
let transferDecoded;
|
|
55
|
+
try {
|
|
56
|
+
transferDecoded = await runDecoder(node, raw);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
warnings.push({
|
|
60
|
+
code: 'transfer_encoding_error',
|
|
61
|
+
mimePart: part.partId,
|
|
62
|
+
detail: `${part.encoding || 'unknown'}: ${errorLabel(err)}`
|
|
63
|
+
});
|
|
64
|
+
transferDecoded = raw;
|
|
65
|
+
}
|
|
66
|
+
let truncated = false;
|
|
67
|
+
if (transferDecoded.length > maxBytes) {
|
|
68
|
+
transferDecoded = transferDecoded.subarray(0, maxBytes);
|
|
69
|
+
truncated = true;
|
|
70
|
+
warnings.push({ code: 'text_part_truncated', mimePart: part.partId, detail: `capped at ${maxBytes} bytes` });
|
|
71
|
+
}
|
|
72
|
+
const decoded = (0, charset_1.decodeCharset)(transferDecoded, part.charset);
|
|
73
|
+
if (decoded.fellBack) {
|
|
74
|
+
warnings.push({
|
|
75
|
+
code: 'unknown_charset',
|
|
76
|
+
mimePart: part.partId,
|
|
77
|
+
detail: `declared ${part.charset || 'none'}, used ${decoded.charsetUsed}`
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (decoded.replacementChars > 0) {
|
|
81
|
+
warnings.push({
|
|
82
|
+
code: 'charset_decode_replacement_chars',
|
|
83
|
+
mimePart: part.partId,
|
|
84
|
+
detail: `${decoded.replacementChars} replacement characters`
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
let text = decoded.text;
|
|
88
|
+
if (part.flowed && part.contentType === 'text/plain') {
|
|
89
|
+
try {
|
|
90
|
+
text = libmime_1.default.decodeFlowed(text, part.delSp);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Keep the unfolded text. A failed flowed decode is cosmetic, never a reason to lose content.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
text,
|
|
98
|
+
charsetUsed: decoded.charsetUsed,
|
|
99
|
+
charsetDeclared: part.charset,
|
|
100
|
+
replacementChars: decoded.replacementChars,
|
|
101
|
+
truncated
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Run mailsplit's transfer decoder over a buffer, using its public streaming interface. */
|
|
105
|
+
function runDecoder(node, raw) {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
let decoder;
|
|
108
|
+
try {
|
|
109
|
+
decoder = node.getDecoder();
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
reject(err);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const chunks = [];
|
|
116
|
+
let size = 0;
|
|
117
|
+
decoder.on('data', (chunk) => {
|
|
118
|
+
chunks.push(chunk);
|
|
119
|
+
size += chunk.length;
|
|
120
|
+
});
|
|
121
|
+
decoder.on('error', reject);
|
|
122
|
+
decoder.on('end', () => resolve(Buffer.concat(chunks, size)));
|
|
123
|
+
decoder.end(raw);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function errorLabel(err) {
|
|
127
|
+
if (err instanceof Error) {
|
|
128
|
+
return err.message.slice(0, 200);
|
|
129
|
+
}
|
|
130
|
+
return String(err).slice(0, 200);
|
|
131
|
+
}
|
|
132
|
+
/** Compact structural signature such as "m/alternative(P,H)", used for corpus statistics. */
|
|
133
|
+
function structureSignature(parts, partsById, rootId) {
|
|
134
|
+
const render = (id, depth) => {
|
|
135
|
+
if (depth > 12) {
|
|
136
|
+
return '...';
|
|
137
|
+
}
|
|
138
|
+
const part = partsById.get(id);
|
|
139
|
+
if (!part) {
|
|
140
|
+
return '?';
|
|
141
|
+
}
|
|
142
|
+
if (part.multipart) {
|
|
143
|
+
const children = part.childIds.map(child => render(child, depth + 1)).join(',');
|
|
144
|
+
return `m/${part.multipart}(${children})`;
|
|
145
|
+
}
|
|
146
|
+
if (part.rfc822 && part.childIds.length) {
|
|
147
|
+
return `rfc822[${part.childIds.map(child => render(child, depth + 1)).join(',')}]`;
|
|
148
|
+
}
|
|
149
|
+
if (part.contentType === 'text/plain') {
|
|
150
|
+
return 'P';
|
|
151
|
+
}
|
|
152
|
+
if (part.contentType === 'text/html') {
|
|
153
|
+
return 'H';
|
|
154
|
+
}
|
|
155
|
+
const major = part.contentType.split('/')[0] || 'unknown';
|
|
156
|
+
return major;
|
|
157
|
+
};
|
|
158
|
+
return parts.length ? render(rootId, 0) : '(empty)';
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Parse a raw RFC 5322 message into the normalised internal representation.
|
|
162
|
+
*
|
|
163
|
+
* Never rejects on malformed input. The only rejection path is an internal stream error, and even
|
|
164
|
+
* that is converted into a parsed message carrying a `mime_parse_error` warning.
|
|
165
|
+
*/
|
|
166
|
+
async function parseMessage(raw, options = {}) {
|
|
167
|
+
const opts = { ...model_1.DEFAULT_PARSE_OPTIONS, ...options };
|
|
168
|
+
const warnings = [];
|
|
169
|
+
const pendingParts = [];
|
|
170
|
+
const partsById = new Map();
|
|
171
|
+
let offset = 0;
|
|
172
|
+
let rootId = 'TEXT';
|
|
173
|
+
let rootHeaderList = [];
|
|
174
|
+
let truncatedTree = false;
|
|
175
|
+
await new Promise(resolve => {
|
|
176
|
+
const splitter = new mailsplit_1.Splitter({
|
|
177
|
+
maxHeadSize: opts.maxHeadSize,
|
|
178
|
+
maxChildNodes: opts.maxParts,
|
|
179
|
+
ignoreEmbedded: opts.ignoreEmbedded
|
|
180
|
+
});
|
|
181
|
+
let settled = false;
|
|
182
|
+
const finish = () => {
|
|
183
|
+
if (!settled) {
|
|
184
|
+
settled = true;
|
|
185
|
+
resolve();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
splitter.on('data', chunk => {
|
|
189
|
+
if (chunk.type === 'node') {
|
|
190
|
+
const node = chunk;
|
|
191
|
+
let headerBytes;
|
|
192
|
+
try {
|
|
193
|
+
headerBytes = node.getHeaders();
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
warnings.push({ code: 'mime_parse_error', detail: errorLabel(err) });
|
|
197
|
+
headerBytes = Buffer.alloc(0);
|
|
198
|
+
}
|
|
199
|
+
const headerStart = offset;
|
|
200
|
+
offset += headerBytes.length;
|
|
201
|
+
if (pendingParts.length >= opts.maxParts) {
|
|
202
|
+
if (!truncatedTree) {
|
|
203
|
+
truncatedTree = true;
|
|
204
|
+
warnings.push({ code: 'max_parts_exceeded', detail: `stopped after ${opts.maxParts} parts` });
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const headerList = node.headers ? node.headers.getList() : [];
|
|
209
|
+
const entries = (0, headers_1.toHeaderEntries)(headerList);
|
|
210
|
+
const contentTypeHeader = entries.find(entry => entry.key === 'content-type');
|
|
211
|
+
const ctParams = contentTypeHeader ? (0, headers_1.parseHeaderValueSafe)(contentTypeHeader.value).params : {};
|
|
212
|
+
const partId = partIdOf(node.partNr);
|
|
213
|
+
const parentId = node.parentNode ? partIdOf(node.parentNode.partNr) : null;
|
|
214
|
+
const part = {
|
|
215
|
+
partId,
|
|
216
|
+
partNr: node.partNr ? [...node.partNr] : ['TEXT'],
|
|
217
|
+
contentType: node.contentType ? String(node.contentType).toLowerCase() : 'text/plain',
|
|
218
|
+
contentTypeParams: ctParams,
|
|
219
|
+
charset: node.charset ? String(node.charset) : null,
|
|
220
|
+
encoding: node.encoding ? String(node.encoding).toLowerCase() : null,
|
|
221
|
+
disposition: normaliseDisposition(node.disposition),
|
|
222
|
+
filename: node.filename ? String(node.filename) : null,
|
|
223
|
+
multipart: node.multipart ? String(node.multipart) : null,
|
|
224
|
+
rfc822: Boolean(node.rfc822),
|
|
225
|
+
embeddedRoot: Boolean(node.messageNode),
|
|
226
|
+
flowed: Boolean(node.flowed),
|
|
227
|
+
delSp: Boolean(node.delSp),
|
|
228
|
+
parentId,
|
|
229
|
+
childIds: [],
|
|
230
|
+
headerRange: { start: headerStart, end: offset },
|
|
231
|
+
bodyRange: null,
|
|
232
|
+
rawBodySize: 0,
|
|
233
|
+
headers: entries,
|
|
234
|
+
decoded: null
|
|
235
|
+
};
|
|
236
|
+
const pending = {
|
|
237
|
+
part,
|
|
238
|
+
node,
|
|
239
|
+
bodyChunks: [],
|
|
240
|
+
bodyBytes: 0,
|
|
241
|
+
collecting: TEXT_TYPES.has(part.contentType),
|
|
242
|
+
nonContiguous: false
|
|
243
|
+
};
|
|
244
|
+
if (node.root) {
|
|
245
|
+
rootId = partId;
|
|
246
|
+
rootHeaderList = headerList;
|
|
247
|
+
}
|
|
248
|
+
// Duplicate part ids can occur in pathological messages. Keep the first, so that
|
|
249
|
+
// references from children stay stable.
|
|
250
|
+
if (!partsById.has(partId)) {
|
|
251
|
+
partsById.set(partId, pending);
|
|
252
|
+
pendingParts.push(pending);
|
|
253
|
+
if (parentId) {
|
|
254
|
+
const parent = partsById.get(parentId);
|
|
255
|
+
if (parent) {
|
|
256
|
+
parent.part.childIds.push(partId);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const value = chunk.value;
|
|
263
|
+
const start = offset;
|
|
264
|
+
offset += value.length;
|
|
265
|
+
if (chunk.type !== 'body' || !chunk.node) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const owner = partsById.get(partIdOf(chunk.node.partNr));
|
|
269
|
+
if (!owner) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (owner.part.bodyRange && owner.part.bodyRange.end !== start) {
|
|
273
|
+
owner.nonContiguous = true;
|
|
274
|
+
}
|
|
275
|
+
owner.part.bodyRange = { start: owner.part.bodyRange ? owner.part.bodyRange.start : start, end: offset };
|
|
276
|
+
owner.part.rawBodySize += value.length;
|
|
277
|
+
if (owner.collecting) {
|
|
278
|
+
// Guard against a single enormous text part exhausting memory. Collect a little more
|
|
279
|
+
// than the cap so that the truncation warning is accurate.
|
|
280
|
+
if (owner.bodyBytes < opts.maxTextPartBytes * 2) {
|
|
281
|
+
owner.bodyChunks.push(value);
|
|
282
|
+
owner.bodyBytes += value.length;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
// The bundled typings narrow `on` to the 'data' event only, which shadows the inherited
|
|
287
|
+
// Transform overloads and makes the class structurally incompatible with Transform. Register
|
|
288
|
+
// lifecycle listeners through a minimal local view instead.
|
|
289
|
+
const lifecycle = splitter;
|
|
290
|
+
lifecycle.on('error', (err) => {
|
|
291
|
+
warnings.push({ code: 'mime_parse_error', detail: errorLabel(err) });
|
|
292
|
+
finish();
|
|
293
|
+
});
|
|
294
|
+
lifecycle.on('end', finish);
|
|
295
|
+
lifecycle.on('close', finish);
|
|
296
|
+
try {
|
|
297
|
+
splitter.end(raw);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
warnings.push({ code: 'mime_parse_error', detail: errorLabel(err) });
|
|
301
|
+
finish();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
for (const pending of pendingParts) {
|
|
305
|
+
if (pending.nonContiguous) {
|
|
306
|
+
warnings.push({ code: 'non_contiguous_body', mimePart: pending.part.partId });
|
|
307
|
+
}
|
|
308
|
+
if (pending.collecting && pending.bodyBytes > 0) {
|
|
309
|
+
pending.part.decoded = await decodeTextPart(pending, opts.maxTextPartBytes, warnings);
|
|
310
|
+
}
|
|
311
|
+
else if (pending.collecting) {
|
|
312
|
+
pending.part.decoded = {
|
|
313
|
+
text: '',
|
|
314
|
+
charsetUsed: pending.part.charset || 'utf-8',
|
|
315
|
+
charsetDeclared: pending.part.charset,
|
|
316
|
+
replacementChars: 0,
|
|
317
|
+
truncated: false
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
// Release references to raw chunks as soon as they are no longer needed.
|
|
321
|
+
pending.bodyChunks.length = 0;
|
|
322
|
+
}
|
|
323
|
+
const parts = pendingParts.map(pending => pending.part);
|
|
324
|
+
const byId = new Map(parts.map(part => [part.partId, part]));
|
|
325
|
+
const rootHeaders = (0, headers_1.toHeaderEntries)(rootHeaderList);
|
|
326
|
+
if (!parts.length) {
|
|
327
|
+
warnings.push({ code: 'mime_parse_error', detail: 'no MIME nodes produced' });
|
|
328
|
+
}
|
|
329
|
+
if (!parts.some(part => TEXT_TYPES.has(part.contentType))) {
|
|
330
|
+
warnings.push({ code: 'no_body_parts' });
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
parts,
|
|
334
|
+
partsById: byId,
|
|
335
|
+
rootId,
|
|
336
|
+
headers: (0, headers_1.extractMessageHeaders)(rootHeaders),
|
|
337
|
+
rootHeaders,
|
|
338
|
+
warnings,
|
|
339
|
+
size: raw.length,
|
|
340
|
+
structure: structureSignature(parts, byId, rootId)
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
//# sourceMappingURL=parse.js.map
|