mailparser 3.9.38 → 3.9.40

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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "3.9.38"
2
+ ".": "3.9.40"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
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
+
12
+ ## [3.9.39](https://github.com/nodemailer/mailparser/compare/v3.9.38...v3.9.39) (2026-10-14)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **deps:** update nodemailer to 10.1.1, libmime to 5.4.8 and mailsplit to 5.4.21 ([43ddf3e](https://github.com/nodemailer/mailparser/commit/43ddf3e9214b2a14b20319d1679ffe001a3a6889))
18
+
3
19
  ## [3.9.38](https://github.com/nodemailer/mailparser/compare/v3.9.37...v3.9.38) (2026-10-10)
4
20
 
5
21
 
package/README.md CHANGED
@@ -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 (for example HTML longer than `maxHtmlLengthToParse`), that error is what the caller receives. `MailParser` itself still emits the `'error'` event and then finishes parsing.
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
 
package/SECURITY.md CHANGED
@@ -3,21 +3,24 @@
3
3
  mailparser is an email parsing library for Node.js. It is routinely pointed at
4
4
  untrusted, adversarial, and malformed messages, so parser robustness is a
5
5
  security concern: a crafted message should never be able to crash the host
6
- process, exhaust memory or CPU, pollute prototypes, or leak data across parses.
7
- We take security reports seriously and aim to respond quickly.
6
+ process, exhaust memory or CPU through amplification, pollute prototypes, or
7
+ leak data across parses. We welcome responsible reports of vulnerabilities that
8
+ present a credible security risk to users. Please distinguish security
9
+ vulnerabilities from ordinary bugs, unusual edge cases, and hardening
10
+ suggestions.
8
11
 
9
12
  ## Supported Versions
10
13
 
11
- Security fixes are released only against the latest version. We do not backport
12
- patches to older releases - upgrading to the current 3.x release line is the
13
- supported way to receive security updates.
14
+ Security fixes are released only against the latest major version. We do not
15
+ backport patches to older majors, so upgrading to the current release line is
16
+ the supported way to receive security updates.
14
17
 
15
18
  | Version | Supported |
16
19
  | ------- | ------------------ |
17
20
  | 3.x | :white_check_mark: |
18
21
  | < 3.0 | :x: |
19
22
 
20
- If you are on an older version, please upgrade. See the release notes at
23
+ If you are on an older major, please upgrade. See the release notes at
21
24
  <https://github.com/nodemailer/mailparser/releases> before updating.
22
25
 
23
26
  > [!NOTE]
