@zone-eu/mailsplit 5.4.8 → 5.4.10
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 +44 -0
- package/index.js +4 -1
- package/lib/chunked-passthrough.d.ts +13 -0
- package/lib/chunked-passthrough.js +13 -0
- package/lib/flowed-decoder.d.ts +13 -0
- package/lib/flowed-decoder.js +21 -3
- package/lib/headers.d.ts +24 -0
- package/lib/headers.js +145 -26
- package/lib/message-joiner.d.ts +12 -0
- package/lib/message-joiner.js +12 -0
- package/lib/message-splitter.d.ts +13 -0
- package/lib/message-splitter.js +55 -9
- package/lib/mime-node.d.ts +38 -0
- package/lib/mime-node.js +126 -47
- package/lib/node-rewriter.d.ts +18 -0
- package/lib/node-rewriter.js +48 -17
- package/lib/node-streamer.d.ts +18 -0
- package/lib/node-streamer.js +42 -8
- package/lib/types.d.ts +105 -0
- package/package.json +16 -6
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
17
|
* NodeRewriter Transform stream. Updates content for all nodes with specified mime type
|
|
10
18
|
*
|
|
11
19
|
* @constructor
|
|
12
|
-
* @param {
|
|
13
|
-
* @param {Function} rewriteAction Function to run with the node content
|
|
20
|
+
* @param {FilterFunc} filterFunc Function to select nodes to rewrite
|
|
21
|
+
* @param {Function} [rewriteAction] Function to run with the node content
|
|
14
22
|
*/
|
|
15
23
|
class NodeRewriter extends Transform {
|
|
24
|
+
/**
|
|
25
|
+
* @param {FilterFunc} filterFunc
|
|
26
|
+
* @param {Function} [rewriteAction]
|
|
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,19 +107,24 @@ 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
|
|
|
@@ -104,9 +141,6 @@ class NodeRewriter extends Transform {
|
|
|
104
141
|
firstChunk = false;
|
|
105
142
|
if (this.readable) {
|
|
106
143
|
this.push(node);
|
|
107
|
-
if (node.type === 'body') {
|
|
108
|
-
lastByte = node.value && node.value.length && node.value[node.value.length - 1];
|
|
109
|
-
}
|
|
110
144
|
}
|
|
111
145
|
}
|
|
112
146
|
|
|
@@ -144,9 +178,6 @@ class NodeRewriter extends Transform {
|
|
|
144
178
|
firstChunk = false;
|
|
145
179
|
if (this.readable) {
|
|
146
180
|
this.push(node);
|
|
147
|
-
if (node.type === 'body') {
|
|
148
|
-
lastByte = node.value && node.value.length && node.value[node.value.length - 1];
|
|
149
|
-
}
|
|
150
181
|
}
|
|
151
182
|
}
|
|
152
183
|
|
|
@@ -164,14 +195,14 @@ class NodeRewriter extends Transform {
|
|
|
164
195
|
}
|
|
165
196
|
});
|
|
166
197
|
|
|
167
|
-
if (/^text\//.test(node.contentType) && node.flowed) {
|
|
198
|
+
if (/^text\//.test(node.contentType || '') && node.flowed) {
|
|
168
199
|
// text/plain; format=flowed is a special case
|
|
169
200
|
let flowDecoder = decoder;
|
|
170
201
|
decoder = new FlowedDecoder({
|
|
171
202
|
delSp: node.delSp,
|
|
172
|
-
encoding: node.encoding
|
|
203
|
+
encoding: node.encoding || false
|
|
173
204
|
});
|
|
174
|
-
flowDecoder.on('error', err => {
|
|
205
|
+
flowDecoder.on('error', /** @param {Error} err */ err => {
|
|
175
206
|
decoder.emit('error', err);
|
|
176
207
|
});
|
|
177
208
|
flowDecoder.pipe(decoder);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Transform } from 'node:stream';
|
|
2
|
+
import type { FilterFunc, SplitterChunk, StreamerNode } from './types';
|
|
3
|
+
|
|
4
|
+
declare class NodeStreamer extends Transform {
|
|
5
|
+
constructor(filterFunc: FilterFunc, streamAction?: Function);
|
|
6
|
+
on(event: 'data', listener: (data: SplitterChunk) => void): this;
|
|
7
|
+
on(event: 'node', listener: (data: StreamerNode) => void): this;
|
|
8
|
+
once(event: 'data', listener: (data: SplitterChunk) => void): this;
|
|
9
|
+
once(event: 'node', listener: (data: StreamerNode) => void): this;
|
|
10
|
+
addListener(event: 'data', listener: (data: SplitterChunk) => void): this;
|
|
11
|
+
addListener(event: 'node', listener: (data: StreamerNode) => void): this;
|
|
12
|
+
prependListener(event: 'data', listener: (data: SplitterChunk) => void): this;
|
|
13
|
+
prependListener(event: 'node', listener: (data: StreamerNode) => void): this;
|
|
14
|
+
emit(event: 'data', data: SplitterChunk): boolean;
|
|
15
|
+
emit(event: 'node', data: StreamerNode): boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export = NodeStreamer;
|
package/lib/node-streamer.js
CHANGED
|
@@ -5,14 +5,25 @@
|
|
|
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
|
* NodeRewriter Transform stream. Updates content for all nodes with specified mime type
|
|
10
17
|
*
|
|
11
18
|
* @constructor
|
|
12
|
-
* @param {
|
|
13
|
-
* @param {Function} streamAction Function to run with the node content
|
|
19
|
+
* @param {FilterFunc} filterFunc Function to select nodes to stream
|
|
20
|
+
* @param {Function} [streamAction] Function to run with the node content
|
|
14
21
|
*/
|
|
15
22
|
class NodeStreamer extends Transform {
|
|
23
|
+
/**
|
|
24
|
+
* @param {FilterFunc} filterFunc
|
|
25
|
+
* @param {Function} [streamAction]
|
|
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
|
}
|
|
@@ -73,7 +102,8 @@ class NodeStreamer extends Transform {
|
|
|
73
102
|
this.continue = () => doContinue();
|
|
74
103
|
}
|
|
75
104
|
|
|
76
|
-
|
|
105
|
+
this.decoder.end();
|
|
106
|
+
return;
|
|
77
107
|
} else if (data.type === 'node' && this.filterFunc(data)) {
|
|
78
108
|
this.push(data);
|
|
79
109
|
// found matching node, create new handler
|
|
@@ -85,18 +115,22 @@ class NodeStreamer extends Transform {
|
|
|
85
115
|
callback();
|
|
86
116
|
}
|
|
87
117
|
|
|
118
|
+
/**
|
|
119
|
+
* @param {MimeNode} node
|
|
120
|
+
* @returns {StreamerNode}
|
|
121
|
+
*/
|
|
88
122
|
createDecoder(node) {
|
|
89
|
-
this.decoder = node.getDecoder();
|
|
123
|
+
this.decoder = /** @type {DecoderStream} */ (node.getDecoder());
|
|
90
124
|
|
|
91
|
-
let decoder = this.decoder;
|
|
125
|
+
let decoder = /** @type {DecoderStream} */ (this.decoder);
|
|
92
126
|
decoder.$reading = false;
|
|
93
127
|
|
|
94
|
-
if (/^text\//.test(node.contentType) && node.flowed) {
|
|
128
|
+
if (/^text\//.test(node.contentType || '') && node.flowed) {
|
|
95
129
|
let flowDecoder = decoder;
|
|
96
130
|
decoder = new FlowedDecoder({
|
|
97
131
|
delSp: node.delSp
|
|
98
132
|
});
|
|
99
|
-
flowDecoder.on('error', err => {
|
|
133
|
+
flowDecoder.on('error', /** @param {Error} err */ err => {
|
|
100
134
|
decoder.emit('error', err);
|
|
101
135
|
});
|
|
102
136
|
flowDecoder.pipe(decoder);
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { PassThrough, Transform } from 'node:stream';
|
|
2
|
+
import type Headers = require('./headers');
|
|
3
|
+
|
|
4
|
+
export type Maybe<T> = T | false;
|
|
5
|
+
export type PartNumberItem = number | 'TEXT';
|
|
6
|
+
export type PartNumber = PartNumberItem[];
|
|
7
|
+
|
|
8
|
+
export interface LibmimeOptions {
|
|
9
|
+
Iconv?: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SplitterOptions extends LibmimeOptions {
|
|
13
|
+
ignoreEmbedded?: boolean;
|
|
14
|
+
defaultInlineEmbedded?: boolean;
|
|
15
|
+
maxHeadSize?: number;
|
|
16
|
+
maxChildNodes?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ChunkedPassthroughOptions {
|
|
20
|
+
chunkSize?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FlowedDecoderOptions extends LibmimeOptions {
|
|
24
|
+
delSp?: boolean;
|
|
25
|
+
encoding?: string | false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface HeaderLine {
|
|
29
|
+
key: string;
|
|
30
|
+
line: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DecodedHeader {
|
|
34
|
+
key: string;
|
|
35
|
+
value: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MimeNode {
|
|
39
|
+
type: 'node';
|
|
40
|
+
root: boolean;
|
|
41
|
+
parentNode: MimeNode | false;
|
|
42
|
+
_boundary: Buffer | false;
|
|
43
|
+
_parentBoundary: Buffer | false;
|
|
44
|
+
_headerlen: number;
|
|
45
|
+
multipart: string | false;
|
|
46
|
+
encoding: string | false;
|
|
47
|
+
headers: Headers | false;
|
|
48
|
+
contentType: string | false;
|
|
49
|
+
charset: string | false;
|
|
50
|
+
disposition: string | false;
|
|
51
|
+
filename: string | false;
|
|
52
|
+
flowed: boolean;
|
|
53
|
+
delSp: boolean;
|
|
54
|
+
config: SplitterOptions;
|
|
55
|
+
partNr: PartNumber | false;
|
|
56
|
+
childPartNumbers: number;
|
|
57
|
+
rfc822: boolean;
|
|
58
|
+
messageNode?: boolean;
|
|
59
|
+
|
|
60
|
+
getPartNr(provided?: PartNumberItem): PartNumber;
|
|
61
|
+
addHeaderChunk(line?: Buffer | false): void;
|
|
62
|
+
parseHeaders(): void;
|
|
63
|
+
getHeaders(): Buffer;
|
|
64
|
+
setContentType(contentType?: string | false): void;
|
|
65
|
+
setCharset(charset?: string | false): void;
|
|
66
|
+
setFilename(filename?: string | false): void;
|
|
67
|
+
getDecoder(): Transform | PassThrough;
|
|
68
|
+
getEncoder(encoding?: string | false): Transform | PassThrough;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface MessageChunk {
|
|
72
|
+
node: MimeNode;
|
|
73
|
+
type: 'data' | 'body';
|
|
74
|
+
value: Buffer;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface EmptyChunk {
|
|
78
|
+
type: 'none';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type SplitterChunk = MimeNode | MessageChunk;
|
|
82
|
+
export type RewriterInput = SplitterChunk | EmptyChunk;
|
|
83
|
+
export type FilterFunc = (node: MimeNode) => boolean;
|
|
84
|
+
export type ErrorWithCode = Error & { code?: string };
|
|
85
|
+
export type ContinueCallback = () => void;
|
|
86
|
+
export type ContentStream = Transform | PassThrough;
|
|
87
|
+
export type DecoderStream = ContentStream & { $reading?: boolean };
|
|
88
|
+
|
|
89
|
+
export interface SplitterGroup {
|
|
90
|
+
node?: MimeNode;
|
|
91
|
+
type: 'none' | 'node' | 'data' | 'body';
|
|
92
|
+
value?: Buffer;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface RewriterNode {
|
|
96
|
+
node: MimeNode;
|
|
97
|
+
decoder: Transform;
|
|
98
|
+
encoder: Transform;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface StreamerNode {
|
|
102
|
+
node: MimeNode;
|
|
103
|
+
decoder: Transform;
|
|
104
|
+
done: () => void;
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -1,35 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zone-eu/mailsplit",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.10",
|
|
4
4
|
"description": "Split email messages into an object stream",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
6
7
|
"directories": {
|
|
7
8
|
"test": "test"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"test": "grunt",
|
|
11
|
-
"update": "rm -rf node_modules package-lock.json && ncu -u && npm install"
|
|
12
|
+
"update": "rm -rf node_modules package-lock.json && ncu -u && npm install",
|
|
13
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
12
14
|
},
|
|
13
15
|
"author": "Andris Reinman",
|
|
14
16
|
"license": "(MIT OR EUPL-1.1+)",
|
|
15
17
|
"dependencies": {
|
|
16
18
|
"libbase64": "1.3.0",
|
|
17
|
-
"libmime": "5.3.
|
|
19
|
+
"libmime": "5.3.8",
|
|
18
20
|
"libqp": "2.1.1"
|
|
19
21
|
},
|
|
20
22
|
"devDependencies": {
|
|
23
|
+
"@types/eslint": "9.6.1",
|
|
24
|
+
"@types/eslint-config-prettier": "6.11.3",
|
|
25
|
+
"@types/grunt": "0.4.32",
|
|
26
|
+
"@types/libmime": "5.3.0",
|
|
27
|
+
"@types/libqp": "1.1.3",
|
|
28
|
+
"@types/node": "25.7.0",
|
|
21
29
|
"eslint": "8.29.0",
|
|
22
30
|
"eslint-config-nodemailer": "1.2.0",
|
|
23
31
|
"eslint-config-prettier": "9.1.0",
|
|
24
|
-
"grunt": "1.6.
|
|
32
|
+
"grunt": "1.6.2",
|
|
25
33
|
"grunt-cli": "1.5.0",
|
|
26
34
|
"grunt-contrib-nodeunit": "5.0.0",
|
|
27
35
|
"grunt-eslint": "24.0.1",
|
|
28
|
-
"random-message": "1.1.0"
|
|
36
|
+
"random-message": "1.1.0",
|
|
37
|
+
"typescript": "6.0.3"
|
|
29
38
|
},
|
|
30
39
|
"files": [
|
|
31
40
|
"lib",
|
|
32
|
-
"index.js"
|
|
41
|
+
"index.js",
|
|
42
|
+
"index.d.ts"
|
|
33
43
|
],
|
|
34
44
|
"repository": {
|
|
35
45
|
"type": "git",
|