@zone-eu/mailsplit 5.4.9 → 5.4.11
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/index.d.ts +100 -0
- package/index.js +4 -1
- package/lib/chunked-passthrough.d.ts +59 -0
- package/lib/chunked-passthrough.js +16 -0
- package/lib/flowed-decoder.d.ts +59 -0
- package/lib/flowed-decoder.js +23 -6
- package/lib/headers.d.ts +117 -0
- package/lib/headers.js +120 -25
- package/lib/message-joiner.d.ts +56 -0
- package/lib/message-joiner.js +18 -0
- package/lib/message-splitter.d.ts +59 -0
- package/lib/message-splitter.js +68 -9
- package/lib/mime-node.d.ts +144 -0
- package/lib/mime-node.js +129 -47
- package/lib/node-rewriter.d.ts +105 -0
- package/lib/node-rewriter.js +66 -21
- package/lib/node-streamer.d.ts +105 -0
- package/lib/node-streamer.js +66 -13
- package/lib/types.d.ts +285 -0
- package/package.json +15 -5
package/lib/headers.js
CHANGED
|
@@ -2,76 +2,120 @@
|
|
|
2
2
|
|
|
3
3
|
const libmime = require('libmime');
|
|
4
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
|
+
|
|
5
11
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
12
|
+
* Parses and builds message headers. A Headers instance allows callers to
|
|
13
|
+
* inspect, delete, update, and add header lines.
|
|
8
14
|
*/
|
|
9
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
|
+
*/
|
|
10
20
|
constructor(headers, config) {
|
|
11
21
|
config = config || {};
|
|
12
22
|
|
|
13
23
|
if (Array.isArray(headers)) {
|
|
14
24
|
// already using parsed headers
|
|
15
25
|
this.changed = true;
|
|
26
|
+
/** @type {string | Buffer | false} */
|
|
16
27
|
this.headers = false;
|
|
17
28
|
this.parsed = true;
|
|
29
|
+
/** @type {HeaderLine[] | false} */
|
|
18
30
|
this.lines = headers;
|
|
19
31
|
} else {
|
|
20
32
|
// using original string/buffer headers
|
|
21
33
|
this.changed = false;
|
|
22
|
-
|
|
34
|
+
/** @type {string | Buffer | false} */
|
|
35
|
+
this.headers = headers || false;
|
|
23
36
|
this.parsed = false;
|
|
37
|
+
/** @type {HeaderLine[] | false} */
|
|
24
38
|
this.lines = false;
|
|
25
39
|
}
|
|
40
|
+
/** @type {string | false} */
|
|
26
41
|
this.mbox = false;
|
|
42
|
+
/** @type {string | false} */
|
|
27
43
|
this.http = false;
|
|
28
44
|
|
|
29
|
-
this.libmime = new
|
|
45
|
+
this.libmime = new Libmime({ Iconv: config.Iconv });
|
|
30
46
|
}
|
|
31
47
|
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} key
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
32
52
|
hasHeader(key) {
|
|
33
53
|
if (!this.parsed) {
|
|
34
54
|
this._parseHeaders();
|
|
35
55
|
}
|
|
56
|
+
let lines = this._getLines();
|
|
36
57
|
key = this._normalizeHeader(key);
|
|
37
|
-
return typeof
|
|
58
|
+
return typeof lines.find(line => line.key === key) === 'object';
|
|
38
59
|
}
|
|
39
60
|
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} key
|
|
63
|
+
* @returns {string[]}
|
|
64
|
+
*/
|
|
40
65
|
get(key) {
|
|
41
66
|
if (!this.parsed) {
|
|
42
67
|
this._parseHeaders();
|
|
43
68
|
}
|
|
69
|
+
let headerLines = this._getLines();
|
|
44
70
|
key = this._normalizeHeader(key);
|
|
45
|
-
let lines =
|
|
71
|
+
let lines = headerLines.filter(line => line.key === key).map(line => this._decodeHeaderValue(line.line));
|
|
46
72
|
|
|
47
73
|
return lines;
|
|
48
74
|
}
|
|
49
75
|
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} key
|
|
78
|
+
* @returns {DecodedHeader[]}
|
|
79
|
+
*/
|
|
50
80
|
getDecoded(key) {
|
|
51
81
|
return this.get(key)
|
|
52
82
|
.map(line => this.libmime.decodeHeader(line))
|
|
53
83
|
.filter(line => line && line.value);
|
|
54
84
|
}
|
|
55
85
|
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} key
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
56
90
|
getFirst(key) {
|
|
57
91
|
if (!this.parsed) {
|
|
58
92
|
this._parseHeaders();
|
|
59
93
|
}
|
|
94
|
+
let lines = this._getLines();
|
|
60
95
|
key = this._normalizeHeader(key);
|
|
61
|
-
let header =
|
|
96
|
+
let header = lines.find(line => line.key === key);
|
|
62
97
|
if (!header) {
|
|
63
98
|
return '';
|
|
64
99
|
}
|
|
65
100
|
return ((this.libmime.decodeHeader(this._decodeHeaderValue(header.line)) || {}).value || '').toString().trim();
|
|
66
101
|
}
|
|
67
102
|
|
|
103
|
+
/**
|
|
104
|
+
* @returns {HeaderLine[]}
|
|
105
|
+
*/
|
|
68
106
|
getList() {
|
|
69
107
|
if (!this.parsed) {
|
|
70
108
|
this._parseHeaders();
|
|
71
109
|
}
|
|
72
|
-
return this.
|
|
110
|
+
return this._getLines();
|
|
73
111
|
}
|
|
74
112
|
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} key
|
|
115
|
+
* @param {string | number | Buffer} [value]
|
|
116
|
+
* @param {number} [index]
|
|
117
|
+
* @returns {void}
|
|
118
|
+
*/
|
|
75
119
|
add(key, value, index) {
|
|
76
120
|
if (typeof value === 'undefined') {
|
|
77
121
|
return;
|
|
@@ -89,10 +133,17 @@ class Headers {
|
|
|
89
133
|
this.addFormatted(key, this.libmime.foldLines(key + ': ' + value.replace(/\r?\n/g, ''), 76, false), index);
|
|
90
134
|
}
|
|
91
135
|
|
|
136
|
+
/**
|
|
137
|
+
* @param {string} key
|
|
138
|
+
* @param {string | Buffer | false} [line]
|
|
139
|
+
* @param {number} [index]
|
|
140
|
+
* @returns {void}
|
|
141
|
+
*/
|
|
92
142
|
addFormatted(key, line, index) {
|
|
93
143
|
if (!this.parsed) {
|
|
94
144
|
this._parseHeaders();
|
|
95
145
|
}
|
|
146
|
+
let lines = this._getLines();
|
|
96
147
|
index = index || 0;
|
|
97
148
|
this.changed = true;
|
|
98
149
|
|
|
@@ -110,45 +161,57 @@ class Headers {
|
|
|
110
161
|
};
|
|
111
162
|
|
|
112
163
|
if (index < 1) {
|
|
113
|
-
|
|
114
|
-
} else if (index >=
|
|
115
|
-
|
|
164
|
+
lines.unshift(header);
|
|
165
|
+
} else if (index >= lines.length) {
|
|
166
|
+
lines.push(header);
|
|
116
167
|
} else {
|
|
117
|
-
|
|
168
|
+
lines.splice(index, 0, header);
|
|
118
169
|
}
|
|
119
170
|
}
|
|
120
171
|
|
|
172
|
+
/**
|
|
173
|
+
* @param {string} key
|
|
174
|
+
* @returns {void}
|
|
175
|
+
*/
|
|
121
176
|
remove(key) {
|
|
122
177
|
if (!this.parsed) {
|
|
123
178
|
this._parseHeaders();
|
|
124
179
|
}
|
|
180
|
+
let lines = this._getLines();
|
|
125
181
|
key = this._normalizeHeader(key);
|
|
126
|
-
for (let i =
|
|
127
|
-
if (
|
|
182
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
183
|
+
if (lines[i].key === key) {
|
|
128
184
|
this.changed = true;
|
|
129
|
-
|
|
185
|
+
lines.splice(i, 1);
|
|
130
186
|
}
|
|
131
187
|
}
|
|
132
188
|
}
|
|
133
189
|
|
|
190
|
+
/**
|
|
191
|
+
* @param {string} key
|
|
192
|
+
* @param {string | number | Buffer} [value]
|
|
193
|
+
* @param {number} [relativeIndex]
|
|
194
|
+
* @returns {void}
|
|
195
|
+
*/
|
|
134
196
|
update(key, value, relativeIndex) {
|
|
135
197
|
if (!this.parsed) {
|
|
136
198
|
this._parseHeaders();
|
|
137
199
|
}
|
|
200
|
+
let lines = this._getLines();
|
|
138
201
|
let keyName = key;
|
|
139
202
|
let index = 0;
|
|
140
203
|
key = this._normalizeHeader(key);
|
|
141
204
|
let relativeIndexCount = 0;
|
|
142
205
|
let relativeMatchFound = false;
|
|
143
|
-
for (let i =
|
|
144
|
-
if (
|
|
206
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
207
|
+
if (lines[i].key === key) {
|
|
145
208
|
if (relativeIndex && relativeIndex !== relativeIndexCount) {
|
|
146
209
|
relativeIndexCount++;
|
|
147
210
|
continue;
|
|
148
211
|
}
|
|
149
212
|
index = i;
|
|
150
213
|
this.changed = true;
|
|
151
|
-
|
|
214
|
+
lines.splice(i, 1);
|
|
152
215
|
if (relativeIndex) {
|
|
153
216
|
relativeMatchFound = true;
|
|
154
217
|
break;
|
|
@@ -163,18 +226,23 @@ class Headers {
|
|
|
163
226
|
this.add(keyName, value, index);
|
|
164
227
|
}
|
|
165
228
|
|
|
229
|
+
/**
|
|
230
|
+
* @param {string | false} [lineEnd]
|
|
231
|
+
* @returns {Buffer}
|
|
232
|
+
*/
|
|
166
233
|
build(lineEnd) {
|
|
167
234
|
if (!this.changed && !lineEnd) {
|
|
168
|
-
return typeof this.headers === 'string' ? Buffer.from(this.headers, 'binary') : this.headers;
|
|
235
|
+
return typeof this.headers === 'string' ? Buffer.from(this.headers, 'binary') : this.headers || Buffer.alloc(0);
|
|
169
236
|
}
|
|
170
237
|
|
|
171
238
|
if (!this.parsed) {
|
|
172
239
|
this._parseHeaders();
|
|
173
240
|
}
|
|
241
|
+
let lines = this._getLines();
|
|
174
242
|
|
|
175
243
|
lineEnd = lineEnd || '\r\n';
|
|
176
244
|
|
|
177
|
-
let headers =
|
|
245
|
+
let headers = lines
|
|
178
246
|
.map(line => this._buildHeaderLine(line.line.replace(/\r?\n/g, lineEnd)))
|
|
179
247
|
.reduce((joined, line, idx) => {
|
|
180
248
|
if (idx) {
|
|
@@ -182,7 +250,7 @@ class Headers {
|
|
|
182
250
|
}
|
|
183
251
|
joined.push(line);
|
|
184
252
|
return joined;
|
|
185
|
-
}, []);
|
|
253
|
+
}, /** @type {Buffer[]} */ ([]));
|
|
186
254
|
|
|
187
255
|
headers.push(Buffer.from(lineEnd + lineEnd, 'binary'));
|
|
188
256
|
|
|
@@ -197,10 +265,27 @@ class Headers {
|
|
|
197
265
|
return Buffer.concat(headers);
|
|
198
266
|
}
|
|
199
267
|
|
|
268
|
+
/**
|
|
269
|
+
* @param {string} key
|
|
270
|
+
* @returns {string}
|
|
271
|
+
*/
|
|
200
272
|
_normalizeHeader(key) {
|
|
201
273
|
return (key || '').toLowerCase().trim();
|
|
202
274
|
}
|
|
203
275
|
|
|
276
|
+
/**
|
|
277
|
+
* @returns {HeaderLine[]}
|
|
278
|
+
*/
|
|
279
|
+
_getLines() {
|
|
280
|
+
if (!this.lines) {
|
|
281
|
+
this.lines = [];
|
|
282
|
+
}
|
|
283
|
+
return this.lines;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @returns {void}
|
|
288
|
+
*/
|
|
204
289
|
_parseHeaders() {
|
|
205
290
|
if (!this.headers) {
|
|
206
291
|
this.lines = [];
|
|
@@ -208,18 +293,20 @@ class Headers {
|
|
|
208
293
|
return;
|
|
209
294
|
}
|
|
210
295
|
|
|
296
|
+
/** @type {Array<string | HeaderLine>} */
|
|
211
297
|
let lines = this.headers
|
|
212
298
|
.toString('binary')
|
|
213
299
|
.replace(/[\r\n]+$/, '')
|
|
214
300
|
.split(/\r?\n/);
|
|
215
301
|
|
|
216
302
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
217
|
-
let
|
|
303
|
+
let currentLine = /** @type {string} */ (lines[i]);
|
|
304
|
+
let chr = currentLine.charAt(0);
|
|
218
305
|
if (i && (chr === ' ' || chr === '\t')) {
|
|
219
|
-
lines[i - 1]
|
|
306
|
+
lines[i - 1] = /** @type {string} */ (lines[i - 1]) + '\r\n' + currentLine;
|
|
220
307
|
lines.splice(i, 1);
|
|
221
308
|
} else {
|
|
222
|
-
let line =
|
|
309
|
+
let line = currentLine;
|
|
223
310
|
if (!i && /^From /i.test(line)) {
|
|
224
311
|
// mbox file
|
|
225
312
|
this.mbox = line;
|
|
@@ -239,15 +326,23 @@ class Headers {
|
|
|
239
326
|
}
|
|
240
327
|
}
|
|
241
328
|
|
|
242
|
-
this.lines = lines;
|
|
329
|
+
this.lines = /** @type {HeaderLine[]} */ (lines);
|
|
243
330
|
this.parsed = true;
|
|
244
331
|
}
|
|
245
332
|
|
|
333
|
+
/**
|
|
334
|
+
* @param {string} line
|
|
335
|
+
* @returns {Buffer}
|
|
336
|
+
*/
|
|
246
337
|
_buildHeaderLine(line) {
|
|
247
338
|
let value = this._decodeHeaderValue(line);
|
|
248
339
|
return Buffer.from(value, value === line ? 'binary' : 'utf8');
|
|
249
340
|
}
|
|
250
341
|
|
|
342
|
+
/**
|
|
343
|
+
* @param {string} str
|
|
344
|
+
* @returns {string}
|
|
345
|
+
*/
|
|
251
346
|
_decodeHeaderValue(str) {
|
|
252
347
|
if (!str) {
|
|
253
348
|
return str;
|
|
@@ -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;
|
package/lib/message-joiner.js
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const Transform = require('stream').Transform;
|
|
4
4
|
|
|
5
|
+
/** @typedef {import('..').SplitterChunk} SplitterChunk */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Transform stream that joins splitter objects back into raw message bytes.
|
|
9
|
+
*/
|
|
5
10
|
class MessageJoiner extends Transform {
|
|
11
|
+
/**
|
|
12
|
+
* Creates a joiner that accepts splitter objects and emits Buffer chunks.
|
|
13
|
+
*/
|
|
6
14
|
constructor() {
|
|
7
15
|
let options = {
|
|
8
16
|
readableObjectMode: false,
|
|
@@ -11,6 +19,12 @@ class MessageJoiner extends Transform {
|
|
|
11
19
|
super(options);
|
|
12
20
|
}
|
|
13
21
|
|
|
22
|
+
/**
|
|
23
|
+
* @param {SplitterChunk | Buffer} obj
|
|
24
|
+
* @param {BufferEncoding} encoding
|
|
25
|
+
* @param {import('stream').TransformCallback} callback
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*/
|
|
14
28
|
_transform(obj, encoding, callback) {
|
|
15
29
|
if (Buffer.isBuffer(obj)) {
|
|
16
30
|
this.push(obj);
|
|
@@ -22,6 +36,10 @@ class MessageJoiner extends Transform {
|
|
|
22
36
|
return callback();
|
|
23
37
|
}
|
|
24
38
|
|
|
39
|
+
/**
|
|
40
|
+
* @param {import('stream').TransformCallback} callback
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
25
43
|
_flush(callback) {
|
|
26
44
|
return callback();
|
|
27
45
|
}
|
|
@@ -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;
|
package/lib/message-splitter.js
CHANGED
|
@@ -3,13 +3,26 @@
|
|
|
3
3
|
const Transform = require('stream').Transform;
|
|
4
4
|
const MimeNode = require('./mime-node');
|
|
5
5
|
|
|
6
|
+
/** @typedef {import('..').MimeNode} MimeNodeType */
|
|
7
|
+
/** @typedef {import('..').MessageChunk} MessageChunk */
|
|
8
|
+
/** @typedef {import('..').SplitterChunk} SplitterChunk */
|
|
9
|
+
/** @typedef {import('..').SplitterGroup} SplitterGroup */
|
|
10
|
+
/** @typedef {import('..').SplitterOptions} SplitterOptions */
|
|
11
|
+
/** @typedef {(err?: (Error & {code?: string}) | null, data?: SplitterChunk | MessageChunk | false, flush?: boolean) => void} ProcessLineCallback */
|
|
12
|
+
|
|
6
13
|
const MAX_HEAD_SIZE = 1 * 1024 * 1024;
|
|
7
14
|
const MAX_CHILD_NODES = 1000;
|
|
8
15
|
|
|
9
16
|
const HEAD = 0x01;
|
|
10
17
|
const BODY = 0x02;
|
|
11
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Transform stream that splits raw email bytes into MIME node and content chunks.
|
|
21
|
+
*/
|
|
12
22
|
class MessageSplitter extends Transform {
|
|
23
|
+
/**
|
|
24
|
+
* @param {SplitterOptions} [config]
|
|
25
|
+
*/
|
|
13
26
|
constructor(config) {
|
|
14
27
|
let options = {
|
|
15
28
|
readableObjectMode: true,
|
|
@@ -20,25 +33,41 @@ class MessageSplitter extends Transform {
|
|
|
20
33
|
this.config = config || {};
|
|
21
34
|
this.maxHeadSize = this.config.maxHeadSize || MAX_HEAD_SIZE;
|
|
22
35
|
this.maxChildNodes = this.config.maxChildNodes || MAX_CHILD_NODES;
|
|
36
|
+
/** @type {MimeNodeType[]} */
|
|
23
37
|
this.tree = [];
|
|
24
38
|
this.nodeCounter = 0;
|
|
39
|
+
this.node = /** @type {MimeNodeType} */ (/** @type {unknown} */ (null));
|
|
25
40
|
this.newNode();
|
|
26
41
|
this.tree.push(this.node);
|
|
42
|
+
/** @type {Buffer | false} */
|
|
27
43
|
this.line = false;
|
|
28
44
|
this.hasFailed = false;
|
|
29
45
|
}
|
|
30
46
|
|
|
47
|
+
/**
|
|
48
|
+
* @param {Buffer} chunk
|
|
49
|
+
* @param {BufferEncoding} encoding
|
|
50
|
+
* @param {import('stream').TransformCallback} callback
|
|
51
|
+
* @returns {void}
|
|
52
|
+
*/
|
|
31
53
|
_transform(chunk, encoding, callback) {
|
|
32
54
|
// process line by line
|
|
33
55
|
// find next line ending
|
|
34
56
|
let pos = 0;
|
|
35
57
|
let i = 0;
|
|
58
|
+
/** @type {SplitterGroup} */
|
|
36
59
|
let group = {
|
|
37
60
|
type: 'none'
|
|
38
61
|
};
|
|
39
62
|
let groupstart = this.line ? -this.line.length : 0;
|
|
40
63
|
let groupend = 0;
|
|
41
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Removes a pending line break from body data that belongs to a following boundary.
|
|
67
|
+
*
|
|
68
|
+
* @param {MessageChunk} data Body chunk to adjust in place.
|
|
69
|
+
* @returns {void}
|
|
70
|
+
*/
|
|
42
71
|
let checkTrailingLinebreak = data => {
|
|
43
72
|
if (data.type === 'body' && data.node.parentNode && data.value && data.value.length) {
|
|
44
73
|
if (data.value[data.value.length - 1] === 0x0a) {
|
|
@@ -67,6 +96,11 @@ class MessageSplitter extends Transform {
|
|
|
67
96
|
}
|
|
68
97
|
};
|
|
69
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Iterates the current input chunk line by line and emits parsed groups.
|
|
101
|
+
*
|
|
102
|
+
* @returns {void}
|
|
103
|
+
*/
|
|
70
104
|
let iterateData = () => {
|
|
71
105
|
for (let len = chunk.length; i < len; i++) {
|
|
72
106
|
// find next <LF>
|
|
@@ -88,7 +122,7 @@ class MessageSplitter extends Transform {
|
|
|
88
122
|
|
|
89
123
|
if (flush) {
|
|
90
124
|
if (group && group.type !== 'none') {
|
|
91
|
-
if (group.type === 'body' && groupend >= groupstart && group.node.parentNode) {
|
|
125
|
+
if (group.type === 'body' && groupend >= groupstart && group.node && group.node.parentNode) {
|
|
92
126
|
// do not include the last line ending for body
|
|
93
127
|
if (chunk[groupend - 1] === 0x0a) {
|
|
94
128
|
groupend--;
|
|
@@ -99,7 +133,7 @@ class MessageSplitter extends Transform {
|
|
|
99
133
|
}
|
|
100
134
|
if (groupstart !== groupend) {
|
|
101
135
|
group.value = chunk.slice(groupstart, groupend);
|
|
102
|
-
if (groupend < i) {
|
|
136
|
+
if (groupend < i && 'value' in data) {
|
|
103
137
|
data.value = chunk.slice(groupend, i);
|
|
104
138
|
}
|
|
105
139
|
}
|
|
@@ -118,7 +152,7 @@ class MessageSplitter extends Transform {
|
|
|
118
152
|
// shift slice end position forward
|
|
119
153
|
groupend = i;
|
|
120
154
|
} else {
|
|
121
|
-
if (group.type === 'body' && groupend >= groupstart && group.node.parentNode) {
|
|
155
|
+
if (group.type === 'body' && groupend >= groupstart && group.node && group.node.parentNode) {
|
|
122
156
|
// do not include the last line ending for body
|
|
123
157
|
if (chunk[groupend - 1] === 0x0a) {
|
|
124
158
|
groupend--;
|
|
@@ -165,7 +199,7 @@ class MessageSplitter extends Transform {
|
|
|
165
199
|
}
|
|
166
200
|
|
|
167
201
|
// skip last linebreak for body
|
|
168
|
-
if (pos >= groupstart + 1 && group.type === 'body' && group.node.parentNode) {
|
|
202
|
+
if (pos >= groupstart + 1 && group.type === 'body' && group.node && group.node.parentNode) {
|
|
169
203
|
// do not include the last line ending for body
|
|
170
204
|
if (chunk[pos - 1] === 0x0a) {
|
|
171
205
|
pos--;
|
|
@@ -200,6 +234,10 @@ class MessageSplitter extends Transform {
|
|
|
200
234
|
setImmediate(iterateData);
|
|
201
235
|
}
|
|
202
236
|
|
|
237
|
+
/**
|
|
238
|
+
* @param {import('stream').TransformCallback} callback
|
|
239
|
+
* @returns {void}
|
|
240
|
+
*/
|
|
203
241
|
_flush(callback) {
|
|
204
242
|
if (this.hasFailed) {
|
|
205
243
|
return callback();
|
|
@@ -215,6 +253,12 @@ class MessageSplitter extends Transform {
|
|
|
215
253
|
});
|
|
216
254
|
}
|
|
217
255
|
|
|
256
|
+
/**
|
|
257
|
+
* @param {Buffer} line
|
|
258
|
+
* @param {number} startpos
|
|
259
|
+
* @param {Buffer} boundary
|
|
260
|
+
* @returns {1 | 2 | false}
|
|
261
|
+
*/
|
|
218
262
|
compareBoundary(line, startpos, boundary) {
|
|
219
263
|
// --{boundary}\r\n or --{boundary}--\r\n
|
|
220
264
|
if (line.length < boundary.length + 3 + startpos || line.length > boundary.length + 6 + startpos) {
|
|
@@ -256,6 +300,10 @@ class MessageSplitter extends Transform {
|
|
|
256
300
|
return 2;
|
|
257
301
|
}
|
|
258
302
|
|
|
303
|
+
/**
|
|
304
|
+
* @param {Buffer} line
|
|
305
|
+
* @returns {1 | 2 | 3 | 4 | false}
|
|
306
|
+
*/
|
|
259
307
|
checkBoundary(line) {
|
|
260
308
|
let startpos = 0;
|
|
261
309
|
if (line.length >= 1 && (line[0] === 0x0d || line[0] === 0x0a)) {
|
|
@@ -269,6 +317,7 @@ class MessageSplitter extends Transform {
|
|
|
269
317
|
return false;
|
|
270
318
|
}
|
|
271
319
|
|
|
320
|
+
/** @type {1 | 2 | false} */
|
|
272
321
|
let boundary;
|
|
273
322
|
if (this.node._boundary && (boundary = this.compareBoundary(line, startpos, this.node._boundary))) {
|
|
274
323
|
// 1: next child
|
|
@@ -279,12 +328,18 @@ class MessageSplitter extends Transform {
|
|
|
279
328
|
if (this.node._parentBoundary && (boundary = this.compareBoundary(line, startpos, this.node._parentBoundary))) {
|
|
280
329
|
// 3: next sibling
|
|
281
330
|
// 4: parent end
|
|
282
|
-
return boundary + 2;
|
|
331
|
+
return /** @type {3 | 4} */ (boundary + 2);
|
|
283
332
|
}
|
|
284
333
|
|
|
285
334
|
return false;
|
|
286
335
|
}
|
|
287
336
|
|
|
337
|
+
/**
|
|
338
|
+
* @param {Buffer | false} line
|
|
339
|
+
* @param {boolean} final
|
|
340
|
+
* @param {ProcessLineCallback} next
|
|
341
|
+
* @returns {void}
|
|
342
|
+
*/
|
|
288
343
|
processLine(line, final, next) {
|
|
289
344
|
let flush = false;
|
|
290
345
|
|
|
@@ -301,7 +356,7 @@ class MessageSplitter extends Transform {
|
|
|
301
356
|
}
|
|
302
357
|
|
|
303
358
|
if (this.nodeCounter > this.maxChildNodes) {
|
|
304
|
-
let err = new Error('Max allowed child nodes exceeded');
|
|
359
|
+
let err = /** @type {Error & {code?: string}} */ (new Error('Max allowed child nodes exceeded'));
|
|
305
360
|
err.code = 'EMAXLEN';
|
|
306
361
|
return next(err);
|
|
307
362
|
}
|
|
@@ -339,7 +394,7 @@ class MessageSplitter extends Transform {
|
|
|
339
394
|
}
|
|
340
395
|
// move up
|
|
341
396
|
if (this.tree.length) {
|
|
342
|
-
this.node = this.tree.pop();
|
|
397
|
+
this.node = /** @type {MimeNodeType} */ (this.tree.pop());
|
|
343
398
|
}
|
|
344
399
|
this.state = BODY;
|
|
345
400
|
break;
|
|
@@ -360,7 +415,7 @@ class MessageSplitter extends Transform {
|
|
|
360
415
|
case HEAD: {
|
|
361
416
|
this.node.addHeaderChunk(line);
|
|
362
417
|
if (this.node._headerlen > this.maxHeadSize) {
|
|
363
|
-
let err = new Error('Max header size for a MIME node exceeded');
|
|
418
|
+
let err = /** @type {Error & {code?: string}} */ (new Error('Max header size for a MIME node exceeded'));
|
|
364
419
|
err.code = 'EMAXLEN';
|
|
365
420
|
return next(err);
|
|
366
421
|
}
|
|
@@ -412,8 +467,12 @@ class MessageSplitter extends Transform {
|
|
|
412
467
|
next(null, false);
|
|
413
468
|
}
|
|
414
469
|
|
|
470
|
+
/**
|
|
471
|
+
* @param {MimeNodeType | false} [parent]
|
|
472
|
+
* @returns {void}
|
|
473
|
+
*/
|
|
415
474
|
newNode(parent) {
|
|
416
|
-
this.node = new MimeNode(parent || false, this.config);
|
|
475
|
+
this.node = /** @type {MimeNodeType} */ (new MimeNode(parent || false, this.config));
|
|
417
476
|
this.state = HEAD;
|
|
418
477
|
this.nodeCounter++;
|
|
419
478
|
}
|