@@ -26,51 +29,126 @@ If you are on an older version, please upgrade. See the release notes at
26
29
  > [PostalMime](https://github.com/postalsys/postal-mime), which works in both
27
30
  > Node.js and browser environments.
28
31
 
29
- ## Reporting a Vulnerability
32
+ ## What Qualifies as a Security Vulnerability
30
33
 
31
- **Please do not report security vulnerabilities through public GitHub issues,
32
- pull requests, or discussions.**
34
+ A security report should identify a realistic way for an attacker to cross a
35
+ security boundary in a supported version of mailparser. Unexpected behavior, a
36
+ failing test, or a technically correct proof of concept is not sufficient on its
37
+ own to establish security impact.
33
38
 
34
- Report privately through one of the following channels:
39
+ In particular, explain:
35
40
 
36
- 1. **GitHub Security Advisories (preferred).** Open a private report at
37
- <https://github.com/nodemailer/mailparser/security/advisories/new>. This keeps
38
- the discussion private until a fix is published and lets us credit you.
39
- 2. **Email.** Send details to **andris@postalsys.com** (the contact listed in
40
- [`SECURITY.txt`](SECURITY.txt)). Encrypt sensitive details if possible - our
41
- PGP key fingerprint is `5D952A46E1D8C931F6364E01DC6C83F4D584D364`.
41
+ - **Attacker capabilities:** Who is the attacker, and what inputs, systems, or
42
+ responses can they control _without_ already having the privileges or secrets
43
+ they are trying to gain?
44
+ - **Attack path:** How does attacker-controlled input reach the affected code
45
+ through a supported public API or a realistic application integration?
46
+ - **Prerequisites:** What configuration and user actions are required? If the
47
+ attack depends on unusual options or deliberately malformed input, explain why
48
+ those conditions could arise in a real application.
49
+ - **Impact:** What confidentiality, integrity, or availability boundary is
50
+ crossed, and what can the attacker achieve as a result?
42
51
 
43
- When reporting, please include as much of the following as you can:
52
+ A vulnerability involving an optional or less common feature can still qualify
53
+ if these conditions are met. Conversely, the existence of a defensive check in
54
+ the code does not, by itself, establish that every bypass of that check has a
55
+ realistic security impact.
44
56
 
45
- - The affected version(s) and environment (mailparser version, Node.js version,
46
- OS).
47
- - A clear description of the issue and its impact (e.g. denial of service via
48
- CPU or memory exhaustion, ReDoS, prototype pollution, unhandled exception that
49
- crashes the process, information disclosure between parses).
50
- - A minimal proof of concept - ideally the raw `.eml` sample or a short script
51
- that reproduces the issue.
52
- - Any suggested remediation, if you have one.
57
+ ## Reporting a Vulnerability
53
58
 
54
- We are a small team, so there is no guaranteed response time - sometimes reports
55
- are handled within hours, sometimes they take longer. Accepted issues are fixed
56
- in a new release and coordinated through a GitHub Security Advisory, and
57
- reporters who wish to be named are credited.
59
+ **Do not disclose suspected exploitable vulnerabilities through public GitHub
60
+ issues, pull requests, or discussions before coordinating with the
61
+ maintainers.**
58
62
 
59
- ## CVEs
63
+ Report privately through one of the following channels:
60
64
 
61
- We track and disclose vulnerabilities through GitHub Security Advisories. We do
62
- not request or manage CVE identifiers ourselves. If you need a CVE assigned for a
63
- reported issue, please request one yourself - for example, through GitHub's own
64
- CVE request flow on the published advisory, or another CNA.
65
+ 1. **GitHub Security Advisories (preferred):**
66
+ <https://github.com/nodemailer/mailparser/security/advisories/new>.
67
+ 2. **Email:** **andris@postalsys.com**, as listed in
68
+ [`SECURITY.txt`](SECURITY.txt). Encrypt sensitive details if possible.
69
+
70
+ ### Required Information
71
+
72
+ Please provide enough information to evaluate both the defect and its security
73
+ impact. Reports should include:
74
+
75
+ 1. The affected mailparser version or commit, Node.js version, and relevant
76
+ configuration.
77
+ 2. A concise description of the issue and the affected public API or code path
78
+ (`MailParser` or `simpleParser`. The raw `.eml` sample that triggers the
79
+ issue is the most useful reproduction).
80
+ 3. A minimal, tested reproduction, including steps to run it, expected behavior,
81
+ and observed behavior.
82
+ 4. A concrete threat model: attacker control, prerequisites, trust boundary, and
83
+ resulting impact. Clearly distinguish what your reproduction proves from what
84
+ you believe might be possible.
85
+ 5. An explanation of why the issue affects real deployments beyond a
86
+ deliberately constructed test setup, where this is not obvious.
87
+
88
+ A suggested fix is welcome but does not replace an explanation of
89
+ exploitability. A complete real-world exploit is not always necessary, but the
90
+ report must make a credible security impact clear.
91
+
92
+ ### Reports That May Be Treated as Ordinary Bugs
93
+
94
+ The following generally do **not** qualify for private security handling unless
95
+ you can demonstrate an additional, meaningful security boundary crossing:
96
+
97
+ - Issues that require the attacker to control trusted application code,
98
+ administrator-only configuration, or the secret they supposedly extract.
99
+ - Problems caused solely by application-level misuse, exposed credentials, or
100
+ insecure deployment choices outside mailparser's control.
101
+ - Contrived proof-of-concept setups where the tester controls both sides of a
102
+ request, without explaining how a separate attacker could trigger the issue.
103
+ - Parser quirks, unexpected behavior, and bypasses of internal checks with no
104
+ demonstrated security impact.
105
+ - Speculative findings based only on static analysis or an automated tool's
106
+ assessment, without a verified reproduction and credible attack scenario.
107
+
108
+ These may still be valid bugs and may be considered through the normal issue
109
+ tracker. Please coordinate before making any potentially exploitable details
110
+ public. Classification as a bug does not guarantee a fix or a particular
111
+ priority.
112
+
113
+ ### Automated and AI-Assisted Reports
114
+
115
+ Automated analysis and AI tools are welcome as research aids, but the person
116
+ submitting a report is responsible for verifying its claims. **Do not submit
117
+ bulk, unverified, or speculative AI-generated findings.** Test each finding
118
+ against a supported release and establish the actual security impact. Reports
119
+ that do not meet the requirements above may be closed without further
120
+ investigation or individual feedback; they can be resubmitted with stronger
121
+ evidence.
122
+
123
+ ## Triage and Disclosure
124
+
125
+ mailparser is maintained by a small team, and there is **no guaranteed response
126
+ or resolution time**. Reports are prioritized based on demonstrated impact,
127
+ exploitability, affected usage, and available maintenance resources.
128
+
129
+ A private report does not automatically become a security advisory. Confirmed
130
+ vulnerabilities may be fixed and disclosed through GitHub Security Advisories;
131
+ other findings may be closed, deferred, or handled as ordinary bugs. Please do
132
+ not disclose details of an unresolved, potentially exploitable issue publicly
133
+ without coordinating with the maintainers.
134
+
135
+ ## CVEs and Credit
136
+
137
+ We do not request or manage CVE identifiers. A report being acknowledged, a bug
138
+ being fixed, or a pull request being merged does **not** mean that mailparser
139
+ has confirmed a security vulnerability or endorsed a CVE assignment. We do not
140
+ guarantee advisories, CVE identifiers, or public credit for reports. For
141
+ confirmed vulnerabilities disclosed through an advisory, substantive reporters
142
+ may be credited if they wish.
65
143
 
66
144
  ## Scope
67
145
 
68
- In scope: the mailparser library source in this repository - the streaming MIME
146
+ In scope: the mailparser library source in this repository: the streaming MIME
69
147
  parser (`MailParser`), the convenience parser (`simpleParser`), header and
70
- address decoding, charset/transfer-encoding handling, and attachment and HTML/
71
- text extraction. Of particular interest are inputs that cause unbounded resource
72
- consumption, catastrophic regular-expression backtracking, prototype pollution,
73
- or process crashes.
148
+ address decoding, charset and transfer-encoding handling, and attachment and
149
+ HTML/text extraction. Of particular interest are small crafted inputs that cause
150
+ amplified resource consumption, catastrophic regular-expression backtracking,
151
+ prototype pollution, or process crashes.
74
152
 
75
153
  Out of scope:
76
154
 
@@ -78,13 +156,11 @@ Out of scope:
78
156
  Parsed email content is attacker-controlled data; you must sanitize and escape
79
157
  it before rendering, storing, or executing it. In particular, the `html` field
80
158
  is not sanitized for safe display in a browser.
81
- - Vulnerabilities in third-party dependencies that are already tracked and fixed
82
- upstream - report those to the respective project (we will still bump the
83
- dependency once a fix is available).
84
- - Issues that require an already-compromised host or a malicious local
85
- environment.
86
159
  - Resource consumption that is proportional to a legitimately large input (for
87
160
  example, parsing a genuine 100MB message uses memory) rather than amplified by
88
161
  a crafted small input.
162
+ - Vulnerabilities in third-party dependencies that are already tracked upstream
163
+ (we will still bump the dependency once a fix is available).
164
+ - Social-engineering reports.
89
165
 
90
166
  Thank you for helping keep mailparser and its users safe.
@@ -82,27 +82,121 @@ linkify.add('@', {
82
82
  }
83
83
  });
