@zone-eu/mailsplit 5.4.6

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/lib/headers.js ADDED
@@ -0,0 +1,238 @@
1
+ 'use strict';
2
+
3
+ const libmime = require('libmime');
4
+
5
+ /**
6
+ * Class Headers to parse and handle message headers. Headers instance allows to
7
+ * check existing, delete or add new headers
8
+ */
9
+ class Headers {
10
+ constructor(headers, config) {
11
+ config = config || {};
12
+
13
+ if (Array.isArray(headers)) {
14
+ // already using parsed headers
15
+ this.changed = true;
16
+ this.headers = false;
17
+ this.parsed = true;
18
+ this.lines = headers;
19
+ } else {
20
+ // using original string/buffer headers
21
+ this.changed = false;
22
+ this.headers = headers;
23
+ this.parsed = false;
24
+ this.lines = false;
25
+ }
26
+ this.mbox = false;
27
+ this.http = false;
28
+
29
+ this.libmime = new libmime.Libmime({ Iconv: config.Iconv });
30
+ }
31
+
32
+ hasHeader(key) {
33
+ if (!this.parsed) {
34
+ this._parseHeaders();
35
+ }
36
+ key = this._normalizeHeader(key);
37
+ return typeof this.lines.find(line => line.key === key) === 'object';
38
+ }
39
+
40
+ get(key) {
41
+ if (!this.parsed) {
42
+ this._parseHeaders();
43
+ }
44
+ key = this._normalizeHeader(key);
45
+ let lines = this.lines.filter(line => line.key === key).map(line => line.line);
46
+
47
+ return lines;
48
+ }
49
+
50
+ getDecoded(key) {
51
+ return this.get(key)
52
+ .map(line => this.libmime.decodeHeader(line))
53
+ .filter(line => line && line.value);
54
+ }
55
+
56
+ getFirst(key) {
57
+ if (!this.parsed) {
58
+ this._parseHeaders();
59
+ }
60
+ key = this._normalizeHeader(key);
61
+ let header = this.lines.find(line => line.key === key);
62
+ if (!header) {
63
+ return '';
64
+ }
65
+ return ((this.libmime.decodeHeader(header.line) || {}).value || '').toString().trim();
66
+ }
67
+
68
+ getList() {
69
+ if (!this.parsed) {
70
+ this._parseHeaders();
71
+ }
72
+ return this.lines;
73
+ }
74
+
75
+ add(key, value, index) {
76
+ if (typeof value === 'undefined') {
77
+ return;
78
+ }
79
+
80
+ if (typeof value === 'number') {
81
+ value = value.toString();
82
+ }
83
+
84
+ if (typeof value === 'string') {
85
+ value = Buffer.from(value);
86
+ }
87
+
88
+ value = value.toString('binary');
89
+ this.addFormatted(key, this.libmime.foldLines(key + ': ' + value.replace(/\r?\n/g, ''), 76, false), index);
90
+ }
91
+
92
+ addFormatted(key, line, index) {
93
+ if (!this.parsed) {
94
+ this._parseHeaders();
95
+ }
96
+ index = index || 0;
97
+ this.changed = true;
98
+
99
+ if (!line) {
100
+ return;
101
+ }
102
+
103
+ if (typeof line !== 'string') {
104
+ line = line.toString('binary');
105
+ }
106
+
107
+ let header = {
108
+ key: this._normalizeHeader(key),
109
+ line
110
+ };
111
+
112
+ if (index < 1) {
113
+ this.lines.unshift(header);
114
+ } else if (index >= this.lines.length) {
115
+ this.lines.push(header);
116
+ } else {
117
+ this.lines.splice(index, 0, header);
118
+ }
119
+ }
120
+
121
+ remove(key) {
122
+ if (!this.parsed) {
123
+ this._parseHeaders();
124
+ }
125
+ key = this._normalizeHeader(key);
126
+ for (let i = this.lines.length - 1; i >= 0; i--) {
127
+ if (this.lines[i].key === key) {
128
+ this.changed = true;
129
+ this.lines.splice(i, 1);
130
+ }
131
+ }
132
+ }
133
+
134
+ update(key, value, relativeIndex) {
135
+ if (!this.parsed) {
136
+ this._parseHeaders();
137
+ }
138
+ let keyName = key;
139
+ let index = 0;
140
+ key = this._normalizeHeader(key);
141
+ let relativeIndexCount = 0;
142
+ let relativeMatchFound = false;
143
+ for (let i = this.lines.length - 1; i >= 0; i--) {
144
+ if (this.lines[i].key === key) {
145
+ if (relativeIndex && relativeIndex !== relativeIndexCount) {
146
+ relativeIndexCount++;
147
+ continue;
148
+ }
149
+ index = i;
150
+ this.changed = true;
151
+ this.lines.splice(i, 1);
152
+ if (relativeIndex) {
153
+ relativeMatchFound = true;
154
+ break;
155
+ }
156
+ }
157
+ }
158
+
159
+ if (relativeIndex && !relativeMatchFound) {
160
+ return;
161
+ }
162
+
163
+ this.add(keyName, value, index);
164
+ }
165
+
166
+ build(lineEnd) {
167
+ if (!this.changed && !lineEnd) {
168
+ return typeof this.headers === 'string' ? Buffer.from(this.headers, 'binary') : this.headers;
169
+ }
170
+
171
+ if (!this.parsed) {
172
+ this._parseHeaders();
173
+ }
174
+
175
+ lineEnd = lineEnd || '\r\n';
176
+
177
+ let headers = this.lines.map(line => line.line.replace(/\r?\n/g, lineEnd)).join(lineEnd) + `${lineEnd}${lineEnd}`;
178
+
179
+ if (this.mbox) {
180
+ headers = this.mbox + lineEnd + headers;
181
+ }
182
+
183
+ if (this.http) {
184
+ headers = this.http + lineEnd + headers;
185
+ }
186
+
187
+ return Buffer.from(headers, 'binary');
188
+ }
189
+
190
+ _normalizeHeader(key) {
191
+ return (key || '').toLowerCase().trim();
192
+ }
193
+
194
+ _parseHeaders() {
195
+ if (!this.headers) {
196
+ this.lines = [];
197
+ this.parsed = true;
198
+ return;
199
+ }
200
+
201
+ let lines = this.headers
202
+ .toString('binary')
203
+ .replace(/[\r\n]+$/, '')
204
+ .split(/\r?\n/);
205
+
206
+ for (let i = lines.length - 1; i >= 0; i--) {
207
+ let chr = lines[i].charAt(0);
208
+ if (i && (chr === ' ' || chr === '\t')) {
209
+ lines[i - 1] += '\r\n' + lines[i];
210
+ lines.splice(i, 1);
211
+ } else {
212
+ let line = lines[i];
213
+ if (!i && /^From /i.test(line)) {
214
+ // mbox file
215
+ this.mbox = line;
216
+ lines.splice(i, 1);
217
+ continue;
218
+ } else if (!i && /^POST /i.test(line)) {
219
+ // HTTP POST request
220
+ this.http = line;
221
+ lines.splice(i, 1);
222
+ continue;
223
+ }
224
+ let key = this._normalizeHeader(line.substr(0, line.indexOf(':')));
225
+ lines[i] = {
226
+ key,
227
+ line
228
+ };
229
+ }
230
+ }
231
+
232
+ this.lines = lines;
233
+ this.parsed = true;
234
+ }
235
+ }
236
+
237
+ // expose to the world
238
+ module.exports = Headers;
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const Transform = require('stream').Transform;
4
+
5
+ class MessageJoiner extends Transform {
6
+ constructor() {
7
+ let options = {
8
+ readableObjectMode: false,
9
+ writableObjectMode: true
10
+ };
11
+ super(options);
12
+ }
13
+
14
+ _transform(obj, encoding, callback) {
15
+ if (Buffer.isBuffer(obj)) {
16
+ this.push(obj);
17
+ } else if (obj.type === 'node') {
18
+ this.push(obj.getHeaders());
19
+ } else if (obj.value) {
20
+ this.push(obj.value);
21
+ }
22
+ return callback();
23
+ }
24
+
25
+ _flush(callback) {
26
+ return callback();
27
+ }
28
+ }
29
+
30
+ module.exports = MessageJoiner;
@@ -0,0 +1,422 @@
1
+ 'use strict';
2
+
3
+ const Transform = require('stream').Transform;
4
+ const MimeNode = require('./mime-node');
5
+
6
+ const MAX_HEAD_SIZE = 1 * 1024 * 1024;
7
+ const MAX_CHILD_NODES = 1000;
8
+
9
+ const HEAD = 0x01;
10
+ const BODY = 0x02;
11
+
12
+ class MessageSplitter extends Transform {
13
+ constructor(config) {
14
+ let options = {
15
+ readableObjectMode: true,
16
+ writableObjectMode: false
17
+ };
18
+ super(options);
19
+
20
+ this.config = config || {};
21
+ this.maxHeadSize = this.config.maxHeadSize || MAX_HEAD_SIZE;
22
+ this.maxChildNodes = this.config.maxChildNodes || MAX_CHILD_NODES;
23
+ this.tree = [];
24
+ this.nodeCounter = 0;
25
+ this.newNode();
26
+ this.tree.push(this.node);
27
+ this.line = false;
28
+ this.hasFailed = false;
29
+ }
30
+
31
+ _transform(chunk, encoding, callback) {
32
+ // process line by line
33
+ // find next line ending
34
+ let pos = 0;
35
+ let i = 0;
36
+ let group = {
37
+ type: 'none'
38
+ };
39
+ let groupstart = this.line ? -this.line.length : 0;
40
+ let groupend = 0;
41
+
42
+ let checkTrailingLinebreak = data => {
43
+ if (data.type === 'body' && data.node.parentNode && data.value && data.value.length) {
44
+ if (data.value[data.value.length - 1] === 0x0a) {
45
+ groupstart--;
46
+ groupend--;
47
+ pos--;
48
+ if (data.value.length > 1 && data.value[data.value.length - 2] === 0x0d) {
49
+ groupstart--;
50
+ groupend--;
51
+ pos--;
52
+ if (groupstart < 0 && !this.line) {
53
+ // store only <CR> as <LF> should be on the positive side
54
+ this.line = Buffer.allocUnsafe(1);
55
+ this.line[0] = 0x0d;
56
+ }
57
+ data.value = data.value.slice(0, data.value.length - 2);
58
+ } else {
59
+ data.value = data.value.slice(0, data.value.length - 1);
60
+ }
61
+ } else if (data.value[data.value.length - 1] === 0x0d) {
62
+ groupstart--;
63
+ groupend--;
64
+ pos--;
65
+ data.value = data.value.slice(0, data.value.length - 1);
66
+ }
67
+ }
68
+ };
69
+
70
+ let iterateData = () => {
71
+ for (let len = chunk.length; i < len; i++) {
72
+ // find next <LF>
73
+ if (chunk[i] === 0x0a) {
74
+ // line end
75
+
76
+ let start = Math.max(pos, 0);
77
+ pos = ++i;
78
+
79
+ return this.processLine(chunk.slice(start, i), false, (err, data, flush) => {
80
+ if (err) {
81
+ this.hasFailed = true;
82
+ return setImmediate(() => callback(err));
83
+ }
84
+
85
+ if (!data) {
86
+ return setImmediate(iterateData);
87
+ }
88
+
89
+ if (flush) {
90
+ if (group && group.type !== 'none') {
91
+ if (group.type === 'body' && groupend >= groupstart && group.node.parentNode) {
92
+ // do not include the last line ending for body
93
+ if (chunk[groupend - 1] === 0x0a) {
94
+ groupend--;
95
+ if (groupend >= groupstart && chunk[groupend - 1] === 0x0d) {
96
+ groupend--;
97
+ }
98
+ }
99
+ }
100
+ if (groupstart !== groupend) {
101
+ group.value = chunk.slice(groupstart, groupend);
102
+ if (groupend < i) {
103
+ data.value = chunk.slice(groupend, i);
104
+ }
105
+ }
106
+ this.push(group);
107
+ group = {
108
+ type: 'none'
109
+ };
110
+ groupstart = groupend = i;
111
+ }
112
+ this.push(data);
113
+ groupend = i;
114
+ return setImmediate(iterateData);
115
+ }
116
+
117
+ if (data.type === group.type) {
118
+ // shift slice end position forward
119
+ groupend = i;
120
+ } else {
121
+ if (group.type === 'body' && groupend >= groupstart && group.node.parentNode) {
122
+ // do not include the last line ending for body
123
+ if (chunk[groupend - 1] === 0x0a) {
124
+ groupend--;
125
+ if (groupend >= groupstart && chunk[groupend - 1] === 0x0d) {
126
+ groupend--;
127
+ }
128
+ }
129
+ }
130
+
131
+ if (group.type !== 'none' && group.type !== 'node') {
132
+ // we have a previous data/body chunk to output
133
+ if (groupstart !== groupend) {
134
+ group.value = chunk.slice(groupstart, groupend);
135
+ if (group.value && group.value.length) {
136
+ this.push(group);
137
+ group = {
138
+ type: 'none'
139
+ };
140
+ }
141
+ }
142
+ }
143
+
144
+ if (data.type === 'node') {
145
+ this.push(data);
146
+ groupstart = i;
147
+ groupend = i;
148
+ } else if (groupstart < 0) {
149
+ groupstart = i;
150
+ groupend = i;
151
+ checkTrailingLinebreak(data);
152
+ if (data.value && data.value.length) {
153
+ this.push(data);
154
+ }
155
+ } else {
156
+ // start new body/data chunk
157
+ group = data;
158
+ groupstart = groupend;
159
+ groupend = i;
160
+ }
161
+ }
162
+ return setImmediate(iterateData);
163
+ });
164
+ }
165
+ }
166
+
167
+ // skip last linebreak for body
168
+ if (pos >= groupstart + 1 && group.type === 'body' && group.node.parentNode) {
169
+ // do not include the last line ending for body
170
+ if (chunk[pos - 1] === 0x0a) {
171
+ pos--;
172
+ if (pos >= groupstart && chunk[pos - 1] === 0x0d) {
173
+ pos--;
174
+ }
175
+ }
176
+ }
177
+
178
+ if (group.type !== 'none' && group.type !== 'node' && pos > groupstart) {
179
+ // we have a leftover data/body chunk to push out
180
+ group.value = chunk.slice(groupstart, pos);
181
+
182
+ if (group.value && group.value.length) {
183
+ this.push(group);
184
+ group = {
185
+ type: 'none'
186
+ };
187
+ }
188
+ }
189
+
190
+ if (pos < chunk.length) {
191
+ if (this.line) {
192
+ this.line = Buffer.concat([this.line, chunk.slice(pos)]);
193
+ } else {
194
+ this.line = chunk.slice(pos);
195
+ }
196
+ }
197
+ callback();
198
+ };
199
+
200
+ setImmediate(iterateData);
201
+ }
202
+
203
+ _flush(callback) {
204
+ if (this.hasFailed) {
205
+ return callback();
206
+ }
207
+ this.processLine(false, true, (err, data) => {
208
+ if (err) {
209
+ return setImmediate(() => callback(err));
210
+ }
211
+ if (data && (data.type === 'node' || (data.value && data.value.length))) {
212
+ this.push(data);
213
+ }
214
+ callback();
215
+ });
216
+ }
217
+
218
+ compareBoundary(line, startpos, boundary) {
219
+ // --{boundary}\r\n or --{boundary}--\r\n
220
+ if (line.length < boundary.length + 3 + startpos || line.length > boundary.length + 6 + startpos) {
221
+ return false;
222
+ }
223
+ for (let i = 0; i < boundary.length; i++) {
224
+ if (line[i + 2 + startpos] !== boundary[i]) {
225
+ return false;
226
+ }
227
+ }
228
+
229
+ let pos = 0;
230
+ for (let i = boundary.length + 2 + startpos; i < line.length; i++) {
231
+ let c = line[i];
232
+ if (pos === 0 && (c === 0x0d || c === 0x0a)) {
233
+ // 1: next node
234
+ return 1;
235
+ }
236
+ if (pos === 0 && c !== 0x2d) {
237
+ // expecting "-"
238
+ return false;
239
+ }
240
+ if (pos === 1 && c !== 0x2d) {
241
+ // expecting "-"
242
+ return false;
243
+ }
244
+ if (pos === 2 && c !== 0x0d && c !== 0x0a) {
245
+ // expecting line terminator, either <CR> or <LF>
246
+ return false;
247
+ }
248
+ if (pos === 3 && c !== 0x0a) {
249
+ // expecting line terminator <LF>
250
+ return false;
251
+ }
252
+ pos++;
253
+ }
254
+
255
+ // 2: multipart end
256
+ return 2;
257
+ }
258
+
259
+ checkBoundary(line) {
260
+ let startpos = 0;
261
+ if (line.length >= 1 && (line[0] === 0x0d || line[0] === 0x0a)) {
262
+ startpos++;
263
+ if (line.length >= 2 && (line[0] === 0x0d || line[1] === 0x0a)) {
264
+ startpos++;
265
+ }
266
+ }
267
+ if (line.length < 4 || line[startpos] !== 0x2d || line[startpos + 1] !== 0x2d) {
268
+ // defnitely not a boundary
269
+ return false;
270
+ }
271
+
272
+ let boundary;
273
+ if (this.node._boundary && (boundary = this.compareBoundary(line, startpos, this.node._boundary))) {
274
+ // 1: next child
275
+ // 2: multipart end
276
+ return boundary;
277
+ }
278
+
279
+ if (this.node._parentBoundary && (boundary = this.compareBoundary(line, startpos, this.node._parentBoundary))) {
280
+ // 3: next sibling
281
+ // 4: parent end
282
+ return boundary + 2;
283
+ }
284
+
285
+ return false;
286
+ }
287
+
288
+ processLine(line, final, next) {
289
+ let flush = false;
290
+
291
+ if (this.line && line) {
292
+ line = Buffer.concat([this.line, line]);
293
+ this.line = false;
294
+ } else if (this.line && !line) {
295
+ line = this.line;
296
+ this.line = false;
297
+ }
298
+
299
+ if (!line) {
300
+ line = Buffer.alloc(0);
301
+ }
302
+
303
+ if (this.nodeCounter > this.maxChildNodes) {
304
+ let err = new Error('Max allowed child nodes exceeded');
305
+ err.code = 'EMAXLEN';
306
+ return next(err);
307
+ }
308
+
309
+ // we check boundary outside the HEAD/BODY scope as it may appear anywhere
310
+ let boundary = this.checkBoundary(line);
311
+ if (boundary) {
312
+ // reached boundary, switch context
313
+ switch (boundary) {
314
+ case 1:
315
+ // next child
316
+ this.newNode(this.node);
317
+ flush = true;
318
+ break;
319
+ case 2:
320
+ // reached end of children, keep current node
321
+ break;
322
+ case 3: {
323
+ // next sibling
324
+ let parentNode = this.node.parentNode;
325
+ if (parentNode && parentNode.contentType === 'message/rfc822') {
326
+ // special case where immediate parent is an inline message block
327
+ // move up another step
328
+ parentNode = parentNode.parentNode;
329
+ }
330
+ this.newNode(parentNode);
331
+ flush = true;
332
+ break;
333
+ }
334
+ case 4:
335
+ // special case when boundary close a node with only header.
336
+ if (this.node && this.node._headerlen && !this.node.headers) {
337
+ this.node.parseHeaders();
338
+ this.push(this.node);
339
+ }
340
+ // move up
341
+ if (this.tree.length) {
342
+ this.node = this.tree.pop();
343
+ }
344
+ this.state = BODY;
345
+ break;
346
+ }
347
+
348
+ return next(
349
+ null,
350
+ {
351
+ node: this.node,
352
+ type: 'data',
353
+ value: line
354
+ },
355
+ flush
356
+ );
357
+ }
358
+
359
+ switch (this.state) {
360
+ case HEAD: {
361
+ this.node.addHeaderChunk(line);
362
+ if (this.node._headerlen > this.maxHeadSize) {
363
+ let err = new Error('Max header size for a MIME node exceeded');
364
+ err.code = 'EMAXLEN';
365
+ return next(err);
366
+ }
367
+ if (final || (line.length === 1 && line[0] === 0x0a) || (line.length === 2 && line[0] === 0x0d && line[1] === 0x0a)) {
368
+ let currentNode = this.node;
369
+
370
+ currentNode.parseHeaders();
371
+
372
+ // if the content is attached message then just continue
373
+ if (
374
+ currentNode.contentType === 'message/rfc822' &&
375
+ !this.config.ignoreEmbedded &&
376
+ (!currentNode.encoding || ['7bit', '8bit', 'binary'].includes(currentNode.encoding)) &&
377
+ (this.config.defaultInlineEmbedded ? currentNode.disposition !== 'attachment' : currentNode.disposition === 'inline')
378
+ ) {
379
+ currentNode.messageNode = true;
380
+ this.newNode(currentNode);
381
+ if (currentNode.parentNode) {
382
+ this.node._parentBoundary = currentNode.parentNode._boundary;
383
+ }
384
+ } else {
385
+ if (currentNode.contentType === 'message/rfc822') {
386
+ currentNode.messageNode = false;
387
+ }
388
+ this.state = BODY;
389
+ if (currentNode.multipart && currentNode._boundary) {
390
+ this.tree.push(currentNode);
391
+ }
392
+ }
393
+
394
+ return next(null, currentNode, flush);
395
+ }
396
+
397
+ return next();
398
+ }
399
+ case BODY: {
400
+ return next(
401
+ null,
402
+ {
403
+ node: this.node,
404
+ type: this.node.multipart ? 'data' : 'body',
405
+ value: line
406
+ },
407
+ flush
408
+ );
409
+ }
410
+ }
411
+
412
+ next(null, false);
413
+ }
414
+
415
+ newNode(parent) {
416
+ this.node = new MimeNode(parent || false, this.config);
417
+ this.state = HEAD;
418
+ this.nodeCounter++;
419
+ }
420
+ }
421
+
422
+ module.exports = MessageSplitter;