@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/LICENSE.EUPL-1.2 +153 -0
- package/LICENSE.MIT +21 -0
- package/README.md +214 -0
- package/index.js +17 -0
- package/lib/chunked-passthrough.js +37 -0
- package/lib/flowed-decoder.js +55 -0
- package/lib/headers.js +238 -0
- package/lib/message-joiner.js +30 -0
- package/lib/message-splitter.js +422 -0
- package/lib/mime-node.js +265 -0
- package/lib/node-rewriter.js +194 -0
- package/lib/node-streamer.js +121 -0
- package/package.json +38 -0
package/lib/mime-node.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Headers = require('./headers');
|
|
4
|
+
const libmime = require('libmime');
|
|
5
|
+
const libqp = require('libqp');
|
|
6
|
+
const libbase64 = require('libbase64');
|
|
7
|
+
const PassThrough = require('stream').PassThrough;
|
|
8
|
+
const pathlib = require('path');
|
|
9
|
+
|
|
10
|
+
class MimeNode {
|
|
11
|
+
constructor(parentNode, config) {
|
|
12
|
+
this.type = 'node';
|
|
13
|
+
this.root = !parentNode;
|
|
14
|
+
this.parentNode = parentNode;
|
|
15
|
+
|
|
16
|
+
this._parentBoundary = this.parentNode && this.parentNode._boundary;
|
|
17
|
+
this._headersLines = [];
|
|
18
|
+
this._headerlen = 0;
|
|
19
|
+
|
|
20
|
+
this._parsedContentType = false;
|
|
21
|
+
this._boundary = false;
|
|
22
|
+
|
|
23
|
+
this.multipart = false;
|
|
24
|
+
this.encoding = false;
|
|
25
|
+
this.headers = false;
|
|
26
|
+
this.contentType = false;
|
|
27
|
+
this.flowed = false;
|
|
28
|
+
this.delSp = false;
|
|
29
|
+
|
|
30
|
+
this.config = config || {};
|
|
31
|
+
this.libmime = new libmime.Libmime({ Iconv: this.config.Iconv });
|
|
32
|
+
|
|
33
|
+
this.parentPartNumber = (parentNode && this.partNr) || [];
|
|
34
|
+
this.partNr = false; // resolved later
|
|
35
|
+
this.childPartNumbers = 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getPartNr(provided) {
|
|
39
|
+
if (provided) {
|
|
40
|
+
return []
|
|
41
|
+
.concat(this.partNr || [])
|
|
42
|
+
.filter(nr => !isNaN(nr))
|
|
43
|
+
.concat(provided);
|
|
44
|
+
}
|
|
45
|
+
let childPartNr = ++this.childPartNumbers;
|
|
46
|
+
return []
|
|
47
|
+
.concat(this.partNr || [])
|
|
48
|
+
.filter(nr => !isNaN(nr))
|
|
49
|
+
.concat(childPartNr);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
addHeaderChunk(line) {
|
|
53
|
+
if (!line) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this._headersLines.push(line);
|
|
57
|
+
this._headerlen += line.length;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
parseHeaders() {
|
|
61
|
+
if (this.headers) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.headers = new Headers(Buffer.concat(this._headersLines, this._headerlen), this.config);
|
|
65
|
+
|
|
66
|
+
this._parsedContentDisposition = this.libmime.parseHeaderValue(this.headers.getFirst('Content-Disposition'));
|
|
67
|
+
|
|
68
|
+
// if content-type is missing default to plaintext
|
|
69
|
+
let contentHeader;
|
|
70
|
+
if (this.headers.get('Content-Type').length) {
|
|
71
|
+
contentHeader = this.headers.getFirst('Content-Type');
|
|
72
|
+
} else {
|
|
73
|
+
if (this._parsedContentDisposition.params.filename) {
|
|
74
|
+
let extension = pathlib.parse(this._parsedContentDisposition.params.filename).ext.replace(/^\./, '');
|
|
75
|
+
if (extension) {
|
|
76
|
+
contentHeader = libmime.detectMimeType(extension);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!contentHeader) {
|
|
80
|
+
if (/^attachment$/i.test(this._parsedContentDisposition.value)) {
|
|
81
|
+
contentHeader = 'application/octet-stream';
|
|
82
|
+
} else {
|
|
83
|
+
contentHeader = 'text/plain';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this._parsedContentType = this.libmime.parseHeaderValue(contentHeader);
|
|
89
|
+
|
|
90
|
+
this.encoding = this.headers
|
|
91
|
+
.getFirst('Content-Transfer-Encoding')
|
|
92
|
+
.replace(/\(.*\)/g, '')
|
|
93
|
+
.toLowerCase()
|
|
94
|
+
.trim();
|
|
95
|
+
this.contentType = (this._parsedContentType.value || '').toLowerCase().trim() || false;
|
|
96
|
+
this.charset = this._parsedContentType.params.charset || false;
|
|
97
|
+
this.disposition = (this._parsedContentDisposition.value || '').toLowerCase().trim() || false;
|
|
98
|
+
|
|
99
|
+
// fix invalidly encoded disposition values
|
|
100
|
+
if (this.disposition) {
|
|
101
|
+
try {
|
|
102
|
+
this.disposition = this.libmime.decodeWords(this.disposition);
|
|
103
|
+
} catch (E) {
|
|
104
|
+
// failed to parse disposition, keep as is (most probably an unknown charset is used)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.filename = this._parsedContentDisposition.params.filename || this._parsedContentType.params.name || false;
|
|
109
|
+
|
|
110
|
+
if (this._parsedContentType.params.format && this._parsedContentType.params.format.toLowerCase().trim() === 'flowed') {
|
|
111
|
+
this.flowed = true;
|
|
112
|
+
if (this._parsedContentType.params.delsp && this._parsedContentType.params.delsp.toLowerCase().trim() === 'yes') {
|
|
113
|
+
this.delSp = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this.filename) {
|
|
118
|
+
try {
|
|
119
|
+
this.filename = this.libmime.decodeWords(this.filename);
|
|
120
|
+
} catch (E) {
|
|
121
|
+
// failed to parse filename, keep as is (most probably an unknown charset is used)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.multipart =
|
|
126
|
+
(this.contentType &&
|
|
127
|
+
this.contentType.substr(0, this.contentType.indexOf('/')) === 'multipart' &&
|
|
128
|
+
this.contentType.substr(this.contentType.indexOf('/') + 1)) ||
|
|
129
|
+
false;
|
|
130
|
+
this._boundary = (this._parsedContentType.params.boundary && Buffer.from(this._parsedContentType.params.boundary)) || false;
|
|
131
|
+
|
|
132
|
+
this.rfc822 = this.contentType === 'message/rfc822';
|
|
133
|
+
|
|
134
|
+
if (!this.parentNode || this.parentNode.rfc822) {
|
|
135
|
+
this.partNr = this.parentNode ? this.parentNode.getPartNr('TEXT') : ['TEXT'];
|
|
136
|
+
} else {
|
|
137
|
+
this.partNr = this.parentNode ? this.parentNode.getPartNr() : [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getHeaders() {
|
|
142
|
+
if (!this.headers) {
|
|
143
|
+
this.parseHeaders();
|
|
144
|
+
}
|
|
145
|
+
return this.headers.build();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setContentType(contentType) {
|
|
149
|
+
if (!this.headers) {
|
|
150
|
+
this.parseHeaders();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
contentType = (contentType || '').toLowerCase().trim();
|
|
154
|
+
if (contentType) {
|
|
155
|
+
this._parsedContentType.value = contentType;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!this.flowed && this._parsedContentType.params.format) {
|
|
159
|
+
delete this._parsedContentType.params.format;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!this.delSp && this._parsedContentType.params.delsp) {
|
|
163
|
+
delete this._parsedContentType.params.delsp;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.headers.update('Content-Type', this.libmime.buildHeaderValue(this._parsedContentType));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
setCharset(charset) {
|
|
170
|
+
if (!this.headers) {
|
|
171
|
+
this.parseHeaders();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
charset = (charset || '').toLowerCase().trim();
|
|
175
|
+
|
|
176
|
+
if (charset === 'ascii') {
|
|
177
|
+
charset = '';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!charset) {
|
|
181
|
+
if (!this._parsedContentType.value) {
|
|
182
|
+
// nothing to set or update
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
delete this._parsedContentType.params.charset;
|
|
186
|
+
} else {
|
|
187
|
+
this._parsedContentType.params.charset = charset;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!this._parsedContentType.value) {
|
|
191
|
+
this._parsedContentType.value = 'text/plain';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.headers.update('Content-Type', this.libmime.buildHeaderValue(this._parsedContentType));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
setFilename(filename) {
|
|
198
|
+
if (!this.headers) {
|
|
199
|
+
this.parseHeaders();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.filename = (filename || '').toLowerCase().trim();
|
|
203
|
+
|
|
204
|
+
if (this._parsedContentType.params.name) {
|
|
205
|
+
delete this._parsedContentType.params.name;
|
|
206
|
+
this.headers.update('Content-Type', this.libmime.buildHeaderValue(this._parsedContentType));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!this.filename) {
|
|
210
|
+
if (!this._parsedContentDisposition.value) {
|
|
211
|
+
// nothing to set or update
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
delete this._parsedContentDisposition.params.filename;
|
|
215
|
+
} else {
|
|
216
|
+
this._parsedContentDisposition.params.filename = this.filename;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!this._parsedContentDisposition.value) {
|
|
220
|
+
this._parsedContentDisposition.value = 'attachment';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.headers.update('Content-Disposition', this.libmime.buildHeaderValue(this._parsedContentDisposition));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
getDecoder() {
|
|
227
|
+
if (!this.headers) {
|
|
228
|
+
this.parseHeaders();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
switch (this.encoding) {
|
|
232
|
+
case 'base64':
|
|
233
|
+
return new libbase64.Decoder();
|
|
234
|
+
case 'quoted-printable':
|
|
235
|
+
return new libqp.Decoder();
|
|
236
|
+
default:
|
|
237
|
+
return new PassThrough();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getEncoder(encoding) {
|
|
242
|
+
if (!this.headers) {
|
|
243
|
+
this.parseHeaders();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
encoding = (encoding || '').toString().toLowerCase().trim();
|
|
247
|
+
|
|
248
|
+
if (encoding && encoding !== this.encoding) {
|
|
249
|
+
this.headers.update('Content-Transfer-Encoding', encoding);
|
|
250
|
+
} else {
|
|
251
|
+
encoding = this.encoding;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
switch (encoding) {
|
|
255
|
+
case 'base64':
|
|
256
|
+
return new libbase64.Encoder();
|
|
257
|
+
case 'quoted-printable':
|
|
258
|
+
return new libqp.Encoder();
|
|
259
|
+
default:
|
|
260
|
+
return new PassThrough();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = MimeNode;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Helper class to rewrite nodes with specific mime type
|
|
4
|
+
|
|
5
|
+
const Transform = require('stream').Transform;
|
|
6
|
+
const FlowedDecoder = require('./flowed-decoder');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* NodeRewriter Transform stream. Updates content for all nodes with specified mime type
|
|
10
|
+
*
|
|
11
|
+
* @constructor
|
|
12
|
+
* @param {String} mimeType Define the Mime-Type to look for
|
|
13
|
+
* @param {Function} rewriteAction Function to run with the node content
|
|
14
|
+
*/
|
|
15
|
+
class NodeRewriter extends Transform {
|
|
16
|
+
constructor(filterFunc, rewriteAction) {
|
|
17
|
+
let options = {
|
|
18
|
+
readableObjectMode: true,
|
|
19
|
+
writableObjectMode: true
|
|
20
|
+
};
|
|
21
|
+
super(options);
|
|
22
|
+
|
|
23
|
+
this.filterFunc = filterFunc;
|
|
24
|
+
this.rewriteAction = rewriteAction;
|
|
25
|
+
|
|
26
|
+
this.decoder = false;
|
|
27
|
+
this.encoder = false;
|
|
28
|
+
this.continue = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_transform(data, encoding, callback) {
|
|
32
|
+
this.processIncoming(data, callback);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_flush(callback) {
|
|
36
|
+
if (this.decoder) {
|
|
37
|
+
// emit an empty node just in case there is pending data to end
|
|
38
|
+
return this.processIncoming(
|
|
39
|
+
{
|
|
40
|
+
type: 'none'
|
|
41
|
+
},
|
|
42
|
+
callback
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return callback();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
processIncoming(data, callback) {
|
|
49
|
+
if (this.decoder && data.type === 'body') {
|
|
50
|
+
// data to parse
|
|
51
|
+
if (!this.decoder.write(data.value)) {
|
|
52
|
+
return this.decoder.once('drain', callback);
|
|
53
|
+
} else {
|
|
54
|
+
return callback();
|
|
55
|
+
}
|
|
56
|
+
} else if (this.decoder && data.type !== 'body') {
|
|
57
|
+
// stop decoding.
|
|
58
|
+
// we can not process the current data chunk as we need to wait until
|
|
59
|
+
// the parsed data is completely processed, so we store a reference to the
|
|
60
|
+
// continue callback
|
|
61
|
+
this.continue = () => {
|
|
62
|
+
this.continue = false;
|
|
63
|
+
this.decoder = false;
|
|
64
|
+
this.encoder = false;
|
|
65
|
+
this.processIncoming(data, callback);
|
|
66
|
+
};
|
|
67
|
+
return this.decoder.end();
|
|
68
|
+
} else if (data.type === 'node' && this.filterFunc(data)) {
|
|
69
|
+
// found matching node, create new handler
|
|
70
|
+
this.emit('node', this.createDecodePair(data));
|
|
71
|
+
} else if (this.readable && data.type !== 'none') {
|
|
72
|
+
// we don't care about this data, just pass it over to the joiner
|
|
73
|
+
this.push(data);
|
|
74
|
+
}
|
|
75
|
+
callback();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
createDecodePair(node) {
|
|
79
|
+
this.decoder = node.getDecoder();
|
|
80
|
+
|
|
81
|
+
if (['base64', 'quoted-printable'].includes(node.encoding)) {
|
|
82
|
+
this.encoder = node.getEncoder();
|
|
83
|
+
} else {
|
|
84
|
+
this.encoder = node.getEncoder('quoted-printable');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let lastByte = false;
|
|
88
|
+
|
|
89
|
+
let decoder = this.decoder;
|
|
90
|
+
let encoder = this.encoder;
|
|
91
|
+
let firstChunk = true;
|
|
92
|
+
decoder.$reading = false;
|
|
93
|
+
|
|
94
|
+
let readFromEncoder = () => {
|
|
95
|
+
decoder.$reading = true;
|
|
96
|
+
|
|
97
|
+
let data = encoder.read();
|
|
98
|
+
if (data === null) {
|
|
99
|
+
decoder.$reading = false;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (firstChunk) {
|
|
104
|
+
firstChunk = false;
|
|
105
|
+
if (this.readable) {
|
|
106
|
+
this.push(node);
|
|
107
|
+
if (node.type === 'body') {
|
|
108
|
+
lastByte = node.value && node.value.length && node.value[node.value.length - 1];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let writeMore = true;
|
|
114
|
+
if (this.readable) {
|
|
115
|
+
writeMore = this.push({
|
|
116
|
+
node,
|
|
117
|
+
type: 'body',
|
|
118
|
+
value: data
|
|
119
|
+
});
|
|
120
|
+
lastByte = data && data.length && data[data.length - 1];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (writeMore) {
|
|
124
|
+
return setImmediate(readFromEncoder);
|
|
125
|
+
} else {
|
|
126
|
+
encoder.pause();
|
|
127
|
+
// no idea how to catch drain? use timeout for now as poor man's substitute
|
|
128
|
+
// this.once('drain', () => encoder.resume());
|
|
129
|
+
setTimeout(() => {
|
|
130
|
+
encoder.resume();
|
|
131
|
+
setImmediate(readFromEncoder);
|
|
132
|
+
}, 100);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
encoder.on('readable', () => {
|
|
137
|
+
if (!decoder.$reading) {
|
|
138
|
+
return readFromEncoder();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
encoder.on('end', () => {
|
|
143
|
+
if (firstChunk) {
|
|
144
|
+
firstChunk = false;
|
|
145
|
+
if (this.readable) {
|
|
146
|
+
this.push(node);
|
|
147
|
+
if (node.type === 'body') {
|
|
148
|
+
lastByte = node.value && node.value.length && node.value[node.value.length - 1];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (lastByte !== 0x0a) {
|
|
154
|
+
// make sure there is a terminating line break
|
|
155
|
+
this.push({
|
|
156
|
+
node,
|
|
157
|
+
type: 'body',
|
|
158
|
+
value: Buffer.from([0x0a])
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (this.continue) {
|
|
163
|
+
return this.continue();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (/^text\//.test(node.contentType) && node.flowed) {
|
|
168
|
+
// text/plain; format=flowed is a special case
|
|
169
|
+
let flowDecoder = decoder;
|
|
170
|
+
decoder = new FlowedDecoder({
|
|
171
|
+
delSp: node.delSp,
|
|
172
|
+
encoding: node.encoding
|
|
173
|
+
});
|
|
174
|
+
flowDecoder.on('error', err => {
|
|
175
|
+
decoder.emit('error', err);
|
|
176
|
+
});
|
|
177
|
+
flowDecoder.pipe(decoder);
|
|
178
|
+
|
|
179
|
+
// we don't know what kind of data we are going to get, does it comply with the
|
|
180
|
+
// requirements of format=flowed, so we just cancel it
|
|
181
|
+
node.flowed = false;
|
|
182
|
+
node.delSp = false;
|
|
183
|
+
node.setContentType();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
node,
|
|
188
|
+
decoder,
|
|
189
|
+
encoder
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = NodeRewriter;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Helper class to rewrite nodes with specific mime type
|
|
4
|
+
|
|
5
|
+
const Transform = require('stream').Transform;
|
|
6
|
+
const FlowedDecoder = require('./flowed-decoder');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* NodeRewriter Transform stream. Updates content for all nodes with specified mime type
|
|
10
|
+
*
|
|
11
|
+
* @constructor
|
|
12
|
+
* @param {String} mimeType Define the Mime-Type to look for
|
|
13
|
+
* @param {Function} streamAction Function to run with the node content
|
|
14
|
+
*/
|
|
15
|
+
class NodeStreamer extends Transform {
|
|
16
|
+
constructor(filterFunc, streamAction) {
|
|
17
|
+
let options = {
|
|
18
|
+
readableObjectMode: true,
|
|
19
|
+
writableObjectMode: true
|
|
20
|
+
};
|
|
21
|
+
super(options);
|
|
22
|
+
|
|
23
|
+
this.filterFunc = filterFunc;
|
|
24
|
+
this.streamAction = streamAction;
|
|
25
|
+
|
|
26
|
+
this.decoder = false;
|
|
27
|
+
this.canContinue = false;
|
|
28
|
+
this.continue = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_transform(data, encoding, callback) {
|
|
32
|
+
this.processIncoming(data, callback);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_flush(callback) {
|
|
36
|
+
if (this.decoder) {
|
|
37
|
+
// emit an empty node just in case there is pending data to end
|
|
38
|
+
return this.processIncoming(
|
|
39
|
+
{
|
|
40
|
+
type: 'none'
|
|
41
|
+
},
|
|
42
|
+
callback
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return callback();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
processIncoming(data, callback) {
|
|
49
|
+
if (this.decoder && data.type === 'body') {
|
|
50
|
+
// data to parse
|
|
51
|
+
this.push(data);
|
|
52
|
+
if (!this.decoder.write(data.value)) {
|
|
53
|
+
return this.decoder.once('drain', callback);
|
|
54
|
+
} else {
|
|
55
|
+
return callback();
|
|
56
|
+
}
|
|
57
|
+
} else if (this.decoder && data.type !== 'body') {
|
|
58
|
+
// stop decoding.
|
|
59
|
+
// we can not process the current data chunk as we need to wait until
|
|
60
|
+
// the parsed data is completely processed, so we store a reference to the
|
|
61
|
+
// continue callback
|
|
62
|
+
|
|
63
|
+
let doContinue = () => {
|
|
64
|
+
this.continue = false;
|
|
65
|
+
this.decoder = false;
|
|
66
|
+
this.canContinue = false;
|
|
67
|
+
this.processIncoming(data, callback);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (this.canContinue) {
|
|
71
|
+
setImmediate(doContinue);
|
|
72
|
+
} else {
|
|
73
|
+
this.continue = () => doContinue();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return this.decoder.end();
|
|
77
|
+
} else if (data.type === 'node' && this.filterFunc(data)) {
|
|
78
|
+
this.push(data);
|
|
79
|
+
// found matching node, create new handler
|
|
80
|
+
this.emit('node', this.createDecoder(data));
|
|
81
|
+
} else if (this.readable && data.type !== 'none') {
|
|
82
|
+
// we don't care about this data, just pass it over to the joiner
|
|
83
|
+
this.push(data);
|
|
84
|
+
}
|
|
85
|
+
callback();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
createDecoder(node) {
|
|
89
|
+
this.decoder = node.getDecoder();
|
|
90
|
+
|
|
91
|
+
let decoder = this.decoder;
|
|
92
|
+
decoder.$reading = false;
|
|
93
|
+
|
|
94
|
+
if (/^text\//.test(node.contentType) && node.flowed) {
|
|
95
|
+
let flowDecoder = decoder;
|
|
96
|
+
decoder = new FlowedDecoder({
|
|
97
|
+
delSp: node.delSp
|
|
98
|
+
});
|
|
99
|
+
flowDecoder.on('error', err => {
|
|
100
|
+
decoder.emit('error', err);
|
|
101
|
+
});
|
|
102
|
+
flowDecoder.pipe(decoder);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
node,
|
|
107
|
+
decoder,
|
|
108
|
+
done: () => {
|
|
109
|
+
if (typeof this.continue === 'function') {
|
|
110
|
+
// called once input stream is processed
|
|
111
|
+
this.continue();
|
|
112
|
+
} else {
|
|
113
|
+
// called before input stream is processed
|
|
114
|
+
this.canContinue = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = NodeStreamer;
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zone-eu/mailsplit",
|
|
3
|
+
"version": "5.4.6",
|
|
4
|
+
"description": "Split email messages into an object stream",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"directories": {
|
|
7
|
+
"test": "test"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "grunt",
|
|
11
|
+
"update": "rm -rf node_modules package-lock.json && ncu -u && npm install"
|
|
12
|
+
},
|
|
13
|
+
"author": "Andris Reinman",
|
|
14
|
+
"license": "(MIT OR EUPL-1.1+)",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"libbase64": "1.3.0",
|
|
17
|
+
"libmime": "5.3.7",
|
|
18
|
+
"libqp": "2.1.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"eslint": "8.29.0",
|
|
22
|
+
"eslint-config-nodemailer": "1.2.0",
|
|
23
|
+
"eslint-config-prettier": "9.1.0",
|
|
24
|
+
"grunt": "1.6.1",
|
|
25
|
+
"grunt-cli": "1.5.0",
|
|
26
|
+
"grunt-contrib-nodeunit": "5.0.0",
|
|
27
|
+
"grunt-eslint": "24.0.1",
|
|
28
|
+
"random-message": "1.1.0"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"lib",
|
|
32
|
+
"index.js"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/zone-eu/mailsplit.git"
|
|
37
|
+
}
|
|
38
|
+
}
|