@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
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Transform = require('stream').Transform;
|
|
4
|
+
const MimeNode = require('./mime-node');
|
|
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
|
+
|
|
13
|
+
const MAX_HEAD_SIZE = 1 * 1024 * 1024;
|
|
14
|
+
const MAX_CHILD_NODES = 1000;
|
|
15
|
+
|
|
16
|
+
// how much of a body line without a line break is buffered before it is flushed
|
|
17
|
+
// out as regular content instead of being kept in memory
|
|
18
|
+
const MAX_PENDING_LINE_SIZE = 64 * 1024;
|
|
19
|
+
|
|
20
|
+
// how many separate writes the pending line may be kept in before it is compacted
|
|
21
|
+
const MAX_PENDING_LINE_CHUNKS = 1024;
|
|
22
|
+
|
|
23
|
+
// what a delimiter line may carry after the boundary value: the "--" prefix, an optional
|
|
24
|
+
// "--" suffix and the line terminator. This is the bound compareBoundary() accepts.
|
|
25
|
+
const BOUNDARY_LINE_SUFFIX = 2 /* "--" prefix */ + 2 /* "--" suffix */ + 2; /* trailing <CR><LF> */
|
|
26
|
+
|
|
27
|
+
// checkBoundary() additionally allows a line ending in front of the delimiter, so this is
|
|
28
|
+
// the longest a delimiter line can ever be once the boundary value is subtracted
|
|
29
|
+
const BOUNDARY_LINE_OVERHEAD = BOUNDARY_LINE_SUFFIX + 2; /* leading <CR><LF> */
|
|
30
|
+
|
|
31
|
+
const HEAD = 0x01;
|
|
32
|
+
const BODY = 0x02;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates the error used for all size limit violations.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} message Human readable error message.
|
|
38
|
+
* @returns {Error & {code: string}} Error tagged with the EMAXLEN code.
|
|
39
|
+
*/
|
|
40
|
+
function maxLenError(message) {
|
|
41
|
+
let err = /** @type {Error & {code: string}} */ (new Error(message));
|
|
42
|
+
err.code = 'EMAXLEN';
|
|
43
|
+
return err;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Moves an end offset back over the line ending that closes a body line, because the line
|
|
48
|
+
* ending in front of a boundary belongs to the delimiter and not to the part content. Only
|
|
49
|
+
* a group holding the body of a child node carries such a line ending, anything else is
|
|
50
|
+
* returned untouched.
|
|
51
|
+
*
|
|
52
|
+
* @param {SplitterGroup} group Group the offsets describe.
|
|
53
|
+
* @param {Buffer} chunk Chunk the offsets point into.
|
|
54
|
+
* @param {number} start Start offset of the body slice.
|
|
55
|
+
* @param {number} end End offset of the body slice.
|
|
56
|
+
* @returns {number} End offset with a trailing <CR><LF>, <LF> or nothing removed.
|
|
57
|
+
*/
|
|
58
|
+
function trimBodyLineEnd(group, chunk, start, end) {
|
|
59
|
+
if (group.type !== 'body' || !group.node || !group.node.parentNode) {
|
|
60
|
+
return end;
|
|
61
|
+
}
|
|
62
|
+
if (end > start && chunk[end - 1] === 0x0a) {
|
|
63
|
+
end--;
|
|
64
|
+
if (end > start && chunk[end - 1] === 0x0d) {
|
|
65
|
+
end--;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return end;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Transform stream that splits raw email bytes into MIME node and content chunks.
|
|
73
|
+
*/
|
|
74
|
+
class MessageSplitter extends Transform {
|
|
75
|
+
/**
|
|
76
|
+
* @param {SplitterOptions} [config]
|
|
77
|
+
*/
|
|
78
|
+
constructor(config) {
|
|
79
|
+
let options = {
|
|
80
|
+
readableObjectMode: true,
|
|
81
|
+
writableObjectMode: false
|
|
82
|
+
};
|
|
83
|
+
super(options);
|
|
84
|
+
|
|
85
|
+
this.config = config || {};
|
|
86
|
+
this.maxHeadSize = this.config.maxHeadSize || MAX_HEAD_SIZE;
|
|
87
|
+
this.maxChildNodes = this.config.maxChildNodes || MAX_CHILD_NODES;
|
|
88
|
+
this.nodeCounter = 0;
|
|
89
|
+
this.node = /** @type {MimeNodeType} */ (/** @type {unknown} */ (null));
|
|
90
|
+
// set once the closing delimiter of the current node's multipart has been seen, so
|
|
91
|
+
// that any later boundary line of that node counts as epilogue. Reset per node.
|
|
92
|
+
this.inEpilogue = false;
|
|
93
|
+
this.newNode();
|
|
94
|
+
// incomplete trailing line of the previous chunk, kept as a list of chunks so
|
|
95
|
+
// that a long line without a line break is not copied over for every write
|
|
96
|
+
/** @type {Buffer[]} */
|
|
97
|
+
this.lineChunks = [];
|
|
98
|
+
this.lineLength = 0;
|
|
99
|
+
this.hasFailed = false;
|
|
100
|
+
// set when the pending line was flushed as overlong content, the remainder
|
|
101
|
+
// of that same line can not be a boundary either
|
|
102
|
+
this.pendingLineTruncated = false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Appends unterminated trailing data to the pending line.
|
|
107
|
+
*
|
|
108
|
+
* @param {Buffer} chunk Data that follows the last line break of a write.
|
|
109
|
+
* @returns {void}
|
|
110
|
+
*/
|
|
111
|
+
appendPendingLine(chunk) {
|
|
112
|
+
if (!chunk.length) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.lineChunks.push(chunk);
|
|
116
|
+
this.lineLength += chunk.length;
|
|
117
|
+
if (this.lineChunks.length >= MAX_PENDING_LINE_CHUNKS) {
|
|
118
|
+
// a line written one byte at a time would otherwise cost an array slot and a
|
|
119
|
+
// Buffer view per byte, which is far more memory than the data itself
|
|
120
|
+
this.lineChunks = [Buffer.concat(this.lineChunks, this.lineLength)];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Returns the pending line as a single buffer and clears the pending state.
|
|
126
|
+
*
|
|
127
|
+
* @returns {Buffer | false} Pending line contents or false if there was none.
|
|
128
|
+
*/
|
|
129
|
+
takePendingLine() {
|
|
130
|
+
if (!this.lineLength) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
let line = this.lineChunks.length === 1 ? this.lineChunks[0] : Buffer.concat(this.lineChunks, this.lineLength);
|
|
134
|
+
this.lineChunks = [];
|
|
135
|
+
this.lineLength = 0;
|
|
136
|
+
return line;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {Buffer} chunk
|
|
141
|
+
* @param {BufferEncoding} encoding
|
|
142
|
+
* @param {import('stream').TransformCallback} callback
|
|
143
|
+
* @returns {void}
|
|
144
|
+
*/
|
|
145
|
+
_transform(chunk, encoding, callback) {
|
|
146
|
+
// process line by line
|
|
147
|
+
// find next line ending
|
|
148
|
+
let pos = 0;
|
|
149
|
+
let i = 0;
|
|
150
|
+
/** @type {SplitterGroup} */
|
|
151
|
+
let group = {
|
|
152
|
+
type: 'none'
|
|
153
|
+
};
|
|
154
|
+
let groupstart = this.lineLength ? -this.lineLength : 0;
|
|
155
|
+
let groupend = 0;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Removes a pending line break from body data that belongs to a following boundary.
|
|
159
|
+
*
|
|
160
|
+
* @param {MessageChunk} data Body chunk to adjust in place.
|
|
161
|
+
* @returns {void}
|
|
162
|
+
*/
|
|
163
|
+
let checkTrailingLinebreak = data => {
|
|
164
|
+
if (data.type === 'body' && data.node.parentNode && data.value && data.value.length) {
|
|
165
|
+
if (data.value[data.value.length - 1] === 0x0a) {
|
|
166
|
+
groupstart--;
|
|
167
|
+
groupend--;
|
|
168
|
+
pos--;
|
|
169
|
+
if (data.value.length > 1 && data.value[data.value.length - 2] === 0x0d) {
|
|
170
|
+
groupstart--;
|
|
171
|
+
groupend--;
|
|
172
|
+
pos--;
|
|
173
|
+
if (groupstart < 0 && !this.lineLength) {
|
|
174
|
+
// store only <CR> as <LF> should be on the positive side
|
|
175
|
+
this.appendPendingLine(Buffer.from([0x0d]));
|
|
176
|
+
}
|
|
177
|
+
data.value = data.value.slice(0, data.value.length - 2);
|
|
178
|
+
} else {
|
|
179
|
+
data.value = data.value.slice(0, data.value.length - 1);
|
|
180
|
+
}
|
|
181
|
+
} else if (data.value[data.value.length - 1] === 0x0d) {
|
|
182
|
+
groupstart--;
|
|
183
|
+
groupend--;
|
|
184
|
+
pos--;
|
|
185
|
+
data.value = data.value.slice(0, data.value.length - 1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Iterates the current input chunk line by line and emits parsed groups.
|
|
192
|
+
*
|
|
193
|
+
* @returns {void}
|
|
194
|
+
*/
|
|
195
|
+
let iterateData = () => {
|
|
196
|
+
for (let len = chunk.length; i < len; i++) {
|
|
197
|
+
// find next <LF>
|
|
198
|
+
if (chunk[i] === 0x0a) {
|
|
199
|
+
// line end
|
|
200
|
+
|
|
201
|
+
let start = Math.max(pos, 0);
|
|
202
|
+
pos = ++i;
|
|
203
|
+
|
|
204
|
+
return this.processLine(chunk.slice(start, i), false, (err, data, flush) => {
|
|
205
|
+
if (err) {
|
|
206
|
+
this.hasFailed = true;
|
|
207
|
+
return setImmediate(() => callback(err));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!data) {
|
|
211
|
+
return setImmediate(iterateData);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (flush) {
|
|
215
|
+
if (group && group.type !== 'none') {
|
|
216
|
+
// do not include the last line ending for body
|
|
217
|
+
groupend = trimBodyLineEnd(group, chunk, groupstart, groupend);
|
|
218
|
+
if (groupstart < groupend) {
|
|
219
|
+
// re-slice, the value the line was emitted with may
|
|
220
|
+
// still include the line ending we just trimmed
|
|
221
|
+
group.value = chunk.slice(groupstart, groupend);
|
|
222
|
+
if (groupend < i && 'value' in data) {
|
|
223
|
+
// the trimmed line ending belongs to the boundary line
|
|
224
|
+
data.value = chunk.slice(groupend, i);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// the group is pushed even when nothing is left of it, so that a
|
|
228
|
+
// part whose whole body is a line ending still reports a body
|
|
229
|
+
this.push(group);
|
|
230
|
+
group = {
|
|
231
|
+
type: 'none'
|
|
232
|
+
};
|
|
233
|
+
groupstart = groupend = i;
|
|
234
|
+
}
|
|
235
|
+
this.push(data);
|
|
236
|
+
groupend = i;
|
|
237
|
+
return setImmediate(iterateData);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (data.type === group.type) {
|
|
241
|
+
// shift slice end position forward
|
|
242
|
+
groupend = i;
|
|
243
|
+
} else {
|
|
244
|
+
// do not include the last line ending for body
|
|
245
|
+
groupend = trimBodyLineEnd(group, chunk, groupstart, groupend);
|
|
246
|
+
|
|
247
|
+
if (group.type !== 'none' && group.type !== 'node') {
|
|
248
|
+
// we have a previous data/body chunk to output
|
|
249
|
+
if (groupstart !== groupend) {
|
|
250
|
+
group.value = chunk.slice(groupstart, groupend);
|
|
251
|
+
if (group.value && group.value.length) {
|
|
252
|
+
this.push(group);
|
|
253
|
+
group = {
|
|
254
|
+
type: 'none'
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (data.type === 'node') {
|
|
261
|
+
this.push(data);
|
|
262
|
+
groupstart = i;
|
|
263
|
+
groupend = i;
|
|
264
|
+
} else if (groupstart < 0) {
|
|
265
|
+
groupstart = i;
|
|
266
|
+
groupend = i;
|
|
267
|
+
checkTrailingLinebreak(data);
|
|
268
|
+
if (data.value && data.value.length) {
|
|
269
|
+
this.push(data);
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
// start new body/data chunk
|
|
273
|
+
group = data;
|
|
274
|
+
groupstart = groupend;
|
|
275
|
+
groupend = i;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return setImmediate(iterateData);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// skip last linebreak for body
|
|
284
|
+
pos = trimBodyLineEnd(group, chunk, groupstart, pos);
|
|
285
|
+
|
|
286
|
+
if (group.type !== 'none' && group.type !== 'node' && pos > groupstart) {
|
|
287
|
+
// we have a leftover data/body chunk to push out
|
|
288
|
+
group.value = chunk.slice(groupstart, pos);
|
|
289
|
+
|
|
290
|
+
if (group.value && group.value.length) {
|
|
291
|
+
this.push(group);
|
|
292
|
+
group = {
|
|
293
|
+
type: 'none'
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (pos < chunk.length) {
|
|
299
|
+
// checkTrailingLinebreak can push pos before the start of this write when a
|
|
300
|
+
// line ending straddles it. A negative start would make slice() count from
|
|
301
|
+
// the END of the buffer and hand over the wrong bytes entirely.
|
|
302
|
+
this.appendPendingLine(chunk.slice(Math.max(pos, 0)));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let pendingLineError = this.enforcePendingLineLimit();
|
|
306
|
+
if (pendingLineError) {
|
|
307
|
+
this.hasFailed = true;
|
|
308
|
+
return callback(pendingLineError);
|
|
309
|
+
}
|
|
310
|
+
callback();
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
setImmediate(iterateData);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @param {import('stream').TransformCallback} callback
|
|
318
|
+
* @returns {void}
|
|
319
|
+
*/
|
|
320
|
+
_flush(callback) {
|
|
321
|
+
if (this.hasFailed) {
|
|
322
|
+
return callback();
|
|
323
|
+
}
|
|
324
|
+
this.processLine(false, true, (err, data) => {
|
|
325
|
+
if (err) {
|
|
326
|
+
return setImmediate(() => callback(err));
|
|
327
|
+
}
|
|
328
|
+
if (data && (data.type === 'node' || (data.value && data.value.length))) {
|
|
329
|
+
this.push(data);
|
|
330
|
+
}
|
|
331
|
+
callback();
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* @param {Buffer} line
|
|
337
|
+
* @param {number} startpos
|
|
338
|
+
* @param {Buffer} boundary
|
|
339
|
+
* @returns {1 | 2 | false}
|
|
340
|
+
*/
|
|
341
|
+
compareBoundary(line, startpos, boundary) {
|
|
342
|
+
// --{boundary}\r\n or --{boundary}--\r\n
|
|
343
|
+
if (line.length < boundary.length + 3 + startpos || line.length > boundary.length + BOUNDARY_LINE_SUFFIX + startpos) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
for (let i = 0; i < boundary.length; i++) {
|
|
347
|
+
if (line[i + 2 + startpos] !== boundary[i]) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let pos = 0;
|
|
353
|
+
for (let i = boundary.length + 2 + startpos; i < line.length; i++) {
|
|
354
|
+
let c = line[i];
|
|
355
|
+
if (pos === 0 && (c === 0x0d || c === 0x0a)) {
|
|
356
|
+
// 1: next node
|
|
357
|
+
return 1;
|
|
358
|
+
}
|
|
359
|
+
if (pos === 0 && c !== 0x2d) {
|
|
360
|
+
// expecting "-"
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
if (pos === 1 && c !== 0x2d) {
|
|
364
|
+
// expecting "-"
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
if (pos === 2 && c !== 0x0d && c !== 0x0a) {
|
|
368
|
+
// expecting line terminator, either <CR> or <LF>
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
if (pos === 3 && c !== 0x0a) {
|
|
372
|
+
// expecting line terminator <LF>
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
pos++;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 2: multipart end
|
|
379
|
+
return 2;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* @param {Buffer} line
|
|
384
|
+
* @returns {1 | 2 | 3 | 4 | false}
|
|
385
|
+
*/
|
|
386
|
+
checkBoundary(line) {
|
|
387
|
+
let startpos = 0;
|
|
388
|
+
if (line.length >= 1 && (line[0] === 0x0d || line[0] === 0x0a)) {
|
|
389
|
+
startpos++;
|
|
390
|
+
if (line.length >= 2 && line[0] === 0x0d && line[1] === 0x0a) {
|
|
391
|
+
// only <CR><LF> is two bytes, a lone <CR> in front of a delimiter is one
|
|
392
|
+
startpos++;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (line.length < 4 || line[startpos] !== 0x2d || line[startpos + 1] !== 0x2d) {
|
|
396
|
+
// defnitely not a boundary
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** @type {1 | 2 | false} */
|
|
401
|
+
let boundary;
|
|
402
|
+
if (!this.inEpilogue && this.node._boundary && (boundary = this.compareBoundary(line, startpos, this.node._boundary))) {
|
|
403
|
+
// 1: next child
|
|
404
|
+
// 2: multipart end
|
|
405
|
+
return boundary;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (this.node._parentBoundary && (boundary = this.compareBoundary(line, startpos, this.node._parentBoundary))) {
|
|
409
|
+
// 3: next sibling
|
|
410
|
+
// 4: parent end
|
|
411
|
+
return /** @type {3 | 4} */ (boundary + 2);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Checks the header bytes collected for the current node against maxHeadSize.
|
|
419
|
+
*
|
|
420
|
+
* @param {number} [extra] Bytes that belong to the header block but are not stored yet.
|
|
421
|
+
* @returns {(Error & {code?: string}) | null} Error object if the limit was exceeded.
|
|
422
|
+
*/
|
|
423
|
+
checkHeadSize(extra) {
|
|
424
|
+
if (this.node._headerlen + (extra || 0) > this.maxHeadSize) {
|
|
425
|
+
return maxLenError('Max header size for a MIME node exceeded');
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Enforces the limits on the pending line so that it can not grow without bound.
|
|
432
|
+
* A line that is still short enough to become a boundary delimiter is always kept.
|
|
433
|
+
* Past that length it is a header line and counts against maxHeadSize, or it is
|
|
434
|
+
* body content, in which case it is pushed out rather than held in memory. Flushing
|
|
435
|
+
* marks the pending line truncated, so the tail of it is not tested as a delimiter.
|
|
436
|
+
*
|
|
437
|
+
* @returns {(Error & {code?: string}) | null} Error object if a limit was exceeded.
|
|
438
|
+
*/
|
|
439
|
+
enforcePendingLineLimit() {
|
|
440
|
+
if (!this.lineLength) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let maxBoundaryLength = Math.max(
|
|
445
|
+
this.node._boundary ? this.node._boundary.length : 0,
|
|
446
|
+
this.node._parentBoundary ? this.node._parentBoundary.length : 0
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
if (this.lineLength <= maxBoundaryLength + BOUNDARY_LINE_OVERHEAD) {
|
|
450
|
+
// might still turn out to be a boundary delimiter line
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (this.state === HEAD) {
|
|
455
|
+
// not a boundary line, so it is a header line and counts against the
|
|
456
|
+
// header size limit even though it has not been stored on the node yet
|
|
457
|
+
return this.checkHeadSize(this.lineLength);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (this.lineLength < MAX_PENDING_LINE_SIZE) {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
let value = /** @type {Buffer} */ (this.takePendingLine());
|
|
465
|
+
if (value[value.length - 1] === 0x0d) {
|
|
466
|
+
// a trailing <CR> may still turn out to be the first half of the line ending
|
|
467
|
+
// that closes this line, and a line ending in front of a boundary belongs to
|
|
468
|
+
// the delimiter. Keep it pending so the normal trimming can decide. Copy it
|
|
469
|
+
// rather than slicing, a view would pin the whole flushed buffer.
|
|
470
|
+
this.appendPendingLine(Buffer.from([0x0d]));
|
|
471
|
+
value = value.slice(0, value.length - 1);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
this.push({
|
|
475
|
+
node: this.node,
|
|
476
|
+
type: this.node.multipart ? 'data' : 'body',
|
|
477
|
+
value
|
|
478
|
+
});
|
|
479
|
+
// whatever follows continues an overlong line, so the tail of it
|
|
480
|
+
// can not be a boundary line either
|
|
481
|
+
this.pendingLineTruncated = true;
|
|
482
|
+
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* @param {Buffer | false} line
|
|
488
|
+
* @param {boolean} final
|
|
489
|
+
* @param {ProcessLineCallback} next
|
|
490
|
+
* @returns {void}
|
|
491
|
+
*/
|
|
492
|
+
processLine(line, final, next) {
|
|
493
|
+
let flush = false;
|
|
494
|
+
|
|
495
|
+
// consumed here so that no later branch can leak it into the next line
|
|
496
|
+
let truncatedLine = this.pendingLineTruncated;
|
|
497
|
+
this.pendingLineTruncated = false;
|
|
498
|
+
|
|
499
|
+
let pending = this.takePendingLine();
|
|
500
|
+
if (pending) {
|
|
501
|
+
line = line ? Buffer.concat([pending, line]) : pending;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (!line) {
|
|
505
|
+
line = Buffer.alloc(0);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (this.nodeCounter > this.maxChildNodes) {
|
|
509
|
+
return next(maxLenError('Max allowed child nodes exceeded'));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// we check boundary outside the HEAD/BODY scope as it may appear anywhere
|
|
513
|
+
// unless the line is the remainder of an already flushed overlong line
|
|
514
|
+
let boundary = truncatedLine ? false : this.checkBoundary(line);
|
|
515
|
+
if (boundary) {
|
|
516
|
+
// reached boundary, switch context
|
|
517
|
+
switch (boundary) {
|
|
518
|
+
case 1:
|
|
519
|
+
// next child
|
|
520
|
+
this.newNode(this.node);
|
|
521
|
+
flush = true;
|
|
522
|
+
break;
|
|
523
|
+
case 2:
|
|
524
|
+
// reached end of children, keep current node
|
|
525
|
+
break;
|
|
526
|
+
case 3: {
|
|
527
|
+
// next sibling
|
|
528
|
+
this.newNode(this.parentMultipartNode());
|
|
529
|
+
flush = true;
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
case 4: {
|
|
533
|
+
// special case when boundary close a node with only header.
|
|
534
|
+
if (this.node && this.node._headerlen && !this.node.headers) {
|
|
535
|
+
this.node.parseHeaders();
|
|
536
|
+
this.push(this.node);
|
|
537
|
+
}
|
|
538
|
+
// move up to the multipart node this closing delimiter belongs to
|
|
539
|
+
let parentNode = this.parentMultipartNode();
|
|
540
|
+
if (parentNode) {
|
|
541
|
+
this.node = parentNode;
|
|
542
|
+
// the closing delimiter of this multipart was just processed, so any
|
|
543
|
+
// later boundary line of this node belongs to the epilogue. A closing
|
|
544
|
+
// delimiter seen in the preamble (case 2) deliberately does not count.
|
|
545
|
+
this.inEpilogue = true;
|
|
546
|
+
}
|
|
547
|
+
this.state = BODY;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return next(
|
|
553
|
+
null,
|
|
554
|
+
{
|
|
555
|
+
node: this.node,
|
|
556
|
+
type: 'data',
|
|
557
|
+
value: line
|
|
558
|
+
},
|
|
559
|
+
flush
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
switch (this.state) {
|
|
564
|
+
case HEAD: {
|
|
565
|
+
this.node.addHeaderChunk(line);
|
|
566
|
+
let headSizeError = this.checkHeadSize();
|
|
567
|
+
if (headSizeError) {
|
|
568
|
+
return next(headSizeError);
|
|
569
|
+
}
|
|
570
|
+
if (final || (line.length === 1 && line[0] === 0x0a) || (line.length === 2 && line[0] === 0x0d && line[1] === 0x0a)) {
|
|
571
|
+
let currentNode = this.node;
|
|
572
|
+
|
|
573
|
+
currentNode.parseHeaders();
|
|
574
|
+
|
|
575
|
+
// if the content is attached message then just continue
|
|
576
|
+
if (
|
|
577
|
+
currentNode.contentType === 'message/rfc822' &&
|
|
578
|
+
!this.config.ignoreEmbedded &&
|
|
579
|
+
(!currentNode.encoding || ['7bit', '8bit', 'binary'].includes(currentNode.encoding)) &&
|
|
580
|
+
(this.config.defaultInlineEmbedded ? currentNode.disposition !== 'attachment' : currentNode.disposition === 'inline')
|
|
581
|
+
) {
|
|
582
|
+
currentNode.messageNode = true;
|
|
583
|
+
this.newNode(currentNode);
|
|
584
|
+
if (currentNode.parentNode) {
|
|
585
|
+
// the embedded message continues inside its container, so a
|
|
586
|
+
// delimiter of the container's own parent still applies here
|
|
587
|
+
this.node._parentBoundary = currentNode.parentNode._boundary;
|
|
588
|
+
this.node._parentBoundaryOwner = currentNode.parentNode;
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
if (currentNode.contentType === 'message/rfc822') {
|
|
592
|
+
currentNode.messageNode = false;
|
|
593
|
+
}
|
|
594
|
+
this.state = BODY;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return next(null, currentNode, flush);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return next();
|
|
601
|
+
}
|
|
602
|
+
case BODY: {
|
|
603
|
+
return next(
|
|
604
|
+
null,
|
|
605
|
+
{
|
|
606
|
+
node: this.node,
|
|
607
|
+
type: this.node.multipart ? 'data' : 'body',
|
|
608
|
+
value: line
|
|
609
|
+
},
|
|
610
|
+
flush
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
next(null, false);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Resolves the multipart node that owns the boundary of the current node, ie. the
|
|
620
|
+
* node a sibling delimiter or a closing delimiter of _parentBoundary refers to.
|
|
621
|
+
*
|
|
622
|
+
* @returns {MimeNodeType | false} Owner of _parentBoundary or false for the root node.
|
|
623
|
+
*/
|
|
624
|
+
parentMultipartNode() {
|
|
625
|
+
return this.node._parentBoundaryOwner || false;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* @param {MimeNodeType | false} [parent]
|
|
630
|
+
* @returns {void}
|
|
631
|
+
*/
|
|
632
|
+
newNode(parent) {
|
|
633
|
+
this.node = /** @type {MimeNodeType} */ (new MimeNode(parent || false, this.config));
|
|
634
|
+
this.state = HEAD;
|
|
635
|
+
this.nodeCounter++;
|
|
636
|
+
// a fresh node starts before its own content, never in an epilogue
|
|
637
|
+
this.inEpilogue = false;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
module.exports = MessageSplitter;
|