@depup/zone-eu__mailsplit 5.4.16-depup.0
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.EUPL-1.2 +153 -0
- package/LICENSE.MIT +21 -0
- package/README.md +25 -0
- package/changes.json +5 -0
- package/index.d.ts +97 -0
- package/index.js +20 -0
- package/lib/chunked-passthrough.d.ts +59 -0
- package/lib/chunked-passthrough.js +53 -0
- package/lib/flowed-decoder.d.ts +59 -0
- package/lib/flowed-decoder.js +72 -0
- package/lib/headers.d.ts +117 -0
- package/lib/headers.js +428 -0
- package/lib/message-joiner.d.ts +56 -0
- package/lib/message-joiner.js +48 -0
- package/lib/message-splitter.d.ts +59 -0
- package/lib/message-splitter.js +641 -0
- package/lib/mime-node.d.ts +147 -0
- package/lib/mime-node.js +351 -0
- package/lib/node-rewriter.d.ts +105 -0
- package/lib/node-rewriter.js +239 -0
- package/lib/node-streamer.d.ts +105 -0
- package/lib/node-streamer.js +174 -0
- package/lib/types.d.ts +288 -0
- package/package.json +66 -0
package/lib/headers.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { DecodedHeader, HeaderLine, LibmimeOptions } from './types';
|
|
2
|
+
|
|
3
|
+
/** Mutable parser and builder for RFC-style message header blocks. */
|
|
4
|
+
declare class Headers {
|
|
5
|
+
/** Whether header lines have been modified after construction. */
|
|
6
|
+
changed: boolean;
|
|
7
|
+
|
|
8
|
+
/** Original unparsed header source, or `false` when constructed from parsed lines. */
|
|
9
|
+
headers: string | Buffer | false;
|
|
10
|
+
|
|
11
|
+
/** Whether `headers` has been parsed into `lines`. */
|
|
12
|
+
parsed: boolean;
|
|
13
|
+
|
|
14
|
+
/** Parsed header lines, or `false` until parsing occurs. */
|
|
15
|
+
lines: HeaderLine[] | false;
|
|
16
|
+
|
|
17
|
+
/** MBOX `From ` prefix line, or `false` when absent. */
|
|
18
|
+
mbox: string | false;
|
|
19
|
+
|
|
20
|
+
/** HTTP request prefix line, or `false` when absent. */
|
|
21
|
+
http: string | false;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a mutable header collection.
|
|
25
|
+
*
|
|
26
|
+
* @param headers Raw header bytes/string, already parsed header lines, or `false` for an empty collection.
|
|
27
|
+
* @param config Optional libmime configuration.
|
|
28
|
+
*/
|
|
29
|
+
constructor(headers?: string | Buffer | HeaderLine[] | false, config?: LibmimeOptions);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Checks whether at least one header with the requested key exists.
|
|
33
|
+
*
|
|
34
|
+
* @param key Header field name to find, case-insensitively.
|
|
35
|
+
* @returns `true` when the header exists.
|
|
36
|
+
*/
|
|
37
|
+
hasHeader(key: string): boolean;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gets all raw header lines for a key.
|
|
41
|
+
*
|
|
42
|
+
* @param key Header field name to find, case-insensitively.
|
|
43
|
+
* @returns Full decoded header lines, including field names.
|
|
44
|
+
*/
|
|
45
|
+
get(key: string): string[];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Gets all decoded structured header values for a key.
|
|
49
|
+
*
|
|
50
|
+
* @param key Header field name to decode, case-insensitively.
|
|
51
|
+
* @returns Decoded header entries with key and value fields.
|
|
52
|
+
*/
|
|
53
|
+
getDecoded(key: string): DecodedHeader[];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Gets the first decoded header value for a key.
|
|
57
|
+
*
|
|
58
|
+
* @param key Header field name to find, case-insensitively.
|
|
59
|
+
* @returns Trimmed decoded value, or an empty string when the header is absent.
|
|
60
|
+
*/
|
|
61
|
+
getFirst(key: string): string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Gets the mutable parsed header list.
|
|
65
|
+
*
|
|
66
|
+
* @returns Parsed header lines in message order.
|
|
67
|
+
*/
|
|
68
|
+
getList(): HeaderLine[];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Adds a folded header line.
|
|
72
|
+
*
|
|
73
|
+
* @param key Header field name to add.
|
|
74
|
+
* @param value Header value; `undefined` leaves the collection unchanged.
|
|
75
|
+
* @param index Insertion index, where omitted or less than 1 inserts at the top.
|
|
76
|
+
* @returns Nothing.
|
|
77
|
+
*/
|
|
78
|
+
add(key: string, value?: string | number | Buffer, index?: number): void;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Adds a preformatted header line.
|
|
82
|
+
*
|
|
83
|
+
* @param key Header field name used for normalized lookup.
|
|
84
|
+
* @param line Full header line to insert; falsy values leave the collection unchanged.
|
|
85
|
+
* @param index Insertion index, where omitted or less than 1 inserts at the top.
|
|
86
|
+
* @returns Nothing.
|
|
87
|
+
*/
|
|
88
|
+
addFormatted(key: string, line?: string | Buffer | false, index?: number): void;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Removes all headers matching a key.
|
|
92
|
+
*
|
|
93
|
+
* @param key Header field name to remove, case-insensitively.
|
|
94
|
+
* @returns Nothing.
|
|
95
|
+
*/
|
|
96
|
+
remove(key: string): void;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Replaces matching headers with a new folded header value.
|
|
100
|
+
*
|
|
101
|
+
* @param key Header field name to update.
|
|
102
|
+
* @param value Header value to write; `undefined` removes matching values without adding a replacement.
|
|
103
|
+
* @param relativeIndex Optional zero-based index among headers with the same key.
|
|
104
|
+
* @returns Nothing.
|
|
105
|
+
*/
|
|
106
|
+
update(key: string, value?: string | number | Buffer, relativeIndex?: number): void;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Builds a raw header block.
|
|
110
|
+
*
|
|
111
|
+
* @param lineEnd Line ending to use when rebuilding changed headers; defaults to CRLF.
|
|
112
|
+
* @returns Header bytes ending with an empty header/body separator line.
|
|
113
|
+
*/
|
|
114
|
+
build(lineEnd?: string | false): Buffer;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export = Headers;
|
package/lib/headers.js
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const libmime = require('libmime');
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('..').HeaderLine} HeaderLine */
|
|
6
|
+
/** @typedef {import('..').LibmimeOptions} LibmimeOptions */
|
|
7
|
+
/** @typedef {import('..').DecodedHeader} DecodedHeader */
|
|
8
|
+
|
|
9
|
+
const Libmime = /** @type {any} */ (libmime.Libmime);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parses and builds message headers. A Headers instance allows callers to
|
|
13
|
+
* inspect, delete, update, and add header lines.
|
|
14
|
+
*/
|
|
15
|
+
class Headers {
|
|
16
|
+
/**
|
|
17
|
+
* @param {string | Buffer | HeaderLine[] | false} [headers] Raw header source or already parsed lines.
|
|
18
|
+
* @param {LibmimeOptions} [config] Optional libmime configuration.
|
|
19
|
+
*/
|
|
20
|
+
constructor(headers, config) {
|
|
21
|
+
config = config || {};
|
|
22
|
+
|
|
23
|
+
if (Array.isArray(headers)) {
|
|
24
|
+
// already using parsed headers
|
|
25
|
+
this.changed = true;
|
|
26
|
+
/** @type {string | Buffer | false} */
|
|
27
|
+
this.headers = false;
|
|
28
|
+
this.parsed = true;
|
|
29
|
+
/** @type {HeaderLine[] | false} */
|
|
30
|
+
this.lines = headers;
|
|
31
|
+
} else {
|
|
32
|
+
// using original string/buffer headers
|
|
33
|
+
this.changed = false;
|
|
34
|
+
/** @type {string | Buffer | false} */
|
|
35
|
+
this.headers = headers || false;
|
|
36
|
+
this.parsed = false;
|
|
37
|
+
/** @type {HeaderLine[] | false} */
|
|
38
|
+
this.lines = false;
|
|
39
|
+
}
|
|
40
|
+
/** @type {string | false} */
|
|
41
|
+
this.mbox = false;
|
|
42
|
+
/** @type {string | false} */
|
|
43
|
+
this.http = false;
|
|
44
|
+
|
|
45
|
+
this.libmime = new Libmime({ Iconv: config.Iconv });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} key
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
hasHeader(key) {
|
|
53
|
+
if (!this.parsed) {
|
|
54
|
+
this._parseHeaders();
|
|
55
|
+
}
|
|
56
|
+
let lines = this._getLines();
|
|
57
|
+
key = this._normalizeHeader(key);
|
|
58
|
+
return typeof lines.find(line => line.key === key) === 'object';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} key
|
|
63
|
+
* @returns {string[]}
|
|
64
|
+
*/
|
|
65
|
+
get(key) {
|
|
66
|
+
if (!this.parsed) {
|
|
67
|
+
this._parseHeaders();
|
|
68
|
+
}
|
|
69
|
+
let headerLines = this._getLines();
|
|
70
|
+
key = this._normalizeHeader(key);
|
|
71
|
+
let lines = headerLines.filter(line => line.key === key).map(line => this._decodeHeaderValue(line.line));
|
|
72
|
+
|
|
73
|
+
return lines;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} key
|
|
78
|
+
* @returns {DecodedHeader[]}
|
|
79
|
+
*/
|
|
80
|
+
getDecoded(key) {
|
|
81
|
+
return this.get(key)
|
|
82
|
+
.map(line => this.libmime.decodeHeader(line))
|
|
83
|
+
.filter(line => line && line.value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} key
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
getFirst(key) {
|
|
91
|
+
if (!this.parsed) {
|
|
92
|
+
this._parseHeaders();
|
|
93
|
+
}
|
|
94
|
+
let lines = this._getLines();
|
|
95
|
+
key = this._normalizeHeader(key);
|
|
96
|
+
let header = lines.find(line => line.key === key);
|
|
97
|
+
if (!header) {
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
return ((this.libmime.decodeHeader(this._decodeHeaderValue(header.line)) || {}).value || '').toString().trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @returns {HeaderLine[]}
|
|
105
|
+
*/
|
|
106
|
+
getList() {
|
|
107
|
+
if (!this.parsed) {
|
|
108
|
+
this._parseHeaders();
|
|
109
|
+
}
|
|
110
|
+
return this._getLines();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} key
|
|
115
|
+
* @param {string | number | Buffer} [value]
|
|
116
|
+
* @param {number} [index]
|
|
117
|
+
* @returns {void}
|
|
118
|
+
*/
|
|
119
|
+
add(key, value, index) {
|
|
120
|
+
if (typeof value === 'undefined') {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof value === 'number') {
|
|
125
|
+
value = value.toString();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (typeof value === 'string') {
|
|
129
|
+
value = Buffer.from(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
value = value.toString('binary');
|
|
133
|
+
// a header value may not contain line breaks of its own, folding is added by foldLines
|
|
134
|
+
this.addFormatted(key, this.libmime.foldLines(key + ': ' + value.replace(/[\r\n]/g, ''), 76, false), index);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {string} key
|
|
139
|
+
* @param {string | Buffer | false} [line]
|
|
140
|
+
* @param {number} [index]
|
|
141
|
+
* @returns {void}
|
|
142
|
+
*/
|
|
143
|
+
addFormatted(key, line, index) {
|
|
144
|
+
if (!this.parsed) {
|
|
145
|
+
this._parseHeaders();
|
|
146
|
+
}
|
|
147
|
+
let lines = this._getLines();
|
|
148
|
+
index = index || 0;
|
|
149
|
+
this.changed = true;
|
|
150
|
+
|
|
151
|
+
if (!line) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (typeof line !== 'string') {
|
|
156
|
+
line = line.toString('binary');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// every header insertion runs through here, so this is where a value or a key
|
|
160
|
+
// built from untrusted input is stopped from injecting an extra header line
|
|
161
|
+
line = this._normalizeInsertedLine(line);
|
|
162
|
+
if (!line) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let header = {
|
|
167
|
+
key: this._normalizeHeader(key),
|
|
168
|
+
line
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
if (index < 1) {
|
|
172
|
+
lines.unshift(header);
|
|
173
|
+
} else if (index >= lines.length) {
|
|
174
|
+
lines.push(header);
|
|
175
|
+
} else {
|
|
176
|
+
lines.splice(index, 0, header);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {string} key
|
|
182
|
+
* @returns {void}
|
|
183
|
+
*/
|
|
184
|
+
remove(key) {
|
|
185
|
+
if (!this.parsed) {
|
|
186
|
+
this._parseHeaders();
|
|
187
|
+
}
|
|
188
|
+
let lines = this._getLines();
|
|
189
|
+
key = this._normalizeHeader(key);
|
|
190
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
191
|
+
if (lines[i].key === key) {
|
|
192
|
+
this.changed = true;
|
|
193
|
+
lines.splice(i, 1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @param {string} key
|
|
200
|
+
* @param {string | number | Buffer} [value]
|
|
201
|
+
* @param {number} [relativeIndex]
|
|
202
|
+
* @returns {void}
|
|
203
|
+
*/
|
|
204
|
+
update(key, value, relativeIndex) {
|
|
205
|
+
if (!this.parsed) {
|
|
206
|
+
this._parseHeaders();
|
|
207
|
+
}
|
|
208
|
+
let lines = this._getLines();
|
|
209
|
+
let keyName = key;
|
|
210
|
+
let index = 0;
|
|
211
|
+
key = this._normalizeHeader(key);
|
|
212
|
+
let relativeIndexCount = 0;
|
|
213
|
+
let relativeMatchFound = false;
|
|
214
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
215
|
+
if (lines[i].key === key) {
|
|
216
|
+
if (relativeIndex && relativeIndex !== relativeIndexCount) {
|
|
217
|
+
relativeIndexCount++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
index = i;
|
|
221
|
+
this.changed = true;
|
|
222
|
+
lines.splice(i, 1);
|
|
223
|
+
if (relativeIndex) {
|
|
224
|
+
relativeMatchFound = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (relativeIndex && !relativeMatchFound) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.add(keyName, value, index);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Serializes the headers. Unmodified headers are returned byte for byte as they
|
|
239
|
+
* were received, otherwise every line is rebuilt with `lineEnd` line endings.
|
|
240
|
+
*
|
|
241
|
+
* @param {string | false} [lineEnd] Line ending to use, defaults to CRLF.
|
|
242
|
+
* @returns {Buffer}
|
|
243
|
+
*/
|
|
244
|
+
build(lineEnd) {
|
|
245
|
+
if (!this.changed && !lineEnd) {
|
|
246
|
+
return typeof this.headers === 'string' ? Buffer.from(this.headers, 'binary') : this.headers || Buffer.alloc(0);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!this.parsed) {
|
|
250
|
+
this._parseHeaders();
|
|
251
|
+
}
|
|
252
|
+
let lines = this._getLines();
|
|
253
|
+
|
|
254
|
+
const ending = lineEnd || '\r\n';
|
|
255
|
+
|
|
256
|
+
let headers = lines
|
|
257
|
+
.map(line => this._normalizeLineBreaks(line.line, ending))
|
|
258
|
+
// an empty line would close the header block and demote every later header
|
|
259
|
+
// into the body, so a line left with nothing in it is dropped instead
|
|
260
|
+
.filter(line => line !== '')
|
|
261
|
+
.map(line => this._buildHeaderLine(line))
|
|
262
|
+
.reduce((joined, line, idx) => {
|
|
263
|
+
if (idx) {
|
|
264
|
+
joined.push(Buffer.from(ending, 'binary'));
|
|
265
|
+
}
|
|
266
|
+
joined.push(line);
|
|
267
|
+
return joined;
|
|
268
|
+
}, /** @type {Buffer[]} */ ([]));
|
|
269
|
+
|
|
270
|
+
headers.push(Buffer.from(ending + ending, 'binary'));
|
|
271
|
+
|
|
272
|
+
if (this.mbox) {
|
|
273
|
+
headers.unshift(Buffer.from(this.mbox + ending, 'binary'));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (this.http) {
|
|
277
|
+
headers.unshift(Buffer.from(this.http + ending, 'binary'));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return Buffer.concat(headers);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* @param {string} key
|
|
285
|
+
* @returns {string}
|
|
286
|
+
*/
|
|
287
|
+
_normalizeHeader(key) {
|
|
288
|
+
return (key || '').toLowerCase().trim();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Rewrites the line breaks of a header line so that the line can only ever parse
|
|
293
|
+
* back as the single header it was reported as. A line break followed by whitespace
|
|
294
|
+
* is folding and becomes `lineEnd`, every other line break would start a new header
|
|
295
|
+
* line and is dropped.
|
|
296
|
+
*
|
|
297
|
+
* A bare <CR> is never a line break for _parseHeaders, so promoting one here would
|
|
298
|
+
* emit a header line that was never reported as parsed.
|
|
299
|
+
*
|
|
300
|
+
* @param {string} line Header line to normalize.
|
|
301
|
+
* @param {string} lineEnd Line ending to fold with.
|
|
302
|
+
* @returns {string} Line with only folding line breaks left.
|
|
303
|
+
*/
|
|
304
|
+
_normalizeLineBreaks(line, lineEnd) {
|
|
305
|
+
return (
|
|
306
|
+
line
|
|
307
|
+
// lines are joined with lineEnd, so a line that opens with a break of its own
|
|
308
|
+
// would close the header block. Dropping only the break leaves any whitespace
|
|
309
|
+
// behind it folding into the line before, which adds no header of its own.
|
|
310
|
+
.replace(/^[\r\n]+/, '')
|
|
311
|
+
.replace(/\r\n|\r|\n/g, (match, offset, source) => (match !== '\r' && this._isFoldingChar(source.charAt(offset + match.length)) ? lineEnd : ''))
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Prepares a caller supplied line for insertion. On top of the line break rules an
|
|
317
|
+
* inserted line has to stand on its own: a leading fold or indent would attach it to
|
|
318
|
+
* whichever header happens to precede it, and a leading line break would close the
|
|
319
|
+
* header block outright.
|
|
320
|
+
*
|
|
321
|
+
* Lines that were parsed out of a message keep their leading whitespace instead, so
|
|
322
|
+
* that rebuilding can never turn an indented continuation into a header of its own.
|
|
323
|
+
*
|
|
324
|
+
* An inserted line is normalized twice, here with CRLF and again in build() with the
|
|
325
|
+
* line ending the caller asked for. That is only sound because _normalizeLineBreaks is
|
|
326
|
+
* idempotent over its own output: the folds this pass emits are still recognized as
|
|
327
|
+
* folds by the next one. Any change to how a fold is represented has to keep that true.
|
|
328
|
+
*
|
|
329
|
+
* @param {string} line Formatted header line supplied by the caller.
|
|
330
|
+
* @returns {string} Line that inserts as exactly one header, or an empty string.
|
|
331
|
+
*/
|
|
332
|
+
_normalizeInsertedLine(line) {
|
|
333
|
+
return this._normalizeLineBreaks(line.replace(/^[\r\n \t]+/, ''), '\r\n');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Tells whether a character continues the previous header line rather than
|
|
338
|
+
* starting a new one. Used by both the parser and the builder so that the two
|
|
339
|
+
* can not disagree on what folding is.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} chr Character that follows a line break.
|
|
342
|
+
* @returns {boolean} True if the line break is folding.
|
|
343
|
+
*/
|
|
344
|
+
_isFoldingChar(chr) {
|
|
345
|
+
return chr === ' ' || chr === '\t';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @returns {HeaderLine[]}
|
|
350
|
+
*/
|
|
351
|
+
_getLines() {
|
|
352
|
+
if (!this.lines) {
|
|
353
|
+
this.lines = [];
|
|
354
|
+
}
|
|
355
|
+
return this.lines;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* @returns {void}
|
|
360
|
+
*/
|
|
361
|
+
_parseHeaders() {
|
|
362
|
+
if (!this.headers) {
|
|
363
|
+
this.lines = [];
|
|
364
|
+
this.parsed = true;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** @type {Array<string | HeaderLine>} */
|
|
369
|
+
let lines = this.headers
|
|
370
|
+
.toString('binary')
|
|
371
|
+
.replace(/[\r\n]+$/, '')
|
|
372
|
+
.split(/\r?\n/);
|
|
373
|
+
|
|
374
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
375
|
+
let currentLine = /** @type {string} */ (lines[i]);
|
|
376
|
+
if (i && this._isFoldingChar(currentLine.charAt(0))) {
|
|
377
|
+
lines[i - 1] = /** @type {string} */ (lines[i - 1]) + '\r\n' + currentLine;
|
|
378
|
+
lines.splice(i, 1);
|
|
379
|
+
} else {
|
|
380
|
+
let line = currentLine;
|
|
381
|
+
if (!i && /^From /i.test(line)) {
|
|
382
|
+
// mbox file
|
|
383
|
+
this.mbox = line;
|
|
384
|
+
lines.splice(i, 1);
|
|
385
|
+
continue;
|
|
386
|
+
} else if (!i && /^POST /i.test(line)) {
|
|
387
|
+
// HTTP POST request
|
|
388
|
+
this.http = line;
|
|
389
|
+
lines.splice(i, 1);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
let key = this._normalizeHeader(line.substr(0, line.indexOf(':')));
|
|
393
|
+
lines[i] = {
|
|
394
|
+
key,
|
|
395
|
+
line
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
this.lines = /** @type {HeaderLine[]} */ (lines);
|
|
401
|
+
this.parsed = true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* @param {string} line
|
|
406
|
+
* @returns {Buffer}
|
|
407
|
+
*/
|
|
408
|
+
_buildHeaderLine(line) {
|
|
409
|
+
let value = this._decodeHeaderValue(line);
|
|
410
|
+
return Buffer.from(value, value === line ? 'binary' : 'utf8');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* @param {string} str
|
|
415
|
+
* @returns {string}
|
|
416
|
+
*/
|
|
417
|
+
_decodeHeaderValue(str) {
|
|
418
|
+
if (!str) {
|
|
419
|
+
return str;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let utf8 = Buffer.from(str, 'binary').toString('utf8');
|
|
423
|
+
return utf8.includes('\uFFFD') ? str : utf8;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// expose to the world
|
|
428
|
+
module.exports = Headers;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Transform } from 'node:stream';
|
|
2
|
+
|
|
3
|
+
/** Transform stream that joins splitter objects back into raw email bytes. */
|
|
4
|
+
declare class MessageJoiner extends Transform {
|
|
5
|
+
/**
|
|
6
|
+
* Creates a joiner that accepts splitter objects and emits Buffer chunks.
|
|
7
|
+
*/
|
|
8
|
+
constructor();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Registers a listener for generated message bytes.
|
|
12
|
+
*
|
|
13
|
+
* @param event Event name.
|
|
14
|
+
* @param listener Receives each generated Buffer chunk.
|
|
15
|
+
* @returns This joiner instance.
|
|
16
|
+
*/
|
|
17
|
+
on(event: 'data', listener: (data: Buffer) => void): this;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Registers a one-time listener for generated message bytes.
|
|
21
|
+
*
|
|
22
|
+
* @param event Event name.
|
|
23
|
+
* @param listener Receives the next generated Buffer chunk.
|
|
24
|
+
* @returns This joiner instance.
|
|
25
|
+
*/
|
|
26
|
+
once(event: 'data', listener: (data: Buffer) => void): this;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Adds a listener for generated message bytes.
|
|
30
|
+
*
|
|
31
|
+
* @param event Event name.
|
|
32
|
+
* @param listener Receives each generated Buffer chunk.
|
|
33
|
+
* @returns This joiner instance.
|
|
34
|
+
*/
|
|
35
|
+
addListener(event: 'data', listener: (data: Buffer) => void): this;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Prepends a listener for generated message bytes.
|
|
39
|
+
*
|
|
40
|
+
* @param event Event name.
|
|
41
|
+
* @param listener Receives each generated Buffer chunk.
|
|
42
|
+
* @returns This joiner instance.
|
|
43
|
+
*/
|
|
44
|
+
prependListener(event: 'data', listener: (data: Buffer) => void): this;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Emits a generated message byte chunk.
|
|
48
|
+
*
|
|
49
|
+
* @param event Event name.
|
|
50
|
+
* @param data Buffer chunk to emit.
|
|
51
|
+
* @returns `true` when the event had listeners.
|
|
52
|
+
*/
|
|
53
|
+
emit(event: 'data', data: Buffer): boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export = MessageJoiner;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Transform = require('stream').Transform;
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('..').SplitterChunk} SplitterChunk */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Transform stream that joins splitter objects back into raw message bytes.
|
|
9
|
+
*/
|
|
10
|
+
class MessageJoiner extends Transform {
|
|
11
|
+
/**
|
|
12
|
+
* Creates a joiner that accepts splitter objects and emits Buffer chunks.
|
|
13
|
+
*/
|
|
14
|
+
constructor() {
|
|
15
|
+
let options = {
|
|
16
|
+
readableObjectMode: false,
|
|
17
|
+
writableObjectMode: true
|
|
18
|
+
};
|
|
19
|
+
super(options);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {SplitterChunk | Buffer} obj
|
|
24
|
+
* @param {BufferEncoding} encoding
|
|
25
|
+
* @param {import('stream').TransformCallback} callback
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*/
|
|
28
|
+
_transform(obj, encoding, callback) {
|
|
29
|
+
if (Buffer.isBuffer(obj)) {
|
|
30
|
+
this.push(obj);
|
|
31
|
+
} else if (obj.type === 'node') {
|
|
32
|
+
this.push(obj.getHeaders());
|
|
33
|
+
} else if (obj.value) {
|
|
34
|
+
this.push(obj.value);
|
|
35
|
+
}
|
|
36
|
+
return callback();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {import('stream').TransformCallback} callback
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
_flush(callback) {
|
|
44
|
+
return callback();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = MessageJoiner;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Transform } from 'node:stream';
|
|
2
|
+
import type { SplitterChunk, SplitterOptions } from './types';
|
|
3
|
+
|
|
4
|
+
/** Transform stream that splits raw email bytes into MIME node and content chunks. */
|
|
5
|
+
declare class MessageSplitter extends Transform {
|
|
6
|
+
/**
|
|
7
|
+
* Creates a splitter that accepts Buffer input and emits `SplitterChunk` objects.
|
|
8
|
+
*
|
|
9
|
+
* @param config Optional parser limits and embedded-message behavior.
|
|
10
|
+
*/
|
|
11
|
+
constructor(config?: SplitterOptions);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Registers a listener for parsed splitter chunks.
|
|
15
|
+
*
|
|
16
|
+
* @param event Event name.
|
|
17
|
+
* @param listener Receives each parsed MIME node, data chunk, or body chunk.
|
|
18
|
+
* @returns This splitter instance.
|
|
19
|
+
*/
|
|
20
|
+
on(event: 'data', listener: (data: SplitterChunk) => void): this;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Registers a one-time listener for the next parsed splitter chunk.
|
|
24
|
+
*
|
|
25
|
+
* @param event Event name.
|
|
26
|
+
* @param listener Receives the next parsed MIME node, data chunk, or body chunk.
|
|
27
|
+
* @returns This splitter instance.
|
|
28
|
+
*/
|
|
29
|
+
once(event: 'data', listener: (data: SplitterChunk) => void): this;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Adds a listener for parsed splitter chunks.
|
|
33
|
+
*
|
|
34
|
+
* @param event Event name.
|
|
35
|
+
* @param listener Receives each parsed MIME node, data chunk, or body chunk.
|
|
36
|
+
* @returns This splitter instance.
|
|
37
|
+
*/
|
|
38
|
+
addListener(event: 'data', listener: (data: SplitterChunk) => void): this;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Prepends a listener for parsed splitter chunks.
|
|
42
|
+
*
|
|
43
|
+
* @param event Event name.
|
|
44
|
+
* @param listener Receives each parsed MIME node, data chunk, or body chunk.
|
|
45
|
+
* @returns This splitter instance.
|
|
46
|
+
*/
|
|
47
|
+
prependListener(event: 'data', listener: (data: SplitterChunk) => void): this;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Emits a parsed splitter chunk.
|
|
51
|
+
*
|
|
52
|
+
* @param event Event name.
|
|
53
|
+
* @param data MIME node, data chunk, or body chunk to emit.
|
|
54
|
+
* @returns `true` when the event had listeners.
|
|
55
|
+
*/
|
|
56
|
+
emit(event: 'data', data: SplitterChunk): boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export = MessageSplitter;
|