@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.
@@ -0,0 +1,239 @@
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
+ /** @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
+
16
+ /**
17
+ * NodeRewriter Transform stream. Updates content for all nodes selected by the
18
+ * filter function.
19
+ *
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.
22
+ */
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
+ */
28
+ constructor(filterFunc, rewriteAction) {
29
+ let options = {
30
+ readableObjectMode: true,
31
+ writableObjectMode: true
32
+ };
33
+ super(options);
34
+
35
+ this.filterFunc = filterFunc;
36
+ this.rewriteAction = rewriteAction;
37
+
38
+ /** @type {DecoderStream | false} */
39
+ this.decoder = false;
40
+ /** @type {ContentStream | false} */
41
+ this.encoder = false;
42
+ /** @type {ContinueCallback | false} */
43
+ this.continue = false;
44
+ }
45
+
46
+ /**
47
+ * @param {RewriterInput} data
48
+ * @param {BufferEncoding} encoding
49
+ * @param {import('stream').TransformCallback} callback
50
+ * @returns {void}
51
+ */
52
+ _transform(data, encoding, callback) {
53
+ this.processIncoming(data, callback);
54
+ }
55
+
56
+ /**
57
+ * @param {import('stream').TransformCallback} callback
58
+ * @returns {void}
59
+ */
60
+ _flush(callback) {
61
+ if (this.decoder) {
62
+ // emit an empty node just in case there is pending data to end
63
+ return this.processIncoming(
64
+ {
65
+ type: 'none'
66
+ },
67
+ callback
68
+ );
69
+ }
70
+ return callback();
71
+ }
72
+
73
+ /**
74
+ * @param {RewriterInput} data
75
+ * @param {import('stream').TransformCallback} callback
76
+ * @returns {void}
77
+ */
78
+ processIncoming(data, callback) {
79
+ if (this.decoder && data.type === 'body') {
80
+ // data to parse
81
+ if (!this.decoder.write(data.value)) {
82
+ this.decoder.once('drain', callback);
83
+ return;
84
+ } else {
85
+ return callback();
86
+ }
87
+ } else if (this.decoder && data.type !== 'body') {
88
+ // stop decoding.
89
+ // we can not process the current data chunk as we need to wait until
90
+ // the parsed data is completely processed, so we store a reference to the
91
+ // continue callback
92
+ this.continue = () => {
93
+ this.continue = false;
94
+ this.decoder = false;
95
+ this.encoder = false;
96
+ this.processIncoming(data, callback);
97
+ };
98
+ this.decoder.end();
99
+ return;
100
+ } else if (data.type === 'node' && this.filterFunc(data)) {
101
+ // found matching node, create new handler
102
+ this.emit('node', this.createDecodePair(data));
103
+ } else if (this.readable && data.type !== 'none') {
104
+ // we don't care about this data, just pass it over to the joiner
105
+ this.push(data);
106
+ }
107
+ callback();
108
+ }
109
+
110
+ /**
111
+ * @param {MimeNode} node
112
+ * @returns {RewriterNode}
113
+ */
114
+ createDecodePair(node) {
115
+ this.decoder = /** @type {DecoderStream} */ (node.getDecoder());
116
+
117
+ if (['base64', 'quoted-printable'].includes(node.encoding || '')) {
118
+ this.encoder = node.getEncoder();
119
+ } else {
120
+ this.encoder = node.getEncoder('quoted-printable');
121
+ }
122
+
123
+ /** @type {number | false} */
124
+ let lastByte = false;
125
+
126
+ let decoder = /** @type {DecoderStream} */ (this.decoder);
127
+ let encoder = /** @type {ContentStream} */ (this.encoder);
128
+ let firstChunk = true;
129
+ decoder.$reading = false;
130
+
131
+ /**
132
+ * Reads encoded replacement bytes and forwards them as body chunks.
133
+ *
134
+ * @returns {void | NodeJS.Immediate}
135
+ */
136
+ let readFromEncoder = () => {
137
+ decoder.$reading = true;
138
+
139
+ let data = encoder.read();
140
+ if (data === null) {
141
+ decoder.$reading = false;
142
+ return;
143
+ }
144
+
145
+ if (firstChunk) {
146
+ firstChunk = false;
147
+ if (this.readable) {
148
+ this.push(node);
149
+ }
150
+ }
151
+
152
+ let writeMore = true;
153
+ if (this.readable) {
154
+ writeMore = this.push({
155
+ node,
156
+ type: 'body',
157
+ value: data
158
+ });
159
+ lastByte = data && data.length && data[data.length - 1];
160
+ }
161
+
162
+ if (writeMore) {
163
+ return setImmediate(readFromEncoder);
164
+ } else {
165
+ encoder.pause();
166
+ // no idea how to catch drain? use timeout for now as poor man's substitute
167
+ // this.once('drain', () => encoder.resume());
168
+ setTimeout(() => {
169
+ encoder.resume();
170
+ setImmediate(readFromEncoder);
171
+ }, 100);
172
+ }
173
+ };
174
+
175
+ encoder.on('readable', () => {
176
+ if (!decoder.$reading) {
177
+ return readFromEncoder();
178
+ }
179
+ });
180
+
181
+ encoder.on('end', () => {
182
+ if (firstChunk) {
183
+ firstChunk = false;
184
+ if (this.readable) {
185
+ this.push(node);
186
+ }
187
+ }
188
+
189
+ if (lastByte !== 0x0a) {
190
+ // make sure there is a terminating line break
191
+ this.push({
192
+ node,
193
+ type: 'body',
194
+ value: Buffer.from([0x0a])
195
+ });
196
+ }
197
+
198
+ if (this.continue) {
199
+ return this.continue();
200
+ }
201
+ });
202
+
203
+ if (/^text\//.test(node.contentType || '') && node.flowed) {
204
+ // text/plain; format=flowed is a special case
205
+ let flowDecoder = decoder;
206
+ decoder = new FlowedDecoder({
207
+ delSp: node.delSp,
208
+ encoding: node.encoding || false
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
+ );
222
+ flowDecoder.pipe(decoder);
223
+
224
+ // we don't know what kind of data we are going to get, does it comply with the
225
+ // requirements of format=flowed, so we just cancel it
226
+ node.flowed = false;
227
+ node.delSp = false;
228
+ node.setContentType();
229
+ }
230
+
231
+ return {
232
+ node,
233
+ decoder,
234
+ encoder
235
+ };
236
+ }
237
+ }
238
+
239
+ module.exports = NodeRewriter;
@@ -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;
@@ -0,0 +1,174 @@
1
+ 'use strict';
2
+
3
+ // Helper class to stream selected nodes by MIME metadata
4
+
5
+ const Transform = require('stream').Transform;
6
+ const FlowedDecoder = require('./flowed-decoder');
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
+
15
+ /**
16
+ * NodeStreamer Transform stream. Exposes decoded content for nodes selected by
17
+ * the filter function while passing the original message through unchanged.
18
+ *
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.
21
+ */
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
+ */
27
+ constructor(filterFunc, streamAction) {
28
+ let options = {
29
+ readableObjectMode: true,
30
+ writableObjectMode: true
31
+ };
32
+ super(options);
33
+
34
+ this.filterFunc = filterFunc;
35
+ this.streamAction = streamAction;
36
+
37
+ /** @type {DecoderStream | false} */
38
+ this.decoder = false;
39
+ this.canContinue = false;
40
+ /** @type {ContinueCallback | false} */
41
+ this.continue = false;
42
+ }
43
+
44
+ /**
45
+ * @param {RewriterInput} data
46
+ * @param {BufferEncoding} encoding
47
+ * @param {import('stream').TransformCallback} callback
48
+ * @returns {void}
49
+ */
50
+ _transform(data, encoding, callback) {
51
+ this.processIncoming(data, callback);
52
+ }
53
+
54
+ /**
55
+ * @param {import('stream').TransformCallback} callback
56
+ * @returns {void}
57
+ */
58
+ _flush(callback) {
59
+ if (this.decoder) {
60
+ // emit an empty node just in case there is pending data to end
61
+ return this.processIncoming(
62
+ {
63
+ type: 'none'
64
+ },
65
+ callback
66
+ );
67
+ }
68
+ return callback();
69
+ }
70
+
71
+ /**
72
+ * @param {RewriterInput} data
73
+ * @param {import('stream').TransformCallback} callback
74
+ * @returns {void}
75
+ */
76
+ processIncoming(data, callback) {
77
+ if (this.decoder && data.type === 'body') {
78
+ // data to parse
79
+ this.push(data);
80
+ if (!this.decoder.write(data.value)) {
81
+ this.decoder.once('drain', callback);
82
+ return;
83
+ } else {
84
+ return callback();
85
+ }
86
+ } else if (this.decoder && data.type !== 'body') {
87
+ // stop decoding.
88
+ // we can not process the current data chunk as we need to wait until
89
+ // the parsed data is completely processed, so we store a reference to the
90
+ // continue callback
91
+
92
+ /**
93
+ * Resumes processing with the first chunk after the streamed node.
94
+ *
95
+ * @returns {void}
96
+ */
97
+ let doContinue = () => {
98
+ this.continue = false;
99
+ this.decoder = false;
100
+ this.canContinue = false;
101
+ this.processIncoming(data, callback);
102
+ };
103
+
104
+ if (this.canContinue) {
105
+ setImmediate(doContinue);
106
+ } else {
107
+ this.continue = () => doContinue();
108
+ }
109
+
110
+ this.decoder.end();
111
+ return;
112
+ } else if (data.type === 'node' && this.filterFunc(data)) {
113
+ this.push(data);
114
+ // found matching node, create new handler
115
+ this.emit('node', this.createDecoder(data));
116
+ } else if (this.readable && data.type !== 'none') {
117
+ // we don't care about this data, just pass it over to the joiner
118
+ this.push(data);
119
+ }
120
+ callback();
121
+ }
122
+
123
+ /**
124
+ * @param {MimeNode} node
125
+ * @returns {StreamerNode}
126
+ */
127
+ createDecoder(node) {
128
+ this.decoder = /** @type {DecoderStream} */ (node.getDecoder());
129
+
130
+ let decoder = /** @type {DecoderStream} */ (this.decoder);
131
+ decoder.$reading = false;
132
+
133
+ if (/^text\//.test(node.contentType || '') && node.flowed) {
134
+ let flowDecoder = decoder;
135
+ decoder = new FlowedDecoder({
136
+ delSp: node.delSp
137
+ });
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
+ );
150
+ flowDecoder.pipe(decoder);
151
+ }
152
+
153
+ return {
154
+ node,
155
+ decoder,
156
+ /**
157
+ * Marks the selected node stream as consumed so the passthrough can continue.
158
+ *
159
+ * @returns {void}
160
+ */
161
+ done: () => {
162
+ if (typeof this.continue === 'function') {
163
+ // called once input stream is processed
164
+ this.continue();
165
+ } else {
166
+ // called before input stream is processed
167
+ this.canContinue = true;
168
+ }
169
+ }
170
+ };
171
+ }
172
+ }
173
+
174
+ module.exports = NodeStreamer;