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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +16 -0
- package/README.md +2 -1
- package/SECURITY.md +120 -44
- package/lib/mail-parser.js +160 -65
- package/lib/simple-parser.js +3 -3
- package/package.json +5 -5
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
|
|
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
|
|
7
|
-
|
|
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
|
|
12
|
-
patches to older
|
|
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
|
|
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
|
-
##
|
|
32
|
+
## What Qualifies as a Security Vulnerability
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
39
|
+
In particular, explain:
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
63
|
+
Report privately through one of the following channels:
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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
|
|
71
|
-
text extraction. Of particular interest are inputs that cause
|
|
72
|
-
consumption, catastrophic regular-expression backtracking,
|
|
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.
|
package/lib/mail-parser.js
CHANGED
|
@@ -82,27 +82,121 @@ linkify.add('@', {
|
|
|
82
82
|
}
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
let
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/lib/simple-parser.js
CHANGED
|
@@ -19,8 +19,8 @@ module.exports = (input, options, callback) => {
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
// MailParser may emit a non-fatal 'error'
|
|
23
|
-
//
|
|
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.
|
|
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.
|
|
26
|
+
"libmime": "5.4.9",
|
|
26
27
|
"linkify-it": "5.0.2",
|
|
27
|
-
"
|
|
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.
|
|
44
|
+
"prettier": "3.9.10",
|
|
45
45
|
"random-message": "1.1.0"
|
|
46
46
|
},
|
|
47
47
|
"repository": {
|