@mavogel/awscdk-rootmail 0.1.21 → 0.1.23
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/.jsii +3 -3
- package/lib/rootmail.js +1 -1
- package/lib/ses-receive.js +1 -1
- package/node_modules/@zone-eu/mailsplit/lib/headers.js +15 -4
- package/node_modules/@zone-eu/mailsplit/lib/message-splitter.js +34 -23
- package/node_modules/@zone-eu/mailsplit/lib/mime-node.js +61 -5
- package/node_modules/@zone-eu/mailsplit/package.json +3 -3
- package/node_modules/libmime/CHANGELOG.md +8 -0
- package/node_modules/libmime/lib/charset.js +14 -2
- package/node_modules/libmime/lib/libmime.js +30 -6
- package/node_modules/libmime/package.json +2 -2
- package/node_modules/libqp/.github/VULNERABILITY_REPORT.yml +75 -0
- package/node_modules/libqp/CHANGELOG.md +7 -0
- package/node_modules/libqp/SECURITY.md +147 -0
- package/node_modules/libqp/lib/libqp.js +96 -24
- package/node_modules/libqp/package.json +1 -1
- package/node_modules/libqp/test/libqp-test.js +108 -0
- package/node_modules/mailparser/.release-please-manifest.json +1 -1
- package/node_modules/mailparser/CHANGELOG.md +9 -0
- package/node_modules/mailparser/README.md +2 -1
- package/node_modules/mailparser/lib/mail-parser.js +160 -65
- package/node_modules/mailparser/lib/simple-parser.js +3 -3
- package/node_modules/mailparser/package.json +4 -4
- package/node_modules/nodemailer/CHANGELOG.md +7 -0
- package/node_modules/nodemailer/dist/cjs/dkim/index.js +3 -2
- package/node_modules/nodemailer/dist/cjs/fetch/index.d.ts +5 -0
- package/node_modules/nodemailer/dist/cjs/fetch/index.js +30 -0
- package/node_modules/nodemailer/dist/cjs/mailer/index.js +4 -1
- package/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +5 -1
- package/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +25 -13
- package/node_modules/nodemailer/dist/cjs/mime-node/index.js +49 -6
- package/node_modules/nodemailer/dist/cjs/package-info.d.ts +1 -1
- package/node_modules/nodemailer/dist/cjs/package-info.js +1 -1
- package/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +16 -1
- package/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +12 -1
- package/node_modules/nodemailer/dist/cjs/well-known/services.js +7 -0
- package/node_modules/nodemailer/dist/cjs/xoauth2/index.js +8 -2
- package/node_modules/nodemailer/dist/esm/dkim/index.js +3 -2
- package/node_modules/nodemailer/dist/esm/fetch/index.d.ts +5 -0
- package/node_modules/nodemailer/dist/esm/fetch/index.js +30 -0
- package/node_modules/nodemailer/dist/esm/mailer/index.js +4 -1
- package/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +5 -1
- package/node_modules/nodemailer/dist/esm/mime-funcs/index.js +25 -13
- package/node_modules/nodemailer/dist/esm/mime-node/index.js +49 -6
- package/node_modules/nodemailer/dist/esm/package-info.d.ts +1 -1
- package/node_modules/nodemailer/dist/esm/package-info.js +1 -1
- package/node_modules/nodemailer/dist/esm/smtp-connection/index.js +16 -1
- package/node_modules/nodemailer/dist/esm/smtp-transport/index.js +12 -1
- package/node_modules/nodemailer/dist/esm/well-known/services.js +7 -0
- package/node_modules/nodemailer/dist/esm/xoauth2/index.js +8 -2
- package/node_modules/nodemailer/dist/well-known/services.json +8 -0
- package/node_modules/nodemailer/package.json +1 -1
- package/package.json +3 -3
|
@@ -17,6 +17,18 @@ function encode(buffer) {
|
|
|
17
17
|
buffer = Buffer.from(buffer, 'utf-8');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
return encodeBytes(buffer, true);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Encodes the bytes of a Buffer. The Encoder stream feeds it one chunk at a time, so it has
|
|
25
|
+
* to be told whether the first byte starts an output line, which a plain encode() call always does
|
|
26
|
+
*
|
|
27
|
+
* @param {Buffer} buffer Buffer to convert
|
|
28
|
+
* @param {Boolean} lineStart True if the first byte starts a line
|
|
29
|
+
* @returns {String} Quoted-Printable encoded string
|
|
30
|
+
*/
|
|
31
|
+
function encodeBytes(buffer, lineStart) {
|
|
20
32
|
// usable characters that do not need encoding
|
|
21
33
|
let ranges = [
|
|
22
34
|
// https://tools.ietf.org/html/rfc2045#section-6.7
|
|
@@ -32,7 +44,13 @@ function encode(buffer) {
|
|
|
32
44
|
for (let i = 0, len = buffer.length; i < len; i++) {
|
|
33
45
|
ord = buffer[i];
|
|
34
46
|
// if the char is in allowed range, then keep as is, unless it is a ws in the end of a line
|
|
35
|
-
|
|
47
|
+
// or a hyphen in the start of a line. A line starting with two hyphens reads as a MIME
|
|
48
|
+
// boundary delimiter, so a decoded body that gets encoded again could inject a part
|
|
49
|
+
if (
|
|
50
|
+
checkRanges(ord, ranges) &&
|
|
51
|
+
!((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d)) &&
|
|
52
|
+
!(ord === 0x2d && (i === 0 ? lineStart : buffer[i - 1] === 0x0a))
|
|
53
|
+
) {
|
|
36
54
|
result += String.fromCharCode(ord);
|
|
37
55
|
continue;
|
|
38
56
|
}
|
|
@@ -53,31 +71,63 @@ function decode(str) {
|
|
|
53
71
|
// 'binary' maps every byte to one char code 0-255. A UTF-8 read would collapse raw
|
|
54
72
|
// multi-byte chars into one byte below and turn raw latin1 bytes into U+FFFD
|
|
55
73
|
str = (Buffer.isBuffer(str) ? str.toString('binary') : (str || '').toString())
|
|
56
|
-
// remove invalid whitespace from the end of lines
|
|
57
|
-
|
|
74
|
+
// remove invalid whitespace from the end of lines. The leading group keeps this linear,
|
|
75
|
+
// a plain /[\t ]+$/gm rescans the rest of a whitespace run from every position inside it,
|
|
76
|
+
// which is quadratic. The lookahead lists the line terminators multiline $ matches before
|
|
77
|
+
.replace(/(^|[^ \t])[ \t]+(?=[\n\r\u2028\u2029]|$)/g, '$1')
|
|
58
78
|
// remove soft line breaks
|
|
59
79
|
.replace(/\=(?:\r?\n|$)/g, '');
|
|
60
80
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (let i = 0, len = str.length; i < len; i++) {
|
|
69
|
-
chr = str.charAt(i);
|
|
70
|
-
if (chr === '=' && (hex = str.substr(i + 1, 2)) && /[\da-fA-F]{2}/.test(hex)) {
|
|
71
|
-
buffer[bufferPos++] = parseInt(hex, 16);
|
|
81
|
+
// every =XX escape decodes into one byte, so count them to size the output.
|
|
82
|
+
// Counted with a loop, a match() would build an array with a string for every escape
|
|
83
|
+
let len = str.length;
|
|
84
|
+
let encodedBytesCount = 0;
|
|
85
|
+
for (let i = str.indexOf('='); i >= 0 && i < len - 2; i = str.indexOf('=', i + 1)) {
|
|
86
|
+
if (hexValue(str.charCodeAt(i + 1)) >= 0 && hexValue(str.charCodeAt(i + 2)) >= 0) {
|
|
87
|
+
encodedBytesCount++;
|
|
72
88
|
i += 2;
|
|
73
|
-
continue;
|
|
74
89
|
}
|
|
75
|
-
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let buffer = Buffer.alloc(len - encodedBytesCount * 2);
|
|
93
|
+
let bufferPos = 0;
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < len; i++) {
|
|
96
|
+
let code = str.charCodeAt(i);
|
|
97
|
+
if (code === 0x3d /* = */ && i < len - 2) {
|
|
98
|
+
let hi = hexValue(str.charCodeAt(i + 1));
|
|
99
|
+
let lo = hexValue(str.charCodeAt(i + 2));
|
|
100
|
+
if (hi >= 0 && lo >= 0) {
|
|
101
|
+
buffer[bufferPos++] = hi * 16 + lo;
|
|
102
|
+
i += 2;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
buffer[bufferPos++] = code;
|
|
76
107
|
}
|
|
77
108
|
|
|
78
109
|
return buffer;
|
|
79
110
|
}
|
|
80
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Helper function to read the value of a hex digit
|
|
114
|
+
*
|
|
115
|
+
* @param {Number} code Char code
|
|
116
|
+
* @returns {Number} 0-15, or -1 if the char is not a hex digit
|
|
117
|
+
*/
|
|
118
|
+
function hexValue(code) {
|
|
119
|
+
if (code >= 0x30 && code <= 0x39) {
|
|
120
|
+
return code - 0x30; // 0-9
|
|
121
|
+
}
|
|
122
|
+
if (code >= 0x41 && code <= 0x46) {
|
|
123
|
+
return code - 0x41 + 10; // A-F
|
|
124
|
+
}
|
|
125
|
+
if (code >= 0x61 && code <= 0x66) {
|
|
126
|
+
return code - 0x61 + 10; // a-f
|
|
127
|
+
}
|
|
128
|
+
return -1;
|
|
129
|
+
}
|
|
130
|
+
|
|
81
131
|
/**
|
|
82
132
|
* Adds soft line breaks to a Quoted-Printable string
|
|
83
133
|
*
|
|
@@ -98,12 +148,27 @@ function wrap(str, lineLength) {
|
|
|
98
148
|
match,
|
|
99
149
|
code,
|
|
100
150
|
line,
|
|
151
|
+
maxLength,
|
|
152
|
+
afterSoftBreak = false,
|
|
101
153
|
lineMargin = Math.floor(lineLength / 3),
|
|
102
154
|
result = '';
|
|
103
155
|
|
|
104
156
|
// insert soft linebreaks where needed
|
|
105
157
|
while (pos < len) {
|
|
106
|
-
|
|
158
|
+
maxLength = lineLength;
|
|
159
|
+
|
|
160
|
+
if (afterSoftBreak) {
|
|
161
|
+
afterSoftBreak = false;
|
|
162
|
+
// a line started by a soft break must not begin with a hyphen either, see encode().
|
|
163
|
+
// The escape takes the room of three characters off this line
|
|
164
|
+
if (str.charAt(pos) === '-') {
|
|
165
|
+
result += '=2D';
|
|
166
|
+
pos++;
|
|
167
|
+
maxLength -= 3;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
line = str.substr(pos, maxLength);
|
|
107
172
|
if ((match = line.match(/\r\n/))) {
|
|
108
173
|
line = line.substr(0, match.index + match[0].length);
|
|
109
174
|
result += line;
|
|
@@ -122,7 +187,7 @@ function wrap(str, lineLength) {
|
|
|
122
187
|
result += line;
|
|
123
188
|
pos += line.length;
|
|
124
189
|
continue;
|
|
125
|
-
} else if (line.length >
|
|
190
|
+
} else if (line.length > maxLength - lineMargin && (match = line.substr(-lineMargin).match(/[ \t\.,!\?][^ \t\.,!\?]*$/))) {
|
|
126
191
|
// truncate to nearest space
|
|
127
192
|
line = line.substr(0, line.length - (match[0].length - 1));
|
|
128
193
|
} else if (line.match(/\=[\da-f]{0,2}$/i)) {
|
|
@@ -147,13 +212,14 @@ function wrap(str, lineLength) {
|
|
|
147
212
|
}
|
|
148
213
|
|
|
149
214
|
if (pos + line.length < len && line.substr(-1) !== '\n') {
|
|
150
|
-
if (line.length ===
|
|
215
|
+
if (line.length === maxLength && line.match(/\=[\da-f]{2}$/i)) {
|
|
151
216
|
line = line.substr(0, line.length - 3);
|
|
152
|
-
} else if (line.length ===
|
|
217
|
+
} else if (line.length === maxLength) {
|
|
153
218
|
line = line.substr(0, line.length - 1);
|
|
154
219
|
}
|
|
155
220
|
pos += line.length;
|
|
156
221
|
line += '=\r\n';
|
|
222
|
+
afterSoftBreak = true;
|
|
157
223
|
} else {
|
|
158
224
|
pos += line.length;
|
|
159
225
|
}
|
|
@@ -205,6 +271,8 @@ class Encoder extends Transform {
|
|
|
205
271
|
}
|
|
206
272
|
|
|
207
273
|
this._curLine = '';
|
|
274
|
+
// true until a chunk that does not end with a line break has been encoded
|
|
275
|
+
this._lineStart = true;
|
|
208
276
|
|
|
209
277
|
this.inputBytes = 0;
|
|
210
278
|
this.outputBytes = 0;
|
|
@@ -225,8 +293,11 @@ class Encoder extends Transform {
|
|
|
225
293
|
|
|
226
294
|
this.inputBytes += chunk.length;
|
|
227
295
|
|
|
296
|
+
let lineStart = this._lineStart;
|
|
297
|
+
this._lineStart = chunk[chunk.length - 1] === 0x0a;
|
|
298
|
+
|
|
228
299
|
if (this.options.lineLength) {
|
|
229
|
-
qp = this._curLine +
|
|
300
|
+
qp = this._curLine + encodeBytes(chunk, lineStart);
|
|
230
301
|
qp = wrap(qp, this.options.lineLength);
|
|
231
302
|
qp = qp.replace(/(^|\n)([^\n]*)$/, (match, lineBreak, lastLine) => {
|
|
232
303
|
this._curLine = lastLine;
|
|
@@ -238,7 +309,7 @@ class Encoder extends Transform {
|
|
|
238
309
|
this.push(qp);
|
|
239
310
|
}
|
|
240
311
|
} else {
|
|
241
|
-
qp =
|
|
312
|
+
qp = encodeBytes(chunk, lineStart);
|
|
242
313
|
this.outputBytes += qp.length;
|
|
243
314
|
this.push(qp, 'ascii');
|
|
244
315
|
}
|
|
@@ -286,7 +357,8 @@ class Decoder extends Transform {
|
|
|
286
357
|
chunk = Buffer.from(chunk, encoding);
|
|
287
358
|
}
|
|
288
359
|
|
|
289
|
-
|
|
360
|
+
// kept as a binary string, so joining the chunks is the only copy made before decoding
|
|
361
|
+
this.qpChunks.push(chunk.toString('binary'));
|
|
290
362
|
this.inputBytes += chunk.length;
|
|
291
363
|
|
|
292
364
|
done();
|
|
@@ -294,7 +366,7 @@ class Decoder extends Transform {
|
|
|
294
366
|
|
|
295
367
|
_flush(done) {
|
|
296
368
|
if (this.inputBytes) {
|
|
297
|
-
let buf = decode(
|
|
369
|
+
let buf = decode(this.qpChunks.join(''));
|
|
298
370
|
this.outputBytes += buf.length;
|
|
299
371
|
this.push(buf);
|
|
300
372
|
}
|
|
@@ -256,3 +256,111 @@ test('Raw 8-bit bytes in Quoted-Printable input', async t => {
|
|
|
256
256
|
assert.deepStrictEqual(await decodeStream([input.subarray(0, 9), input.subarray(9)]), expected);
|
|
257
257
|
});
|
|
258
258
|
});
|
|
259
|
+
|
|
260
|
+
test('Hyphens at the start of a line', async t => {
|
|
261
|
+
const encodeStream = async (chunks, options) =>
|
|
262
|
+
await new Promise((resolve, reject) => {
|
|
263
|
+
const encoder = new libqp.Encoder(options);
|
|
264
|
+
const output = [];
|
|
265
|
+
encoder.on('data', chunk => output.push(chunk));
|
|
266
|
+
encoder.on('end', () => resolve(Buffer.concat(output).toString()));
|
|
267
|
+
encoder.on('error', reject);
|
|
268
|
+
for (const chunk of chunks) {
|
|
269
|
+
encoder.write(chunk);
|
|
270
|
+
}
|
|
271
|
+
encoder.end();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await t.test('encode() escapes a hyphen that starts a line', async () => {
|
|
275
|
+
// two hyphens at the start of a line would read as a MIME boundary delimiter
|
|
276
|
+
assert.strictEqual(libqp.encode('--boundary\r\n-x'), '=2D-boundary\r\n=2Dx');
|
|
277
|
+
assert.strictEqual(libqp.encode('\n-'), '\n=2D');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await t.test('encode() keeps a hyphen inside a line', async () => {
|
|
281
|
+
assert.strictEqual(libqp.encode('a-b --c'), 'a-b --c');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await t.test('wrap() escapes a hyphen that a soft line break would move to the start of a line', async () => {
|
|
285
|
+
const input = 'a'.repeat(75) + '-x';
|
|
286
|
+
const wrapped = libqp.wrap(libqp.encode(input));
|
|
287
|
+
assert.strictEqual(wrapped, 'a'.repeat(75) + '=\r\n=2Dx');
|
|
288
|
+
assert.strictEqual(libqp.decode(wrapped).toString(), input);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await t.test('wrap() keeps every line within the line length when escaping', async () => {
|
|
292
|
+
const input = '-'.repeat(300);
|
|
293
|
+
const wrapped = libqp.wrap(libqp.encode(input));
|
|
294
|
+
const lines = wrapped.split('\r\n');
|
|
295
|
+
assert.ok(lines.length > 3);
|
|
296
|
+
for (const line of lines) {
|
|
297
|
+
assert.ok(line.length <= 76, `line too long: ${line.length}`);
|
|
298
|
+
assert.notStrictEqual(line.charAt(0), '-');
|
|
299
|
+
}
|
|
300
|
+
assert.strictEqual(libqp.decode(wrapped).toString(), input);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await t.test('Encoder escapes a hyphen at the start of a chunk only when it starts a line', async () => {
|
|
304
|
+
for (const options of [{}, { lineLength: false }]) {
|
|
305
|
+
assert.strictEqual(await encodeStream(['-a'], options), '=2Da');
|
|
306
|
+
assert.strictEqual(await encodeStream(['ab\n', '-c'], options), 'ab\n=2Dc');
|
|
307
|
+
assert.strictEqual(await encodeStream(['ab', '-c'], options), 'ab-c');
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await t.test('Encoder output never starts a line with a hyphen', async () => {
|
|
312
|
+
// a body that decodes to a boundary delimiter followed by the headers of a part
|
|
313
|
+
const input = 'text\r\n--boundary\r\nContent-Type: application/x-evil; name=evil.exe\r\n\r\n' + '-'.repeat(200) + '\r\n';
|
|
314
|
+
const chunks = [];
|
|
315
|
+
for (let i = 0; i < input.length; i += 7) {
|
|
316
|
+
chunks.push(Buffer.from(input.substring(i, i + 7)));
|
|
317
|
+
}
|
|
318
|
+
for (const options of [{}, { lineLength: false }]) {
|
|
319
|
+
const encoded = await encodeStream(chunks, options);
|
|
320
|
+
assert.ok(!/(^|\n)-/.test(encoded), `line starts with a hyphen: ${JSON.stringify(encoded)}`);
|
|
321
|
+
assert.strictEqual(libqp.decode(encoded).toString(), input);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('Trailing whitespace', async t => {
|
|
327
|
+
await t.test('decode() removes whitespace from the end of lines', async () => {
|
|
328
|
+
assert.strictEqual(libqp.decode('abc \r\ndef \t\nghi ').toString(), 'abc\r\ndef\nghi');
|
|
329
|
+
assert.strictEqual(libqp.decode(' \n ').toString(), '\n');
|
|
330
|
+
assert.strictEqual(libqp.decode('a b').toString(), 'a b');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await t.test('decode() treats a soft line break with trailing whitespace as a soft line break', async () => {
|
|
334
|
+
assert.strictEqual(libqp.decode('foo= \t\r\nbar').toString(), 'foobar');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await t.test('decode() handles a long run of whitespace in linear time', async () => {
|
|
338
|
+
// the trim used to be quadratic on a run that does not end the line, 50 KB took 2 seconds
|
|
339
|
+
for (const chr of [' ', '\t']) {
|
|
340
|
+
const input = chr.repeat(500 * 1024) + 'x';
|
|
341
|
+
const start = Date.now();
|
|
342
|
+
const decoded = libqp.decode(input).toString('binary');
|
|
343
|
+
const duration = Date.now() - start;
|
|
344
|
+
assert.strictEqual(decoded, input);
|
|
345
|
+
assert.ok(duration < 500, `decoding took ${duration} ms`);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('Escape sequences', async t => {
|
|
351
|
+
await t.test('decode() decodes valid escapes and keeps invalid ones', async () => {
|
|
352
|
+
assert.strictEqual(libqp.decode('=41=42=43').toString(), 'ABC');
|
|
353
|
+
assert.strictEqual(libqp.decode('=c3=a9').toString(), 'é');
|
|
354
|
+
assert.strictEqual(libqp.decode('=4G=4').toString(), '=4G=4');
|
|
355
|
+
assert.strictEqual(libqp.decode('==41').toString(), '=A');
|
|
356
|
+
assert.strictEqual(libqp.decode('a=\r\nb=\nc=').toString(), 'abc');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await t.test('decode() sizes the output for a body made of escapes', async () => {
|
|
360
|
+
const count = 1024 * 1024;
|
|
361
|
+
const decoded = libqp.decode('=41'.repeat(count));
|
|
362
|
+
assert.strictEqual(decoded.length, count);
|
|
363
|
+
assert.strictEqual(decoded[0], 0x41);
|
|
364
|
+
assert.strictEqual(decoded[count - 1], 0x41);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.9.40](https://github.com/nodemailer/mailparser/compare/v3.9.39...v3.9.40) (2026-10-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* bump @zone-eu/mailsplit to 5.4.23 ([32adf4f](https://github.com/nodemailer/mailparser/commit/32adf4f9b7d10a95f98359de7476ebb7fe526cf0))
|
|
9
|
+
* bump libmime to 5.4.9 and nodemailer to 10.1.2 ([a928892](https://github.com/nodemailer/mailparser/commit/a92889221c11eb81b06472f911672b71c288c4ef))
|
|
10
|
+
* keep the message when its HTML can not be converted to text, and never turn a decoded display name into an address ([0d234fd](https://github.com/nodemailer/mailparser/commit/0d234fd3ab1207a617e3682b27c72f4902d76936))
|
|
11
|
+
|
|
3
12
|
## [3.9.39](https://github.com/nodemailer/mailparser/compare/v3.9.38...v3.9.39) (2026-10-14)
|
|
4
13
|
|
|
5
14
|
|
|
@@ -28,7 +28,8 @@ See [mailparser homepage](https://nodemailer.com/extras/mailparser/) for documen
|
|
|
28
28
|
### Limits
|
|
29
29
|
|
|
30
30
|
- `simpleParser` replaces `cid:` image references in the HTML with `data:` URLs. The total length of the URLs written into the HTML is capped by the `maxInlinedImagesSize` option (default 20 MB, counted in characters of the inserted URLs). References past the cap stay as `cid:` links. The same cap applies to URLs returned from a custom `updateImageLinks()` callback.
|
|
31
|
-
- `simpleParser` calls its callback, or settles its promise, exactly once. When `MailParser` reports a non-fatal error
|
|
31
|
+
- `simpleParser` calls its callback, or settles its promise, exactly once. When `MailParser` reports a non-fatal error and then finishes parsing, that error is what the caller receives.
|
|
32
|
+
- HTML that can not be converted to text (longer than `maxHtmlLengthToParse`, nested deeper than 1000 levels, or rejected by html-to-text) does not fail the message. `text` is empty, `html` is kept, and the reason is set as `textConversionError` on the result (and on the `text` object `MailParser` emits).
|
|
32
33
|
|
|
33
34
|
### License
|
|
34
35
|
|