mailparser 3.9.29 → 3.9.30
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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +5 -0
- package/lib/mail-parser.js +98 -31
- package/lib/simple-parser.js +12 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.9.30](https://github.com/nodemailer/mailparser/compare/v3.9.29...v3.9.30) (2026-09-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* cap inlined cid images, report late splitter errors, settle once, honour attachment backpressure, derive partId from the part number ([af3fe56](https://github.com/nodemailer/mailparser/commit/af3fe56719ae6bf5d867f22faf3bf81942659bdd))
|
|
9
|
+
|
|
3
10
|
## [3.9.29](https://github.com/nodemailer/mailparser/compare/v3.9.28...v3.9.29) (2026-09-27)
|
|
4
11
|
|
|
5
12
|
|
package/README.md
CHANGED
|
@@ -25,6 +25,11 @@ const mailparser = require('mailparser');
|
|
|
25
25
|
|
|
26
26
|
See [mailparser homepage](https://nodemailer.com/extras/mailparser/) for documentation and terms.
|
|
27
27
|
|
|
28
|
+
### Limits
|
|
29
|
+
|
|
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 (for example HTML longer than `maxHtmlLengthToParse`), that error is what the caller receives. `MailParser` itself still emits the `'error'` event and then finishes parsing.
|
|
32
|
+
|
|
28
33
|
### License
|
|
29
34
|
|
|
30
35
|
Licensed under MIT
|
package/lib/mail-parser.js
CHANGED
|
@@ -34,6 +34,16 @@ const MAX_LINKIFY_SEGMENT_LENGTH = 4096;
|
|
|
34
34
|
const MAX_LINKIFY_HOST_LABELS = 6;
|
|
35
35
|
const MAX_LINKIFY_WORK = 768 * 1024 * 1024; // shared by all text parts of a message
|
|
36
36
|
|
|
37
|
+
// Default upper bound for the total length of the URLs updateImageLinks() writes
|
|
38
|
+
// into the HTML (options.maxInlinedImagesSize). simpleParser inlines every cid:
|
|
39
|
+
// reference as a full data: URL, so a small message repeating a reference to
|
|
40
|
+
// one image thousands of times would otherwise grow the HTML past the maximum
|
|
41
|
+
// string length. References beyond the budget are left as cid: links.
|
|
42
|
+
const MAX_INLINED_IMAGES_SIZE = 20 * 1024 * 1024;
|
|
43
|
+
|
|
44
|
+
// List-* header names that must not become keys of the plain list object
|
|
45
|
+
const UNSAFE_LIST_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
46
|
+
|
|
37
47
|
linkify
|
|
38
48
|
.tlds(tlds) // Reload with full tlds list
|
|
39
49
|
.tlds('onion', true) // Add unofficial `.onion` domain
|
|
@@ -72,6 +82,31 @@ linkify.add('@', {
|
|
|
72
82
|
}
|
|
73
83
|
});
|
|
74
84
|
|
|
85
|
+
// Linear equivalent of /<[^<>]+@[^<>]+>/.test(str). The regex backtracks
|
|
86
|
+
// quadratically on a long run of @ characters after a lone <, and the input is a
|
|
87
|
+
// decoded encoded-word bounded only by the header size limit.
|
|
88
|
+
function hasBracketedAddress(str) {
|
|
89
|
+
let start = -1;
|
|
90
|
+
// first @ after the current < that has at least one character before it
|
|
91
|
+
let at = -1;
|
|
92
|
+
for (let i = 0, len = str.length; i < len; i++) {
|
|
93
|
+
let chr = str.charAt(i);
|
|
94
|
+
if (chr === '<') {
|
|
95
|
+
start = i;
|
|
96
|
+
at = -1;
|
|
97
|
+
} else if (chr === '>') {
|
|
98
|
+
if (start >= 0 && at >= 0 && at < i - 1) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
start = -1;
|
|
102
|
+
at = -1;
|
|
103
|
+
} else if (chr === '@' && start >= 0 && at < 0 && i > start + 1) {
|
|
104
|
+
at = i;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
75
110
|
class IconvDecoder extends Transform {
|
|
76
111
|
constructor(Iconv, charset) {
|
|
77
112
|
super();
|
|
@@ -185,8 +220,6 @@ class MailParser extends Transform {
|
|
|
185
220
|
|
|
186
221
|
this.attachmentList = [];
|
|
187
222
|
|
|
188
|
-
this.boundaries = [];
|
|
189
|
-
|
|
190
223
|
this.textTypes = ['text/plain', 'text/html'].concat(!this.options.keepDeliveryStatus ? 'message/delivery-status' : []);
|
|
191
224
|
|
|
192
225
|
this.decoder = this.getDecoder();
|
|
@@ -236,7 +269,7 @@ class MailParser extends Transform {
|
|
|
236
269
|
decodeStream(charset) {
|
|
237
270
|
// resolve charset aliases (eg. iso-8859-8-i -> ISO-8859-8) via libmime's canonical table
|
|
238
271
|
charset = libmime.normalizeCharset((charset || 'ascii').toString());
|
|
239
|
-
if (/^jis|^iso-?2022-?jp
|
|
272
|
+
if (/^jis|^iso-?2022-?jp/i.test(charset)) {
|
|
240
273
|
// special case not supported by iconv-lite
|
|
241
274
|
return new JPDecoder(charset);
|
|
242
275
|
}
|
|
@@ -302,7 +335,12 @@ class MailParser extends Transform {
|
|
|
302
335
|
if (this.finished) {
|
|
303
336
|
return this.cleanup(done);
|
|
304
337
|
}
|
|
305
|
-
this.waitingEnd =
|
|
338
|
+
this.waitingEnd = err => {
|
|
339
|
+
// runs once, either on a splitter error after end of input or when the splitter ends
|
|
340
|
+
this.waitingEnd = false;
|
|
341
|
+
if (err) {
|
|
342
|
+
return done(err);
|
|
343
|
+
}
|
|
306
344
|
this.cleanup(done);
|
|
307
345
|
};
|
|
308
346
|
}
|
|
@@ -479,6 +517,10 @@ class MailParser extends Transform {
|
|
|
479
517
|
}
|
|
480
518
|
|
|
481
519
|
parseListHeader(key, value) {
|
|
520
|
+
if (UNSAFE_LIST_KEYS.has(key)) {
|
|
521
|
+
// would replace the prototype of (or shadow) the normalized list object
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
482
524
|
let addresses = addressparser(value);
|
|
483
525
|
let response = {};
|
|
484
526
|
let data = addresses
|
|
@@ -563,7 +605,7 @@ class MailParser extends Transform {
|
|
|
563
605
|
// must not be re-interpreted as an address -- RFC 2047 Section 5 prohibits
|
|
564
606
|
// encoded-words in addr-spec, and re-parsing fabricates addresses not present
|
|
565
607
|
// in the original header.
|
|
566
|
-
if (
|
|
608
|
+
if (hasBracketedAddress(decoded)) {
|
|
567
609
|
let parsed = addressparser(decoded);
|
|
568
610
|
if (parsed.length) {
|
|
569
611
|
parsed.forEach(entry => {
|
|
@@ -852,7 +894,8 @@ class MailParser extends Transform {
|
|
|
852
894
|
processChunk(data, done) {
|
|
853
895
|
let partId = null;
|
|
854
896
|
if (data._parentBoundary) {
|
|
855
|
-
|
|
897
|
+
// mailsplit numbers parts IMAP style from the actual tree, TEXT markers are not part of the id
|
|
898
|
+
partId = (data.partNr || []).filter(nr => typeof nr === 'number').join('.') || null;
|
|
856
899
|
}
|
|
857
900
|
switch (data.type) {
|
|
858
901
|
case 'node': {
|
|
@@ -884,6 +927,11 @@ class MailParser extends Transform {
|
|
|
884
927
|
contentType = this.libmime.detectMimeType(data.filename) || 'application/octet-stream';
|
|
885
928
|
}
|
|
886
929
|
|
|
930
|
+
// backpressure state for the attachment stream, see pump() below
|
|
931
|
+
let released = false;
|
|
932
|
+
let waitingDrain = false;
|
|
933
|
+
let pump = null;
|
|
934
|
+
|
|
887
935
|
let attachment = {
|
|
888
936
|
type: 'attachment',
|
|
889
937
|
content: null,
|
|
@@ -891,6 +939,11 @@ class MailParser extends Transform {
|
|
|
891
939
|
partId,
|
|
892
940
|
release: () => {
|
|
893
941
|
attachment.release = null;
|
|
942
|
+
released = true;
|
|
943
|
+
if (waitingDrain) {
|
|
944
|
+
waitingDrain = false;
|
|
945
|
+
pump();
|
|
946
|
+
}
|
|
894
947
|
if (this.waitUntilAttachmentEnd && typeof this.attachmentCallback === 'function') {
|
|
895
948
|
setImmediate(this.attachmentCallback);
|
|
896
949
|
}
|
|
@@ -905,11 +958,25 @@ class MailParser extends Transform {
|
|
|
905
958
|
hasher.emit('error', err);
|
|
906
959
|
});
|
|
907
960
|
|
|
908
|
-
|
|
961
|
+
// Honour the consumer's backpressure while it reads the attachment. Once the
|
|
962
|
+
// attachment is released it is drained regardless, so that a caller that
|
|
963
|
+
// releases without reading the content does not stall the parser.
|
|
964
|
+
pump = () => {
|
|
909
965
|
let chunk;
|
|
910
|
-
|
|
911
966
|
while ((chunk = node.decoder.read()) !== null) {
|
|
912
|
-
hasher.write(chunk)
|
|
967
|
+
if (hasher.write(chunk) === false && !released) {
|
|
968
|
+
waitingDrain = true;
|
|
969
|
+
return hasher.once('drain', () => {
|
|
970
|
+
waitingDrain = false;
|
|
971
|
+
pump();
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
node.decoder.on('readable', () => {
|
|
978
|
+
if (!waitingDrain) {
|
|
979
|
+
pump();
|
|
913
980
|
}
|
|
914
981
|
});
|
|
915
982
|
|
|
@@ -1041,22 +1108,6 @@ class MailParser extends Transform {
|
|
|
1041
1108
|
setImmediate(done);
|
|
1042
1109
|
}
|
|
1043
1110
|
|
|
1044
|
-
_getPartId(parentBoundary) {
|
|
1045
|
-
let boundaryIndex = this.boundaries.findIndex(item => item.name === parentBoundary);
|
|
1046
|
-
if (boundaryIndex === -1) {
|
|
1047
|
-
this.boundaries.push({ name: parentBoundary, count: 1 });
|
|
1048
|
-
boundaryIndex = this.boundaries.length - 1;
|
|
1049
|
-
} else {
|
|
1050
|
-
this.boundaries[boundaryIndex].count++;
|
|
1051
|
-
}
|
|
1052
|
-
let partId = '1';
|
|
1053
|
-
for (let i = 0; i <= boundaryIndex; i++) {
|
|
1054
|
-
if (i === 0) partId = this.boundaries[i].count.toString();
|
|
1055
|
-
else partId += '.' + this.boundaries[i].count.toString();
|
|
1056
|
-
}
|
|
1057
|
-
return partId;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
1111
|
getAddressesHTML(value) {
|
|
1061
1112
|
let formatSingleLevel = addresses =>
|
|
1062
1113
|
addresses
|
|
@@ -1139,12 +1190,28 @@ class MailParser extends Transform {
|
|
|
1139
1190
|
let pos = 0;
|
|
1140
1191
|
let processNext = () => {
|
|
1141
1192
|
if (pos >= cidList.length) {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1193
|
+
let maxSize = this.options.maxInlinedImagesSize;
|
|
1194
|
+
if (typeof maxSize !== 'number' || !(maxSize >= 0)) {
|
|
1195
|
+
maxSize = MAX_INLINED_IMAGES_SIZE;
|
|
1196
|
+
}
|
|
1197
|
+
let inlinedSize = 0;
|
|
1198
|
+
try {
|
|
1199
|
+
html = html.replace(/\bcid:([^'"\s]{1,256})/g, (match, cid) => {
|
|
1200
|
+
let url = cids.has(cid) && cids.get(cid).url;
|
|
1201
|
+
if (!url) {
|
|
1202
|
+
return match;
|
|
1203
|
+
}
|
|
1204
|
+
url = String(url);
|
|
1205
|
+
if (inlinedSize + url.length > maxSize) {
|
|
1206
|
+
return match;
|
|
1207
|
+
}
|
|
1208
|
+
inlinedSize += url.length;
|
|
1209
|
+
return url;
|
|
1210
|
+
});
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
// runs from setImmediate, a throw here would be an uncaught exception
|
|
1213
|
+
return done(err);
|
|
1214
|
+
}
|
|
1148
1215
|
|
|
1149
1216
|
return done(null, html);
|
|
1150
1217
|
}
|
package/lib/simple-parser.js
CHANGED
|
@@ -19,6 +19,18 @@ module.exports = (input, options, callback) => {
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// MailParser may emit a non-fatal 'error' (eg. HTML too long to convert to text) and
|
|
23
|
+
// still reach 'end', so settle only once: the first error or the result wins
|
|
24
|
+
let settled = false;
|
|
25
|
+
let settle = callback;
|
|
26
|
+
callback = (...args) => {
|
|
27
|
+
if (settled) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
settled = true;
|
|
31
|
+
settle(...args);
|
|
32
|
+
};
|
|
33
|
+
|
|
22
34
|
options = options || {};
|
|
23
35
|
let keepCidLinks = !!options.keepCidLinks;
|
|
24
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mailparser",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.30",
|
|
4
4
|
"description": "Parse e-mails",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"he": "1.2.0",
|
|
23
23
|
"html-to-text": "10.0.1",
|
|
24
24
|
"iconv-lite": "0.7.3",
|
|
25
|
-
"libmime": "5.4.
|
|
25
|
+
"libmime": "5.4.5",
|
|
26
26
|
"linkify-it": "5.0.2",
|
|
27
|
-
"@zone-eu/mailsplit": "5.4.
|
|
28
|
-
"nodemailer": "10.0.
|
|
27
|
+
"@zone-eu/mailsplit": "5.4.18",
|
|
28
|
+
"nodemailer": "10.0.12",
|
|
29
29
|
"punycode.js": "2.3.1",
|
|
30
30
|
"tlds": "1.261.0"
|
|
31
31
|
},
|