@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/node-rewriter.js
CHANGED
|
@@ -5,14 +5,26 @@
|
|
|
5
5
|
const Transform = require('stream').Transform;
|
|
6
6
|
const FlowedDecoder = require('./flowed-decoder');
|
|
7
7
|
|
|
8
|
+
/** @typedef {import('..').MimeNode} MimeNode */
|
|
9
|
+
/** @typedef {import('..').RewriterInput} RewriterInput */
|
|
10
|
+
/** @typedef {import('..').FilterFunc} FilterFunc */
|
|
11
|
+
/** @typedef {import('..').RewriterNode} RewriterNode */
|
|
12
|
+
/** @typedef {import('..').DecoderStream} DecoderStream */
|
|
13
|
+
/** @typedef {import('..').ContentStream} ContentStream */
|
|
14
|
+
/** @typedef {import('..').ContinueCallback} ContinueCallback */
|
|
15
|
+
|
|
8
16
|
/**
|
|
9
|
-
* NodeRewriter Transform stream. Updates content for all nodes
|
|
17
|
+
* NodeRewriter Transform stream. Updates content for all nodes selected by the
|
|
18
|
+
* filter function.
|
|
10
19
|
*
|
|
11
|
-
* @
|
|
12
|
-
* @param {
|
|
13
|
-
* @param {Function} rewriteAction Function to run with the node content
|
|
20
|
+
* @param {FilterFunc} filterFunc Function that receives a MIME node and returns true to rewrite it.
|
|
21
|
+
* @param {Function} [rewriteAction] Optional compatibility hook stored on the instance.
|
|
14
22
|
*/
|
|
15
23
|
class NodeRewriter extends Transform {
|
|
24
|
+
/**
|
|
25
|
+
* @param {FilterFunc} filterFunc Function that receives a MIME node and returns true to rewrite it.
|
|
26
|
+
* @param {Function} [rewriteAction] Optional compatibility hook stored on the instance.
|
|
27
|
+
*/
|
|
16
28
|
constructor(filterFunc, rewriteAction) {
|
|
17
29
|
let options = {
|
|
18
30
|
readableObjectMode: true,
|
|
@@ -23,15 +35,28 @@ class NodeRewriter extends Transform {
|
|
|
23
35
|
this.filterFunc = filterFunc;
|
|
24
36
|
this.rewriteAction = rewriteAction;
|
|
25
37
|
|
|
38
|
+
/** @type {DecoderStream | false} */
|
|
26
39
|
this.decoder = false;
|
|
40
|
+
/** @type {ContentStream | false} */
|
|
27
41
|
this.encoder = false;
|
|
42
|
+
/** @type {ContinueCallback | false} */
|
|
28
43
|
this.continue = false;
|
|
29
44
|
}
|
|
30
45
|
|
|
46
|
+
/**
|
|
47
|
+
* @param {RewriterInput} data
|
|
48
|
+
* @param {BufferEncoding} encoding
|
|
49
|
+
* @param {import('stream').TransformCallback} callback
|
|
50
|
+
* @returns {void}
|
|
51
|
+
*/
|
|
31
52
|
_transform(data, encoding, callback) {
|
|
32
53
|
this.processIncoming(data, callback);
|
|
33
54
|
}
|
|
34
55
|
|
|
56
|
+
/**
|
|
57
|
+
* @param {import('stream').TransformCallback} callback
|
|
58
|
+
* @returns {void}
|
|
59
|
+
*/
|
|
35
60
|
_flush(callback) {
|
|
36
61
|
if (this.decoder) {
|
|
37
62
|
// emit an empty node just in case there is pending data to end
|
|
@@ -45,11 +70,17 @@ class NodeRewriter extends Transform {
|
|
|
45
70
|
return callback();
|
|
46
71
|
}
|
|
47
72
|
|
|
73
|
+
/**
|
|
74
|
+
* @param {RewriterInput} data
|
|
75
|
+
* @param {import('stream').TransformCallback} callback
|
|
76
|
+
* @returns {void}
|
|
77
|
+
*/
|
|
48
78
|
processIncoming(data, callback) {
|
|
49
79
|
if (this.decoder && data.type === 'body') {
|
|
50
80
|
// data to parse
|
|
51
81
|
if (!this.decoder.write(data.value)) {
|
|
52
|
-
|
|
82
|
+
this.decoder.once('drain', callback);
|
|
83
|
+
return;
|
|
53
84
|
} else {
|
|
54
85
|
return callback();
|
|
55
86
|
}
|
|
@@ -64,7 +95,8 @@ class NodeRewriter extends Transform {
|
|
|
64
95
|
this.encoder = false;
|
|
65
96
|
this.processIncoming(data, callback);
|
|
66
97
|
};
|
|
67
|
-
|
|
98
|
+
this.decoder.end();
|
|
99
|
+
return;
|
|
68
100
|
} else if (data.type === 'node' && this.filterFunc(data)) {
|
|
69
101
|
// found matching node, create new handler
|
|
70
102
|
this.emit('node', this.createDecodePair(data));
|
|
@@ -75,22 +107,32 @@ class NodeRewriter extends Transform {
|
|
|
75
107
|
callback();
|
|
76
108
|
}
|
|
77
109
|
|
|
110
|
+
/**
|
|
111
|
+
* @param {MimeNode} node
|
|
112
|
+
* @returns {RewriterNode}
|
|
113
|
+
*/
|
|
78
114
|
createDecodePair(node) {
|
|
79
|
-
this.decoder = node.getDecoder();
|
|
115
|
+
this.decoder = /** @type {DecoderStream} */ (node.getDecoder());
|
|
80
116
|
|
|
81
|
-
if (['base64', 'quoted-printable'].includes(node.encoding)) {
|
|
117
|
+
if (['base64', 'quoted-printable'].includes(node.encoding || '')) {
|
|
82
118
|
this.encoder = node.getEncoder();
|
|
83
119
|
} else {
|
|
84
120
|
this.encoder = node.getEncoder('quoted-printable');
|
|
85
121
|
}
|
|
86
122
|
|
|
123
|
+
/** @type {number | false} */
|
|
87
124
|
let lastByte = false;
|
|
88
125
|
|
|
89
|
-
let decoder = this.decoder;
|
|
90
|
-
let encoder = this.encoder;
|
|
126
|
+
let decoder = /** @type {DecoderStream} */ (this.decoder);
|
|
127
|
+
let encoder = /** @type {ContentStream} */ (this.encoder);
|
|
91
128
|
let firstChunk = true;
|
|
92
129
|
decoder.$reading = false;
|
|
93
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Reads encoded replacement bytes and forwards them as body chunks.
|
|
133
|
+
*
|
|
134
|
+
* @returns {void | NodeJS.Immediate}
|
|
135
|
+
*/
|
|
94
136
|
let readFromEncoder = () => {
|
|
95
137
|
decoder.$reading = true;
|
|
96
138
|
|
|
@@ -104,9 +146,6 @@ class NodeRewriter extends Transform {
|
|
|
104
146
|
firstChunk = false;
|
|
105
147
|
if (this.readable) {
|
|
106
148
|
this.push(node);
|
|
107
|
-
if (node.type === 'body') {
|
|
108
|
-
lastByte = node.value && node.value.length && node.value[node.value.length - 1];
|
|
109
|
-
}
|
|
110
149
|
}
|
|
111
150
|
}
|
|
112
151
|
|
|
@@ -144,9 +183,6 @@ class NodeRewriter extends Transform {
|
|
|
144
183
|
firstChunk = false;
|
|
145
184
|
if (this.readable) {
|
|
146
185
|
this.push(node);
|
|
147
|
-
if (node.type === 'body') {
|
|
148
|
-
lastByte = node.value && node.value.length && node.value[node.value.length - 1];
|
|
149
|
-
}
|
|
150
186
|
}
|
|
151
187
|
}
|
|
152
188
|
|
|
@@ -164,16 +200,25 @@ class NodeRewriter extends Transform {
|
|
|
164
200
|
}
|
|
165
201
|
});
|
|
166
202
|
|
|
167
|
-
if (/^text\//.test(node.contentType) && node.flowed) {
|
|
203
|
+
if (/^text\//.test(node.contentType || '') && node.flowed) {
|
|
168
204
|
// text/plain; format=flowed is a special case
|
|
169
205
|
let flowDecoder = decoder;
|
|
170
206
|
decoder = new FlowedDecoder({
|
|
171
207
|
delSp: node.delSp,
|
|
172
|
-
encoding: node.encoding
|
|
173
|
-
});
|
|
174
|
-
flowDecoder.on('error', err => {
|
|
175
|
-
decoder.emit('error', err);
|
|
208
|
+
encoding: node.encoding || false
|
|
176
209
|
});
|
|
210
|
+
flowDecoder.on(
|
|
211
|
+
'error',
|
|
212
|
+
/**
|
|
213
|
+
* Forwards flowed decoder errors to the replacement decoder.
|
|
214
|
+
*
|
|
215
|
+
* @param {Error} err Decoder error to forward.
|
|
216
|
+
* @returns {void}
|
|
217
|
+
*/
|
|
218
|
+
err => {
|
|
219
|
+
decoder.emit('error', err);
|
|
220
|
+
}
|
|
221
|
+
);
|
|
177
222
|
flowDecoder.pipe(decoder);
|
|
178
223
|
|
|
179
224
|
// we don't know what kind of data we are going to get, does it comply with the
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Transform } from 'node:stream';
|
|
2
|
+
import type { FilterFunc, SplitterChunk, StreamerNode } from './types';
|
|
3
|
+
|
|
4
|
+
/** Transform stream that exposes decoded body streams for selected MIME nodes without replacing them. */
|
|
5
|
+
declare class NodeStreamer extends Transform {
|
|
6
|
+
/**
|
|
7
|
+
* Creates a node streamer that accepts splitter chunks and passes them through unchanged.
|
|
8
|
+
*
|
|
9
|
+
* @param filterFunc Predicate that selects MIME nodes to stream.
|
|
10
|
+
* @param streamAction Optional compatibility hook stored on the instance; consumers usually handle the `node` event.
|
|
11
|
+
*/
|
|
12
|
+
constructor(filterFunc: FilterFunc, streamAction?: Function);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Registers a listener for passed-through splitter chunks.
|
|
16
|
+
*
|
|
17
|
+
* @param event Event name.
|
|
18
|
+
* @param listener Receives each outgoing MIME node, data chunk, or body chunk.
|
|
19
|
+
* @returns This streamer instance.
|
|
20
|
+
*/
|
|
21
|
+
on(event: 'data', listener: (data: SplitterChunk) => void): this;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Registers a listener for selected nodes.
|
|
25
|
+
*
|
|
26
|
+
* @param event Event name.
|
|
27
|
+
* @param listener Receives a decoder stream and completion callback for a selected node.
|
|
28
|
+
* @returns This streamer instance.
|
|
29
|
+
*/
|
|
30
|
+
on(event: 'node', listener: (data: StreamerNode) => void): this;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Registers a one-time listener for the next passed-through splitter chunk.
|
|
34
|
+
*
|
|
35
|
+
* @param event Event name.
|
|
36
|
+
* @param listener Receives the next outgoing MIME node, data chunk, or body chunk.
|
|
37
|
+
* @returns This streamer instance.
|
|
38
|
+
*/
|
|
39
|
+
once(event: 'data', listener: (data: SplitterChunk) => void): this;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Registers a one-time listener for the next selected node.
|
|
43
|
+
*
|
|
44
|
+
* @param event Event name.
|
|
45
|
+
* @param listener Receives a decoder stream and completion callback for the next selected node.
|
|
46
|
+
* @returns This streamer instance.
|
|
47
|
+
*/
|
|
48
|
+
once(event: 'node', listener: (data: StreamerNode) => void): this;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adds a listener for passed-through splitter chunks.
|
|
52
|
+
*
|
|
53
|
+
* @param event Event name.
|
|
54
|
+
* @param listener Receives each outgoing MIME node, data chunk, or body chunk.
|
|
55
|
+
* @returns This streamer instance.
|
|
56
|
+
*/
|
|
57
|
+
addListener(event: 'data', listener: (data: SplitterChunk) => void): this;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Adds a listener for selected nodes.
|
|
61
|
+
*
|
|
62
|
+
* @param event Event name.
|
|
63
|
+
* @param listener Receives a decoder stream and completion callback for a selected node.
|
|
64
|
+
* @returns This streamer instance.
|
|
65
|
+
*/
|
|
66
|
+
addListener(event: 'node', listener: (data: StreamerNode) => void): this;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Prepends a listener for passed-through splitter chunks.
|
|
70
|
+
*
|
|
71
|
+
* @param event Event name.
|
|
72
|
+
* @param listener Receives each outgoing MIME node, data chunk, or body chunk.
|
|
73
|
+
* @returns This streamer instance.
|
|
74
|
+
*/
|
|
75
|
+
prependListener(event: 'data', listener: (data: SplitterChunk) => void): this;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Prepends a listener for selected nodes.
|
|
79
|
+
*
|
|
80
|
+
* @param event Event name.
|
|
81
|
+
* @param listener Receives a decoder stream and completion callback for a selected node.
|
|
82
|
+
* @returns This streamer instance.
|
|
83
|
+
*/
|
|
84
|
+
prependListener(event: 'node', listener: (data: StreamerNode) => void): this;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Emits a passed-through splitter chunk.
|
|
88
|
+
*
|
|
89
|
+
* @param event Event name.
|
|
90
|
+
* @param data MIME node, data chunk, or body chunk to emit.
|
|
91
|
+
* @returns `true` when the event had listeners.
|
|
92
|
+
*/
|
|
93
|
+
emit(event: 'data', data: SplitterChunk): boolean;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Emits a selected-node payload.
|
|
97
|
+
*
|
|
98
|
+
* @param event Event name.
|
|
99
|
+
* @param data Selected node payload containing decoder stream and completion callback.
|
|
100
|
+
* @returns `true` when the event had listeners.
|
|
101
|
+
*/
|
|
102
|
+
emit(event: 'node', data: StreamerNode): boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export = NodeStreamer;
|
package/lib/node-streamer.js
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Helper class to
|
|
3
|
+
// Helper class to stream selected nodes by MIME metadata
|
|
4
4
|
|
|
5
5
|
const Transform = require('stream').Transform;
|
|
6
6
|
const FlowedDecoder = require('./flowed-decoder');
|
|
7
7
|
|
|
8
|
+
/** @typedef {import('..').MimeNode} MimeNode */
|
|
9
|
+
/** @typedef {import('..').RewriterInput} RewriterInput */
|
|
10
|
+
/** @typedef {import('..').FilterFunc} FilterFunc */
|
|
11
|
+
/** @typedef {import('..').StreamerNode} StreamerNode */
|
|
12
|
+
/** @typedef {import('..').DecoderStream} DecoderStream */
|
|
13
|
+
/** @typedef {import('..').ContinueCallback} ContinueCallback */
|
|
14
|
+
|
|
8
15
|
/**
|
|
9
|
-
*
|
|
16
|
+
* NodeStreamer Transform stream. Exposes decoded content for nodes selected by
|
|
17
|
+
* the filter function while passing the original message through unchanged.
|
|
10
18
|
*
|
|
11
|
-
* @
|
|
12
|
-
* @param {
|
|
13
|
-
* @param {Function} streamAction Function to run with the node content
|
|
19
|
+
* @param {FilterFunc} filterFunc Function that receives a MIME node and returns true to stream it.
|
|
20
|
+
* @param {Function} [streamAction] Optional compatibility hook stored on the instance.
|
|
14
21
|
*/
|
|
15
22
|
class NodeStreamer extends Transform {
|
|
23
|
+
/**
|
|
24
|
+
* @param {FilterFunc} filterFunc Function that receives a MIME node and returns true to stream it.
|
|
25
|
+
* @param {Function} [streamAction] Optional compatibility hook stored on the instance.
|
|
26
|
+
*/
|
|
16
27
|
constructor(filterFunc, streamAction) {
|
|
17
28
|
let options = {
|
|
18
29
|
readableObjectMode: true,
|
|
@@ -23,15 +34,27 @@ class NodeStreamer extends Transform {
|
|
|
23
34
|
this.filterFunc = filterFunc;
|
|
24
35
|
this.streamAction = streamAction;
|
|
25
36
|
|
|
37
|
+
/** @type {DecoderStream | false} */
|
|
26
38
|
this.decoder = false;
|
|
27
39
|
this.canContinue = false;
|
|
40
|
+
/** @type {ContinueCallback | false} */
|
|
28
41
|
this.continue = false;
|
|
29
42
|
}
|
|
30
43
|
|
|
44
|
+
/**
|
|
45
|
+
* @param {RewriterInput} data
|
|
46
|
+
* @param {BufferEncoding} encoding
|
|
47
|
+
* @param {import('stream').TransformCallback} callback
|
|
48
|
+
* @returns {void}
|
|
49
|
+
*/
|
|
31
50
|
_transform(data, encoding, callback) {
|
|
32
51
|
this.processIncoming(data, callback);
|
|
33
52
|
}
|
|
34
53
|
|
|
54
|
+
/**
|
|
55
|
+
* @param {import('stream').TransformCallback} callback
|
|
56
|
+
* @returns {void}
|
|
57
|
+
*/
|
|
35
58
|
_flush(callback) {
|
|
36
59
|
if (this.decoder) {
|
|
37
60
|
// emit an empty node just in case there is pending data to end
|
|
@@ -45,12 +68,18 @@ class NodeStreamer extends Transform {
|
|
|
45
68
|
return callback();
|
|
46
69
|
}
|
|
47
70
|
|
|
71
|
+
/**
|
|
72
|
+
* @param {RewriterInput} data
|
|
73
|
+
* @param {import('stream').TransformCallback} callback
|
|
74
|
+
* @returns {void}
|
|
75
|
+
*/
|
|
48
76
|
processIncoming(data, callback) {
|
|
49
77
|
if (this.decoder && data.type === 'body') {
|
|
50
78
|
// data to parse
|
|
51
79
|
this.push(data);
|
|
52
80
|
if (!this.decoder.write(data.value)) {
|
|
53
|
-
|
|
81
|
+
this.decoder.once('drain', callback);
|
|
82
|
+
return;
|
|
54
83
|
} else {
|
|
55
84
|
return callback();
|
|
56
85
|
}
|
|
@@ -60,6 +89,11 @@ class NodeStreamer extends Transform {
|
|
|
60
89
|
// the parsed data is completely processed, so we store a reference to the
|
|
61
90
|
// continue callback
|
|
62
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Resumes processing with the first chunk after the streamed node.
|
|
94
|
+
*
|
|
95
|
+
* @returns {void}
|
|
96
|
+
*/
|
|
63
97
|
let doContinue = () => {
|
|
64
98
|
this.continue = false;
|
|
65
99
|
this.decoder = false;
|
|
@@ -73,7 +107,8 @@ class NodeStreamer extends Transform {
|
|
|
73
107
|
this.continue = () => doContinue();
|
|
74
108
|
}
|
|
75
109
|
|
|
76
|
-
|
|
110
|
+
this.decoder.end();
|
|
111
|
+
return;
|
|
77
112
|
} else if (data.type === 'node' && this.filterFunc(data)) {
|
|
78
113
|
this.push(data);
|
|
79
114
|
// found matching node, create new handler
|
|
@@ -85,26 +120,44 @@ class NodeStreamer extends Transform {
|
|
|
85
120
|
callback();
|
|
86
121
|
}
|
|
87
122
|
|
|
123
|
+
/**
|
|
124
|
+
* @param {MimeNode} node
|
|
125
|
+
* @returns {StreamerNode}
|
|
126
|
+
*/
|
|
88
127
|
createDecoder(node) {
|
|
89
|
-
this.decoder = node.getDecoder();
|
|
128
|
+
this.decoder = /** @type {DecoderStream} */ (node.getDecoder());
|
|
90
129
|
|
|
91
|
-
let decoder = this.decoder;
|
|
130
|
+
let decoder = /** @type {DecoderStream} */ (this.decoder);
|
|
92
131
|
decoder.$reading = false;
|
|
93
132
|
|
|
94
|
-
if (/^text\//.test(node.contentType) && node.flowed) {
|
|
133
|
+
if (/^text\//.test(node.contentType || '') && node.flowed) {
|
|
95
134
|
let flowDecoder = decoder;
|
|
96
135
|
decoder = new FlowedDecoder({
|
|
97
136
|
delSp: node.delSp
|
|
98
137
|
});
|
|
99
|
-
flowDecoder.on(
|
|
100
|
-
|
|
101
|
-
|
|
138
|
+
flowDecoder.on(
|
|
139
|
+
'error',
|
|
140
|
+
/**
|
|
141
|
+
* Forwards flowed decoder errors to the output decoder.
|
|
142
|
+
*
|
|
143
|
+
* @param {Error} err Decoder error to forward.
|
|
144
|
+
* @returns {void}
|
|
145
|
+
*/
|
|
146
|
+
err => {
|
|
147
|
+
decoder.emit('error', err);
|
|
148
|
+
}
|
|
149
|
+
);
|
|
102
150
|
flowDecoder.pipe(decoder);
|
|
103
151
|
}
|
|
104
152
|
|
|
105
153
|
return {
|
|
106
154
|
node,
|
|
107
155
|
decoder,
|
|
156
|
+
/**
|
|
157
|
+
* Marks the selected node stream as consumed so the passthrough can continue.
|
|
158
|
+
*
|
|
159
|
+
* @returns {void}
|
|
160
|
+
*/
|
|
108
161
|
done: () => {
|
|
109
162
|
if (typeof this.continue === 'function') {
|
|
110
163
|
// called once input stream is processed
|