84
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;
85
+ // Lists the parameters a header value gives in RFC 2231 form (name*=, name*0= or
86
+ // name*0*=). libmime decodes those while parsing the header, so the result must not go
87
+ // through decodeWords as well: filename*=utf-8''a%3D%3Futf-8%3FQ%3F%3D2Eexe%3F%3D would
88
+ // otherwise come out as "a.exe" while a parser that decodes once sees the encoded word.
89
+ function rfc2231ParamNames(value) {
90
+ let names = new Set();
91
+ let quote = false;
92
+ let escaped = false;
93
+ let start = 0;
94
+
95
+ let addSegment = end => {
96
+ let segment = value.substring(start, end);
97
+ let eq = segment.indexOf('=');
98
+ if (eq < 0) {
99
+ return;
100
+ }
101
+ let match = segment
102
+ .substring(0, eq)
103
+ .trim()
104
+ .toLowerCase()
105
+ .match(/^(.+?)\*(\d+\*?)?$/);
106
+ if (match) {
107
+ names.add(match[1]);
108
+ }
109
+ };
110
+
111
+ for (let i = 0; i < value.length; i++) {
112
+ let chr = value.charAt(i);
113
+ if (escaped) {
114
+ escaped = false;
115
+ } else if (chr === '\\') {
116
+ escaped = true;
117
+ } else if (chr === '"') {
118
+ quote = !quote;
119
+ } else if (chr === ';' && !quote) {
120
+ addSegment(i);
121
+ start = i + 1;
122
+ }
123
+ }
124
+ addSegment(value.length);
125
+
126
+ return names;
127
+ }
128
+
129
+ // Deepest element nesting html-to-text is asked to convert. htmlparser2 keeps its open
130
+ // element stack at the front of an array, so parsing alone is quadratic in the nesting
131
+ // depth (200k nested <b> took 9 s), and html-to-text's recursive walk overflows the call
132
+ // stack at about 2000 levels. Real markup stays far below this: Chrome's own parser
133
+ // flattens anything nested deeper than 512 levels.
134
+ const MAX_HTML_NESTING_DEPTH = 1000;
135
+
136
+ // Elements that never hold content, so a start tag does not open a level
137
+ const HTML_VOID_ELEMENTS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
138
+
139
+ // Elements the HTML parser closes implicitly when a sibling opens (li after li, td after
140
+ // td, p after p), so an unclosed run of them does not nest. They are left out of the depth
141
+ // count: a report with thousands of unclosed table cells is sloppy but common markup
142
+ const HTML_IMPLIED_END_ELEMENTS = new Set(['p', 'li', 'dt', 'dd', 'option', 'optgroup', 'tr', 'td', 'th', 'thead', 'tbody', 'tfoot', 'rt', 'rp']);
143
+
144
+ function isTagNameChar(code) {
145
+ return (
146
+ (code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a) || (code >= 0x30 && code <= 0x39) || code === 0x2d || code === 0x3a || code === 0x5f
147
+ );
148
+ }
149
+
150
+ // Reports whether `html` nests elements deeper than `maxDepth`, in one linear pass that
151
+ // follows what htmlparser2 builds: a start tag opens an element (a trailing "/" does not
152
+ // close it in HTML mode), an end tag closes down to the nearest open element of that name
153
+ // and is ignored otherwise, comments are skipped. Returns as soon as the limit is crossed.
154
+ function exceedsHtmlNestingDepth(html, maxDepth) {
155
+ let stack = [];
156
+ let open = new Map(); // element name -> how many of them are on the stack
157
+ let len = html.length;
158
+ let i = html.indexOf('<');
159
+ while (i >= 0) {
160
+ let pos = i + 1;
161
+ if (html.startsWith('!--', pos)) {
162
+ let end = html.indexOf('-->', pos + 3);
163
+ i = end < 0 ? -1 : html.indexOf('<', end + 3);
164
+ continue;
165
+ }
166
+ let closing = html.charCodeAt(pos) === 0x2f; // "/"
167
+ if (closing) {
168
+ pos++;
169
+ }
170
+ let nameStart = pos;
171
+ while (pos < len && isTagNameChar(html.charCodeAt(pos))) {
172
+ pos++;
173
+ }
174
+ if (pos === nameStart) {
175
+ // not a tag, "<" is text here
176
+ i = html.indexOf('<', pos);
177
+ continue;
178
+ }
179
+ let name = html.slice(nameStart, pos).toLowerCase();
180
+ let end = html.indexOf('>', pos);
181
+ if (end < 0) {
182
+ break;
183
+ }
184
+ if (!HTML_VOID_ELEMENTS.has(name) && !HTML_IMPLIED_END_ELEMENTS.has(name)) {
185
+ if (!closing) {
186
+ stack.push(name);
187
+ open.set(name, (open.get(name) || 0) + 1);
188
+ if (stack.length > maxDepth) {
189
+ return true;
190
+ }
191
+ } else if (open.get(name)) {
192
+ let popped;
193
+ do {
194
+ popped = stack.pop();
195
+ open.set(popped, open.get(popped) - 1);
196
+ } while (popped !== name);
100
197
  }
101
- start = -1;
102
- at = -1;
103
- } else if (chr === '@' && start >= 0 && at < 0 && i > start + 1) {
104
- at = i;
105
198
  }
199
+ i = html.indexOf('<', end + 1);
106
200
  }
107
201
  return false;
108
202
  }
