@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,117 @@
1
+ import type { DecodedHeader, HeaderLine, LibmimeOptions } from './types';
2
+
3
+ /** Mutable parser and builder for RFC-style message header blocks. */
4
+ declare class Headers {
5
+ /** Whether header lines have been modified after construction. */
6
+ changed: boolean;
7
+
8
+ /** Original unparsed header source, or `false` when constructed from parsed lines. */
9
+ headers: string | Buffer | false;
10
+
11
+ /** Whether `headers` has been parsed into `lines`. */
12
+ parsed: boolean;
13
+
14
+ /** Parsed header lines, or `false` until parsing occurs. */
15
+ lines: HeaderLine[] | false;
16
+
17
+ /** MBOX `From ` prefix line, or `false` when absent. */
18
+ mbox: string | false;
19
+
20
+ /** HTTP request prefix line, or `false` when absent. */
21
+ http: string | false;
22
+
23
+ /**
24
+ * Creates a mutable header collection.
25
+ *
26
+ * @param headers Raw header bytes/string, already parsed header lines, or `false` for an empty collection.
27
+ * @param config Optional libmime configuration.
28
+ */
29
+ constructor(headers?: string | Buffer | HeaderLine[] | false, config?: LibmimeOptions);
30
+
31
+ /**
32
+ * Checks whether at least one header with the requested key exists.
33
+ *
34
+ * @param key Header field name to find, case-insensitively.
35
+ * @returns `true` when the header exists.
36
+ */
37
+ hasHeader(key: string): boolean;
38
+
39
+ /**
40
+ * Gets all raw header lines for a key.
41
+ *
42
+ * @param key Header field name to find, case-insensitively.
43
+ * @returns Full decoded header lines, including field names.
44
+ */
45
+ get(key: string): string[];
46
+
47
+ /**
48
+ * Gets all decoded structured header values for a key.
49
+ *
50
+ * @param key Header field name to decode, case-insensitively.
51
+ * @returns Decoded header entries with key and value fields.
52
+ */
53
+ getDecoded(key: string): DecodedHeader[];
54
+
55
+ /**
56
+ * Gets the first decoded header value for a key.
57
+ *
58
+ * @param key Header field name to find, case-insensitively.
59
+ * @returns Trimmed decoded value, or an empty string when the header is absent.
60
+ */
61
+ getFirst(key: string): string;
62
+
63
+ /**
64
+ * Gets the mutable parsed header list.
65
+ *
66
+ * @returns Parsed header lines in message order.
67
+ */
68
+ getList(): HeaderLine[];
69
+
70
+ /**
71
+ * Adds a folded header line.
72
+ *
73
+ * @param key Header field name to add.
74
+ * @param value Header value; `undefined` leaves the collection unchanged.
75
+ * @param index Insertion index, where omitted or less than 1 inserts at the top.
76
+ * @returns Nothing.
77
+ */
78
+ add(key: string, value?: string | number | Buffer, index?: number): void;
79
+
80
+ /**
81
+ * Adds a preformatted header line.
82
+ *
83
+ * @param key Header field name used for normalized lookup.
84
+ * @param line Full header line to insert; falsy values leave the collection unchanged.
85
+ * @param index Insertion index, where omitted or less than 1 inserts at the top.
86
+ * @returns Nothing.
87
+ */
88
+ addFormatted(key: string, line?: string | Buffer | false, index?: number): void;
89
+
90
+ /**
91
+ * Removes all headers matching a key.
92
+ *
93
+ * @param key Header field name to remove, case-insensitively.
94
+ * @returns Nothing.
95
+ */
96
+ remove(key: string): void;
97
+
98
+ /**
99
+ * Replaces matching headers with a new folded header value.
100
+ *
101
+ * @param key Header field name to update.
102
+ * @param value Header value to write; `undefined` removes matching values without adding a replacement.
103
+ * @param relativeIndex Optional zero-based index among headers with the same key.
104
+ * @returns Nothing.
105
+ */
106
+ update(key: string, value?: string | number | Buffer, relativeIndex?: number): void;
107
+
108
+ /**
109
+ * Builds a raw header block.
110
+ *
111
+ * @param lineEnd Line ending to use when rebuilding changed headers; defaults to CRLF.
112
+ * @returns Header bytes ending with an empty header/body separator line.
113
+ */
114
+ build(lineEnd?: string | false): Buffer;
115
+ }
116
+
117
+ export = Headers;
package/lib/headers.js ADDED
@@ -0,0 +1,428 @@
1
+ 'use strict';
2
+
3
+ const libmime = require('libmime');
4
+
5
+ /** @typedef {import('..').HeaderLine} HeaderLine */
6
+ /** @typedef {import('..').LibmimeOptions} LibmimeOptions */
7
+ /** @typedef {import('..').DecodedHeader} DecodedHeader */
8
+
9
+ const Libmime = /** @type {any} */ (libmime.Libmime);
10
+
11
+ /**
12
+ * Parses and builds message headers. A Headers instance allows callers to
13
+ * inspect, delete, update, and add header lines.
14
+ */
15
+ class Headers {
16
+ /**
17
+ * @param {string | Buffer | HeaderLine[] | false} [headers] Raw header source or already parsed lines.
18
+ * @param {LibmimeOptions} [config] Optional libmime configuration.
19
+ */
20
+ constructor(headers, config) {
21
+ config = config || {};
22
+
23
+ if (Array.isArray(headers)) {
24
+ // already using parsed headers
25
+ this.changed = true;
26
+ /** @type {string | Buffer | false} */
27
+ this.headers = false;
28
+ this.parsed = true;
29
+ /** @type {HeaderLine[] | false} */
30
+ this.lines = headers;
31
+ } else {
32
+ // using original string/buffer headers
33
+ this.changed = false;
34
+ /** @type {string | Buffer | false} */
35
+ this.headers = headers || false;
36
+ this.parsed = false;
37
+ /** @type {HeaderLine[] | false} */
38
+ this.lines = false;
39
+ }
40
+ /** @type {string | false} */
41
+ this.mbox = false;
42
+ /** @type {string | false} */
43
+ this.http = false;
44
+
45
+ this.libmime = new Libmime({ Iconv: config.Iconv });
46
+ }
47
+
48
+ /**
49
+ * @param {string} key
50
+ * @returns {boolean}
51
+ */
52
+ hasHeader(key) {
53
+ if (!this.parsed) {
54
+ this._parseHeaders();
55
+ }
56
+ let lines = this._getLines();
57
+ key = this._normalizeHeader(key);
58
+ return typeof lines.find(line => line.key === key) === 'object';
59
+ }
60
+
61
+ /**
62
+ * @param {string} key
63
+ * @returns {string[]}
64
+ */
65
+ get(key) {
66
+ if (!this.parsed) {
67
+ this._parseHeaders();
68
+ }
69
+ let headerLines = this._getLines();
70
+ key = this._normalizeHeader(key);
71
+ let lines = headerLines.filter(line => line.key === key).map(line => this._decodeHeaderValue(line.line));
72
+
73
+ return lines;
74
+ }
75
+
76
+ /**
77
+ * @param {string} key
78
+ * @returns {DecodedHeader[]}
79
+ */
80
+ getDecoded(key) {
81
+ return this.get(key)
82
+ .map(line => this.libmime.decodeHeader(line))
83
+ .filter(line => line && line.value);
84
+ }
85
+
86
+ /**
87
+ * @param {string} key
88
+ * @returns {string}
89
+ */
90
+ getFirst(key) {
91
+ if (!this.parsed) {
92
+ this._parseHeaders();
93
+ }
94
+ let lines = this._getLines();
95
+ key = this._normalizeHeader(key);
96
+ let header = lines.find(line => line.key === key);
97
+ if (!header) {
98
+ return '';
99
+ }
100
+ return ((this.libmime.decodeHeader(this._decodeHeaderValue(header.line)) || {}).value || '').toString().trim();
101
+ }
102
+
103
+ /**
104
+ * @returns {HeaderLine[]}
105
+ */
106
+ getList() {
107
+ if (!this.parsed) {
108
+ this._parseHeaders();
109
+ }
110
+ return this._getLines();
111
+ }
112
+
113
+ /**
114
+ * @param {string} key
115
+ * @param {string | number | Buffer} [value]
116
+ * @param {number} [index]
117
+ * @returns {void}
118
+ */
119
+ add(key, value, index) {
120
+ if (typeof value === 'undefined') {
121
+ return;
122
+ }
123
+
124
+ if (typeof value === 'number') {
125
+ value = value.toString();
126
+ }
127
+
128
+ if (typeof value === 'string') {
129
+ value = Buffer.from(value);
130
+ }
131
+
132
+ value = value.toString('binary');
133
+ // a header value may not contain line breaks of its own, folding is added by foldLines
134
+ this.addFormatted(key, this.libmime.foldLines(key + ': ' + value.replace(/[\r\n]/g, ''), 76, false), index);
135
+ }
136
+
137
+ /**
138
+ * @param {string} key
139
+ * @param {string | Buffer | false} [line]
140
+ * @param {number} [index]
141
+ * @returns {void}
142
+ */
143
+ addFormatted(key, line, index) {
144
+ if (!this.parsed) {
145
+ this._parseHeaders();
146
+ }
147
+ let lines = this._getLines();
148
+ index = index || 0;
149
+ this.changed = true;
150
+
151
+ if (!line) {
152
+ return;
153
+ }
154
+
155
+ if (typeof line !== 'string') {
156
+ line = line.toString('binary');
157
+ }
158
+
159
+ // every header insertion runs through here, so this is where a value or a key
160
+ // built from untrusted input is stopped from injecting an extra header line
161
+ line = this._normalizeInsertedLine(line);
162
+ if (!line) {
163
+ return;
164
+ }
165
+
166
+ let header = {
167
+ key: this._normalizeHeader(key),
168
+ line
169
+ };
170
+
171
+ if (index < 1) {
172
+ lines.unshift(header);
173
+ } else if (index >= lines.length) {
174
+ lines.push(header);
175
+ } else {
176
+ lines.splice(index, 0, header);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * @param {string} key
182
+ * @returns {void}
183
+ */
184
+ remove(key) {
185
+ if (!this.parsed) {
186
+ this._parseHeaders();
187
+ }
188
+ let lines = this._getLines();
189
+ key = this._normalizeHeader(key);
190
+ for (let i = lines.length - 1; i >= 0; i--) {
191
+ if (lines[i].key === key) {
192
+ this.changed = true;
193
+ lines.splice(i, 1);
194
+ }
195
+ }
196
+ }
197
+
198
+ /**
199
+ * @param {string} key
200
+ * @param {string | number | Buffer} [value]
201
+ * @param {number} [relativeIndex]
202
+ * @returns {void}
203
+ */
204
+ update(key, value, relativeIndex) {
205
+ if (!this.parsed) {
206
+ this._parseHeaders();
207
+ }
208
+ let lines = this._getLines();
209
+ let keyName = key;
210
+ let index = 0;
211
+ key = this._normalizeHeader(key);
212
+ let relativeIndexCount = 0;
213
+ let relativeMatchFound = false;
214
+ for (let i = lines.length - 1; i >= 0; i--) {
215
+ if (lines[i].key === key) {
216
+ if (relativeIndex && relativeIndex !== relativeIndexCount) {
217
+ relativeIndexCount++;
218
+ continue;
219
+ }
220
+ index = i;
221
+ this.changed = true;
222
+ lines.splice(i, 1);
223
+ if (relativeIndex) {
224
+ relativeMatchFound = true;
225
+ break;
226
+ }
227
+ }
228
+ }
229
+
230
+ if (relativeIndex && !relativeMatchFound) {
231
+ return;
232
+ }
233
+
234
+ this.add(keyName, value, index);
235
+ }
236
+
237
+ /**
238
+ * Serializes the headers. Unmodified headers are returned byte for byte as they
239
+ * were received, otherwise every line is rebuilt with `lineEnd` line endings.
240
+ *
241
+ * @param {string | false} [lineEnd] Line ending to use, defaults to CRLF.
242
+ * @returns {Buffer}
243
+ */
244
+ build(lineEnd) {
245
+ if (!this.changed && !lineEnd) {
246
+ return typeof this.headers === 'string' ? Buffer.from(this.headers, 'binary') : this.headers || Buffer.alloc(0);
247
+ }
248
+
249
+ if (!this.parsed) {
250
+ this._parseHeaders();
251
+ }
252
+ let lines = this._getLines();
253
+
254
+ const ending = lineEnd || '\r\n';
255
+
256
+ let headers = lines
257
+ .map(line => this._normalizeLineBreaks(line.line, ending))
258
+ // an empty line would close the header block and demote every later header
259
+ // into the body, so a line left with nothing in it is dropped instead
260
+ .filter(line => line !== '')
261
+ .map(line => this._buildHeaderLine(line))
262
+ .reduce((joined, line, idx) => {
263
+ if (idx) {
264
+ joined.push(Buffer.from(ending, 'binary'));
265
+ }
266
+ joined.push(line);
267
+ return joined;
268
+ }, /** @type {Buffer[]} */ ([]));
269
+
270
+ headers.push(Buffer.from(ending + ending, 'binary'));
271
+
272
+ if (this.mbox) {
273
+ headers.unshift(Buffer.from(this.mbox + ending, 'binary'));
274
+ }
275
+
276
+ if (this.http) {
277
+ headers.unshift(Buffer.from(this.http + ending, 'binary'));
278
+ }
279
+
280
+ return Buffer.concat(headers);
281
+ }
282
+
283
+ /**
284
+ * @param {string} key
285
+ * @returns {string}
286
+ */
287
+ _normalizeHeader(key) {
288
+ return (key || '').toLowerCase().trim();
289
+ }
290
+
291
+ /**
292
+ * Rewrites the line breaks of a header line so that the line can only ever parse
293
+ * back as the single header it was reported as. A line break followed by whitespace
294
+ * is folding and becomes `lineEnd`, every other line break would start a new header
295
+ * line and is dropped.
296
+ *
297
+ * A bare <CR> is never a line break for _parseHeaders, so promoting one here would
298
+ * emit a header line that was never reported as parsed.
299
+ *
300
+ * @param {string} line Header line to normalize.
301
+ * @param {string} lineEnd Line ending to fold with.
302
+ * @returns {string} Line with only folding line breaks left.
303
+ */
304
+ _normalizeLineBreaks(line, lineEnd) {
305
+ return (
306
+ line
307
+ // lines are joined with lineEnd, so a line that opens with a break of its own
308
+ // would close the header block. Dropping only the break leaves any whitespace
309
+ // behind it folding into the line before, which adds no header of its own.
310
+ .replace(/^[\r\n]+/, '')
311
+ .replace(/\r\n|\r|\n/g, (match, offset, source) => (match !== '\r' && this._isFoldingChar(source.charAt(offset + match.length)) ? lineEnd : ''))
312
+ );
313
+ }
314
+
315
+ /**
316
+ * Prepares a caller supplied line for insertion. On top of the line break rules an
317
+ * inserted line has to stand on its own: a leading fold or indent would attach it to
318
+ * whichever header happens to precede it, and a leading line break would close the
319
+ * header block outright.
320
+ *
321
+ * Lines that were parsed out of a message keep their leading whitespace instead, so
322
+ * that rebuilding can never turn an indented continuation into a header of its own.
323
+ *
324
+ * An inserted line is normalized twice, here with CRLF and again in build() with the
325
+ * line ending the caller asked for. That is only sound because _normalizeLineBreaks is
326
+ * idempotent over its own output: the folds this pass emits are still recognized as
327
+ * folds by the next one. Any change to how a fold is represented has to keep that true.
328
+ *
329
+ * @param {string} line Formatted header line supplied by the caller.
330
+ * @returns {string} Line that inserts as exactly one header, or an empty string.
331
+ */
332
+ _normalizeInsertedLine(line) {
333
+ return this._normalizeLineBreaks(line.replace(/^[\r\n \t]+/, ''), '\r\n');
334
+ }
335
+
336
+ /**
337
+ * Tells whether a character continues the previous header line rather than
338
+ * starting a new one. Used by both the parser and the builder so that the two
339
+ * can not disagree on what folding is.
340
+ *
341
+ * @param {string} chr Character that follows a line break.
342
+ * @returns {boolean} True if the line break is folding.
343
+ */
344
+ _isFoldingChar(chr) {
345
+ return chr === ' ' || chr === '\t';
346
+ }
347
+
348
+ /**
349
+ * @returns {HeaderLine[]}
350
+ */
351
+ _getLines() {
352
+ if (!this.lines) {
353
+ this.lines = [];
354
+ }
355
+ return this.lines;
356
+ }
357
+
358
+ /**
359
+ * @returns {void}
360
+ */
361
+ _parseHeaders() {
362
+ if (!this.headers) {
363
+ this.lines = [];
364
+ this.parsed = true;
365
+ return;
366
+ }
367
+
368
+ /** @type {Array<string | HeaderLine>} */
369
+ let lines = this.headers
370
+ .toString('binary')
371
+ .replace(/[\r\n]+$/, '')
372
+ .split(/\r?\n/);
373
+
374
+ for (let i = lines.length - 1; i >= 0; i--) {
375
+ let currentLine = /** @type {string} */ (lines[i]);
376
+ if (i && this._isFoldingChar(currentLine.charAt(0))) {
377
+ lines[i - 1] = /** @type {string} */ (lines[i - 1]) + '\r\n' + currentLine;
378
+ lines.splice(i, 1);
379
+ } else {
380
+ let line = currentLine;
381
+ if (!i && /^From /i.test(line)) {
382
+ // mbox file
383
+ this.mbox = line;
384
+ lines.splice(i, 1);
385
+ continue;
386
+ } else if (!i && /^POST /i.test(line)) {
387
+ // HTTP POST request
388
+ this.http = line;
389
+ lines.splice(i, 1);
390
+ continue;
391
+ }
392
+ let key = this._normalizeHeader(line.substr(0, line.indexOf(':')));
393
+ lines[i] = {
394
+ key,
395
+ line
396
+ };
397
+ }
398
+ }
399
+
400
+ this.lines = /** @type {HeaderLine[]} */ (lines);
401
+ this.parsed = true;
402
+ }
403
+
404
+ /**
405
+ * @param {string} line
406
+ * @returns {Buffer}
407
+ */
408
+ _buildHeaderLine(line) {
409
+ let value = this._decodeHeaderValue(line);
410
+ return Buffer.from(value, value === line ? 'binary' : 'utf8');
411
+ }
412
+
413
+ /**
414
+ * @param {string} str
415
+ * @returns {string}
416
+ */
417
+ _decodeHeaderValue(str) {
418
+ if (!str) {
419
+ return str;
420
+ }
421
+
422
+ let utf8 = Buffer.from(str, 'binary').toString('utf8');
423
+ return utf8.includes('\uFFFD') ? str : utf8;
424
+ }
425
+ }
426
+
427
+ // expose to the world
428
+ module.exports = Headers;
@@ -0,0 +1,56 @@
1
+ import { Transform } from 'node:stream';
2
+
3
+ /** Transform stream that joins splitter objects back into raw email bytes. */
4
+ declare class MessageJoiner extends Transform {
5
+ /**
6
+ * Creates a joiner that accepts splitter objects and emits Buffer chunks.
7
+ */
8
+ constructor();
9
+
10
+ /**
11
+ * Registers a listener for generated message bytes.
12
+ *
13
+ * @param event Event name.
14
+ * @param listener Receives each generated Buffer chunk.
15
+ * @returns This joiner instance.
16
+ */
17
+ on(event: 'data', listener: (data: Buffer) => void): this;
18
+
19
+ /**
20
+ * Registers a one-time listener for generated message bytes.
21
+ *
22
+ * @param event Event name.
23
+ * @param listener Receives the next generated Buffer chunk.
24
+ * @returns This joiner instance.
25
+ */
26
+ once(event: 'data', listener: (data: Buffer) => void): this;
27
+
28
+ /**
29
+ * Adds a listener for generated message bytes.
30
+ *
31
+ * @param event Event name.
32
+ * @param listener Receives each generated Buffer chunk.
33
+ * @returns This joiner instance.
34
+ */
35
+ addListener(event: 'data', listener: (data: Buffer) => void): this;
36
+
37
+ /**
38
+ * Prepends a listener for generated message bytes.
39
+ *
40
+ * @param event Event name.
41
+ * @param listener Receives each generated Buffer chunk.
42
+ * @returns This joiner instance.
43
+ */
44
+ prependListener(event: 'data', listener: (data: Buffer) => void): this;
45
+
46
+ /**
47
+ * Emits a generated message byte chunk.
48
+ *
49
+ * @param event Event name.
50
+ * @param data Buffer chunk to emit.
51
+ * @returns `true` when the event had listeners.
52
+ */
53
+ emit(event: 'data', data: Buffer): boolean;
54
+ }
55
+
56
+ export = MessageJoiner;
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const Transform = require('stream').Transform;
4
+
5
+ /** @typedef {import('..').SplitterChunk} SplitterChunk */
6
+
7
+ /**
8
+ * Transform stream that joins splitter objects back into raw message bytes.
9
+ */
10
+ class MessageJoiner extends Transform {
11
+ /**
12
+ * Creates a joiner that accepts splitter objects and emits Buffer chunks.
13
+ */
14
+ constructor() {
15
+ let options = {
16
+ readableObjectMode: false,
17
+ writableObjectMode: true
18
+ };
19
+ super(options);
20
+ }
21
+
22
+ /**
23
+ * @param {SplitterChunk | Buffer} obj
24
+ * @param {BufferEncoding} encoding
25
+ * @param {import('stream').TransformCallback} callback
26
+ * @returns {void}
27
+ */
28
+ _transform(obj, encoding, callback) {
29
+ if (Buffer.isBuffer(obj)) {
30
+ this.push(obj);
31
+ } else if (obj.type === 'node') {
32
+ this.push(obj.getHeaders());
33
+ } else if (obj.value) {
34
+ this.push(obj.value);
35
+ }
36
+ return callback();
37
+ }
38
+
39
+ /**
40
+ * @param {import('stream').TransformCallback} callback
41
+ * @returns {void}
42
+ */
43
+ _flush(callback) {
44
+ return callback();
45
+ }
46
+ }
47
+
48
+ module.exports = MessageJoiner;
@@ -0,0 +1,59 @@
1
+ import { Transform } from 'node:stream';
2
+ import type { SplitterChunk, SplitterOptions } from './types';
3
+
4
+ /** Transform stream that splits raw email bytes into MIME node and content chunks. */
5
+ declare class MessageSplitter extends Transform {
6
+ /**
7
+ * Creates a splitter that accepts Buffer input and emits `SplitterChunk` objects.
8
+ *
9
+ * @param config Optional parser limits and embedded-message behavior.
10
+ */
11
+ constructor(config?: SplitterOptions);
12
+
13
+ /**
14
+ * Registers a listener for parsed splitter chunks.
15
+ *
16
+ * @param event Event name.
17
+ * @param listener Receives each parsed MIME node, data chunk, or body chunk.
18
+ * @returns This splitter instance.
19
+ */
20
+ on(event: 'data', listener: (data: SplitterChunk) => void): this;
21
+
22
+ /**
23
+ * Registers a one-time listener for the next parsed splitter chunk.
24
+ *
25
+ * @param event Event name.
26
+ * @param listener Receives the next parsed MIME node, data chunk, or body chunk.
27
+ * @returns This splitter instance.
28
+ */
29
+ once(event: 'data', listener: (data: SplitterChunk) => void): this;
30
+
31
+ /**
32
+ * Adds a listener for parsed splitter chunks.
33
+ *
34
+ * @param event Event name.
35
+ * @param listener Receives each parsed MIME node, data chunk, or body chunk.
36
+ * @returns This splitter instance.
37
+ */
38
+ addListener(event: 'data', listener: (data: SplitterChunk) => void): this;
39
+
40
+ /**
41
+ * Prepends a listener for parsed splitter chunks.
42
+ *
43
+ * @param event Event name.
44
+ * @param listener Receives each parsed MIME node, data chunk, or body chunk.
45
+ * @returns This splitter instance.
46
+ */
47
+ prependListener(event: 'data', listener: (data: SplitterChunk) => void): this;
48
+
49
+ /**
50
+ * Emits a parsed splitter chunk.
51
+ *
52
+ * @param event Event name.
53
+ * @param data MIME node, data chunk, or body chunk to emit.
54
+ * @returns `true` when the event had listeners.
55
+ */
56
+ emit(event: 'data', data: SplitterChunk): boolean;
57
+ }
58
+
59
+ export = MessageSplitter;