@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.
@@ -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
+ }