@@ -379,12 +473,17 @@ class MailParser extends Transform {
379
473
  switch (key) {
380
474
  case 'content-type':
381
475
  case 'content-disposition':
382
- case 'dkim-signature':
476
+ case 'dkim-signature': {
477
+ // a parameter given in RFC 2231 form is decoded by parseHeaderValue already
478
+ let rfc2231 = rfc2231ParamNames(value);
383
479
  value = this.libmime.parseHeaderValue(value);
384
480
  if (value.value) {
385
481
  value.value = this.libmime.decodeWords(value.value);
386
482
  }
387
483
  Object.keys((value && value.params) || {}).forEach(key => {
484
+ if (rfc2231.has(key)) {
485
+ return;
486
+ }
388
487
  try {
389
488
  value.params[key] = this.libmime.decodeWords(value.params[key]);
390
489
  } catch (E) {
@@ -392,6 +491,7 @@ class MailParser extends Transform {
392
491
  }
393
492
  });
394
493
  break;
494
+ }
395
495
  case 'date': {
396
496
  let dateValue = new Date(value);
397
497
  if (isNaN(dateValue)) {
@@ -464,7 +564,12 @@ class MailParser extends Transform {
464
564
  if (!headers.has(key)) {
465
565
  headers.set(key, [].concat(value || []));
466
566
  } else if (Array.isArray(value)) {
467
- headers.set(key, headers.get(key).concat(value));
567
+ // appended in place: rebuilding the list with concat for every repeated
568
+ // header was quadratic in the number of References lines
569
+ let list = headers.get(key);
570
+ for (let entry of value) {
571
+ list.push(entry);
572
+ }
468
573
  } else {
469
574
  headers.get(key).push(value);
470
575
  }
@@ -495,7 +600,9 @@ class MailParser extends Transform {
495
600
  headers.forEach((value, key) => {
496
601
  if (Array.isArray(value)) {
497
602
  if (singleKeys.includes(key) && value.length) {
498
- headers.set(key, value[value.length - 1]);
603
+ // the first occurrence: the one mailsplit built the MIME structure from,
604
+ // and the one mail clients display when the header is duplicated
605
+ headers.set(key, value[0]);
499
606
  } else if (value.length === 1) {
500
607
  headers.set(key, value[0]);
501
608
  }
@@ -593,38 +700,14 @@ class MailParser extends Transform {
593
700
  }
594
701
 
595
702
  decodeAddresses(addresses) {
596
- let processedAddress = new WeakSet();
597
703
  for (let i = 0; i < addresses.length; i++) {
598
704
  let address = addresses[i];
599
705
  address.name = (address.name || '').toString().trim();
600
706
 
601
- if (!address.address && /^(=\?([^?]+)\?[Bb]\?[^?]*\?=)(\s*=\?([^?]+)\?[Bb]\?[^?]*\?=)*$/.test(address.name) && !processedAddress.has(address)) {
602
- let decoded = this.libmime.decodeWords(address.name);
603
- // Security fix: only re-parse if decoded text contains angle-bracket-delimited
604
- // addresses (e.g. "Name <user@domain>"). Bare decoded text like "attacker@evil.com"
605
- // must not be re-interpreted as an address -- RFC 2047 Section 5 prohibits
606
- // encoded-words in addr-spec, and re-parsing fabricates addresses not present
607
- // in the original header.
608
- if (hasBracketedAddress(decoded)) {
609
- let parsed = addressparser(decoded);
610
- if (parsed.length) {
611
- parsed.forEach(entry => {
612
- processedAddress.add(entry);
613
- addresses.push(entry);
614
- });
615
- }
616
-
617
- // remove current element
618
- addresses.splice(i, 1);
619
- i--;
620
- continue;
621
- } else {
622
- // Treat decoded content as display name only
623
- address.name = decoded;
624
- continue;
625
- }
626
- }
627
-
707
+ // An encoded word decodes into the display name and nothing else, whatever it
708
+ // decodes to. RFC 2047 section 5 keeps encoded words out of the addr-spec, so a
709
+ // name that decodes to "CEO" <ceo@bank.com> is still only a name: parsing it
710
+ // again would report an address the header never carried
628
711
  if (address.name) {
629
712
  try {
630
713
  address.name = this.libmime.decodeWords(address.name);
@@ -762,6 +845,7 @@ class MailParser extends Transform {
762
845
  getTextContent() {
763
846
  let text = [];
764
847
  let html = [];
848
+ let textConversionError = null;
765
849
  let processNode = (alternative, level, node) => {
766
850
  if (node.showMeta) {
767
851
  let meta = ['From', 'Subject', 'Date', 'To', 'Cc', 'Bcc']
@@ -845,27 +929,34 @@ class MailParser extends Transform {
845
929
  html.push(this.textToHtml(node.textContent));
846
930
  }
847
931
  } else if (node.contentType === 'text/html') {
848
- let failedToParseHtml = false;
849
932
  if ((!alternative && this.hasText) || (node.root && !this.hasText)) {
850
933
  if (this.options.skipHtmlToText) {
851
934
  text.push('');
852
- } else if (node.textContent.length > this.options.maxHtmlLengthToParse) {
853
- this.emit('error', new Error(`HTML too long for parsing ${node.textContent.length} bytes`));
854
- text.push('Invalid HTML content (too long)');
855
- failedToParseHtml = true;
856
935
  } else {
857
- try {
858
- text.push(htmlToText(node.textContent));
859
- } catch (err) {
860
- this.emit('error', new Error('Failed to parse HTML'));
861
- text.push('Invalid HTML content');
862
- failedToParseHtml = true;
936
+ // HTML that can not be converted is no reason to lose the message:
937
+ // the text stays empty, the HTML is kept and the reason travels with
938
+ // the result as textConversionError. An 'error' event here used to
939
+ // reject the whole simpleParser() promise, headers and attachments
940
+ // included, after burning the CPU the conversion attempt took
941
+ let err = null;
942
+ if (node.textContent.length > this.options.maxHtmlLengthToParse) {
943
+ err = new Error(`HTML too long for parsing ${node.textContent.length} bytes`);
944
+ } else if (exceedsHtmlNestingDepth(node.textContent, MAX_HTML_NESTING_DEPTH)) {
945
+ err = new Error(`HTML nested deeper than ${MAX_HTML_NESTING_DEPTH} levels`);
946
+ } else {
947
+ try {
948
+ text.push(htmlToText(node.textContent, { limits: { maxDepth: MAX_HTML_NESTING_DEPTH } }));
949
+ } catch (cause) {
950
+ err = new Error('Failed to parse HTML', { cause });
951
+ }
952
+ }
953
+ if (err) {
954
+ text.push('');
955
+ textConversionError = textConversionError || err;
863
956
  }
864
957
  }
865
958
  }
866
- if (!failedToParseHtml) {
867
- html.push(node.textContent);
868
- }
959
+ html.push(node.textContent);
869
960
  }
870
961
  }
871
962
  alternative = alternative || node.contentType === 'multipart/alternative';
@@ -888,6 +979,9 @@ class MailParser extends Transform {
888
979
  this.text = response.text = text.join('\n');
889
980
  this.textAsHtml = response.textAsHtml = text.map(part => this.textToHtml(part)).join('<br/>\n');
890
981
  }
982
+ if (textConversionError) {
983
+ response.textConversionError = textConversionError;
984
+ }
891
985
  return response;
892
986
  }
893
987
 
@@ -1147,7 +1241,8 @@ class MailParser extends Transform {
1147
1241
  .map(address => {
1148
1242
  let str = '';
1149
1243
  if (address.name) {
1150
- str += `"${address.name}"` + (address.group ? ': ' : '');
1244
+ // a quoted-string, so a quote or backslash inside the name is escaped
1245
+ str += `"${address.name.replace(/["\\]/g, '\\$&')}"` + (address.group ? ': ' : '');
1151
1246
  }
1152
1247
  if (address.address) {
1153
1248
  let link = address.address;
@@ -19,8 +19,8 @@ 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
22
+ // MailParser may emit a non-fatal 'error' and still reach 'end', so settle only once:
23
+ // the first error or the result wins
24
24
  let settled = false;
25
25
  let settle = callback;
26
26
  callback = (...args) => {
@@ -62,7 +62,7 @@ module.exports = (input, options, callback) => {
62
62
 
63
63
  if (data.type === 'text') {
64
64
  Object.keys(data).forEach(key => {
65
- if (['text', 'html', 'textAsHtml'].includes(key)) {
65
+ if (['text', 'html', 'textAsHtml', 'textConversionError'].includes(key)) {
66
66
  mail[key] = data[key];
67
67
  }
68
68
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailparser",
3
- "version": "3.9.38",
3
+ "version": "3.9.40",
4
4
  "description": "Parse e-mails",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -18,14 +18,14 @@
18
18
  ],
19
19
  "license": "MIT",
20
20
  "dependencies": {
21
+ "@zone-eu/mailsplit": "5.4.23",
21
22
  "encoding-japanese": "2.4.0",
22
23
  "he": "1.2.0",
23
24
  "html-to-text": "10.0.1",
24
25
  "iconv-lite": "0.7.3",
25
- "libmime": "5.4.7",
26
+ "libmime": "5.4.9",
26
27
  "linkify-it": "5.0.2",
27
- "@zone-eu/mailsplit": "5.4.20",
28
- "nodemailer": "10.1.0",
28
+ "nodemailer": "10.1.2",
29
29
  "punycode.js": "2.3.1",
30
30
  "tlds": "1.261.0"
31
31
  },
@@ -41,7 +41,7 @@
41
41
  "grunt-contrib-nodeunit": "5.0.0",
42
42
  "grunt-eslint": "27.0.0",
43
43
  "iconv": "3.0.1",
44
- "prettier": "3.9.9",
44
+ "prettier": "3.9.10",
45
45
  "random-message": "1.1.0"
46
46
  },
47
47
  "repository": {