@mavogel/awscdk-rootmail 0.1.22 → 0.1.24
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 +4 -4
- 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
package/.jsii
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
"@aws-sdk/client-ses": "^3.1152.0",
|
|
15
15
|
"@aws-sdk/client-ssm": "^3.1152.0",
|
|
16
16
|
"cdk-nag": "^3.0.1",
|
|
17
|
-
"mailparser": "^3.9.
|
|
17
|
+
"mailparser": "^3.9.40",
|
|
18
18
|
"uuid": "^14.0.3"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@mavogel/mvc-projen": "^0.0.
|
|
21
|
+
"@mavogel/mvc-projen": "^0.0.61",
|
|
22
22
|
"aws-cdk-lib": "^2.263.0",
|
|
23
23
|
"constructs": "^10.5.1"
|
|
24
24
|
},
|
|
@@ -9620,6 +9620,6 @@
|
|
|
9620
9620
|
"symbolId": "src/ses-receive:SESReceiveProps"
|
|
9621
9621
|
}
|
|
9622
9622
|
},
|
|
9623
|
-
"version": "0.1.
|
|
9624
|
-
"fingerprint": "
|
|
9623
|
+
"version": "0.1.24",
|
|
9624
|
+
"fingerprint": "VsXTNX+CbDEHj6QL4Wk23mv6Fvtf5IcmZ+EaZ822B68="
|
|
9625
9625
|
}
|
package/lib/rootmail.js
CHANGED
|
@@ -11,7 +11,7 @@ const ses_receive_1 = require("./ses-receive");
|
|
|
11
11
|
* Rootmail construct
|
|
12
12
|
*/
|
|
13
13
|
class Rootmail extends constructs_1.Construct {
|
|
14
|
-
static [JSII_RTTI_SYMBOL_1] = { fqn: "@mavogel/awscdk-rootmail.Rootmail", version: "0.1.
|
|
14
|
+
static [JSII_RTTI_SYMBOL_1] = { fqn: "@mavogel/awscdk-rootmail.Rootmail", version: "0.1.24" };
|
|
15
15
|
/**
|
|
16
16
|
* The name parameter in SSM to store the domain name server.
|
|
17
17
|
*/
|
package/lib/ses-receive.js
CHANGED
|
@@ -11,7 +11,7 @@ const ses_receipt_ruleset_activation_1 = require("./ses-receipt-ruleset-activati
|
|
|
11
11
|
* SES Receive construct
|
|
12
12
|
*/
|
|
13
13
|
class SESReceive extends constructs_1.Construct {
|
|
14
|
-
static [JSII_RTTI_SYMBOL_1] = { fqn: "@mavogel/awscdk-rootmail.SESReceive", version: "0.1.
|
|
14
|
+
static [JSII_RTTI_SYMBOL_1] = { fqn: "@mavogel/awscdk-rootmail.SESReceive", version: "0.1.24" };
|
|
15
15
|
constructor(scope, id, props) {
|
|
16
16
|
super(scope, id);
|
|
17
17
|
const filteredEmailSubjects = props.filteredEmailSubjects || [];
|
|
@@ -392,18 +392,29 @@ class Headers {
|
|
|
392
392
|
lines.push({ key: this._normalizeHeader(first.substr(0, first.indexOf(':'))), line: first });
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
// Set while the last entry has no colon yet, so that its key may still come from a
|
|
396
|
+
// continuation line. Only the new line is searched then: searching the whole
|
|
397
|
+
// accumulated line again for every continuation was quadratic on a keyless block
|
|
398
|
+
let keyPending = false;
|
|
399
|
+
|
|
395
400
|
for (; i < rawLines.length; i++) {
|
|
396
401
|
let currentLine = rawLines[i];
|
|
397
402
|
if (this._isFoldingChar(currentLine.charAt(0))) {
|
|
398
403
|
// the loop starts on a non-folded line, so there is always a last entry here
|
|
399
404
|
let entry = lines[lines.length - 1];
|
|
400
|
-
|
|
401
|
-
if (!entry.key) {
|
|
405
|
+
if (keyPending) {
|
|
402
406
|
// the key comes from the first colon, which may only appear in a continuation
|
|
403
|
-
|
|
407
|
+
let colon = currentLine.indexOf(':');
|
|
408
|
+
if (colon >= 0) {
|
|
409
|
+
entry.key = this._normalizeHeader(entry.line + '\r\n' + currentLine.substr(0, colon));
|
|
410
|
+
keyPending = false;
|
|
411
|
+
}
|
|
404
412
|
}
|
|
413
|
+
entry.line += '\r\n' + currentLine;
|
|
405
414
|
} else {
|
|
406
|
-
|
|
415
|
+
let colon = currentLine.indexOf(':');
|
|
416
|
+
lines.push({ key: this._normalizeHeader(currentLine.substr(0, colon)), line: currentLine });
|
|
417
|
+
keyPending = colon < 0;
|
|
407
418
|
}
|
|
408
419
|
}
|
|
409
420
|
|
|
@@ -94,6 +94,9 @@ class MessageSplitter extends Transform {
|
|
|
94
94
|
// set once the closing delimiter of the current node's multipart has been seen, so
|
|
95
95
|
// that any later boundary line of that node counts as epilogue. Reset per node.
|
|
96
96
|
this.inEpilogue = false;
|
|
97
|
+
// the open ancestor multipart whose boundary the last delimiter line matched, set
|
|
98
|
+
// by checkBoundary() for a sibling (3) or closing (4) delimiter
|
|
99
|
+
this.delimiterOwner = /** @type {MimeNodeType | false} */ (false);
|
|
97
100
|
this.newNode();
|
|
98
101
|
// incomplete trailing line of the previous chunk, kept as a list of chunks so
|
|
99
102
|
// that a long line without a line break is not copied over for every write
|
|
@@ -452,15 +455,38 @@ class MessageSplitter extends Transform {
|
|
|
452
455
|
return boundary;
|
|
453
456
|
}
|
|
454
457
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
458
|
+
// A delimiter of any open ancestor multipart, not only the parent, ends everything
|
|
459
|
+
// below it. An inner multipart that is never closed would otherwise swallow the
|
|
460
|
+
// rest of the message as body text of its last part, while other parsers and mail
|
|
461
|
+
// clients show the parts that follow the outer delimiter
|
|
462
|
+
for (let owner = this.node._parentBoundaryOwner; owner; owner = owner._parentBoundaryOwner) {
|
|
463
|
+
if (owner._boundary && (boundary = this.compareBoundary(line, startpos, owner._boundary))) {
|
|
464
|
+
// 3: next sibling under the owner
|
|
465
|
+
// 4: the owner's multipart ends
|
|
466
|
+
this.delimiterOwner = owner;
|
|
467
|
+
return /** @type {3 | 4} */ (boundary + 2);
|
|
468
|
+
}
|
|
459
469
|
}
|
|
460
470
|
|
|
461
471
|
return false;
|
|
462
472
|
}
|
|
463
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Longest boundary a delimiter line on the current node could carry: the node's own
|
|
476
|
+
* boundary or that of any open ancestor multipart.
|
|
477
|
+
*
|
|
478
|
+
* @returns {number} Boundary length in bytes, 0 when no boundary applies.
|
|
479
|
+
*/
|
|
480
|
+
maxBoundaryLength() {
|
|
481
|
+
let max = this.node._boundary ? this.node._boundary.length : 0;
|
|
482
|
+
for (let owner = this.node._parentBoundaryOwner; owner; owner = owner._parentBoundaryOwner) {
|
|
483
|
+
if (owner._boundary && owner._boundary.length > max) {
|
|
484
|
+
max = owner._boundary.length;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return max;
|
|
488
|
+
}
|
|
489
|
+
|
|
464
490
|
/**
|
|
465
491
|
* Checks the header bytes collected for the current node against maxHeadSize.
|
|
466
492
|
*
|
|
@@ -488,12 +514,7 @@ class MessageSplitter extends Transform {
|
|
|
488
514
|
return null;
|
|
489
515
|
}
|
|
490
516
|
|
|
491
|
-
|
|
492
|
-
this.node._boundary ? this.node._boundary.length : 0,
|
|
493
|
-
this.node._parentBoundary ? this.node._parentBoundary.length : 0
|
|
494
|
-
);
|
|
495
|
-
|
|
496
|
-
if (this.lineLength <= maxBoundaryLength + BOUNDARY_LINE_OVERHEAD) {
|
|
517
|
+
if (this.lineLength <= this.maxBoundaryLength() + BOUNDARY_LINE_OVERHEAD) {
|
|
497
518
|
// might still turn out to be a boundary delimiter line
|
|
498
519
|
return null;
|
|
499
520
|
}
|
|
@@ -571,8 +592,8 @@ class MessageSplitter extends Transform {
|
|
|
571
592
|
// reached end of children, keep current node
|
|
572
593
|
break;
|
|
573
594
|
case 3: {
|
|
574
|
-
// next sibling
|
|
575
|
-
this.newNode(this.
|
|
595
|
+
// next sibling under the multipart that owns the matched boundary
|
|
596
|
+
this.newNode(this.delimiterOwner);
|
|
576
597
|
flush = true;
|
|
577
598
|
break;
|
|
578
599
|
}
|
|
@@ -583,7 +604,7 @@ class MessageSplitter extends Transform {
|
|
|
583
604
|
this.push(this.node);
|
|
584
605
|
}
|
|
585
606
|
// move up to the multipart node this closing delimiter belongs to
|
|
586
|
-
let parentNode = this.
|
|
607
|
+
let parentNode = this.delimiterOwner;
|
|
587
608
|
if (parentNode) {
|
|
588
609
|
this.node = parentNode;
|
|
589
610
|
// the closing delimiter of this multipart was just processed, so any
|
|
@@ -662,16 +683,6 @@ class MessageSplitter extends Transform {
|
|
|
662
683
|
next(null, false);
|
|
663
684
|
}
|
|
664
685
|
|
|
665
|
-
/**
|
|
666
|
-
* Resolves the multipart node that owns the boundary of the current node, ie. the
|
|
667
|
-
* node a sibling delimiter or a closing delimiter of _parentBoundary refers to.
|
|
668
|
-
*
|
|
669
|
-
* @returns {MimeNodeType | false} Owner of _parentBoundary or false for the root node.
|
|
670
|
-
*/
|
|
671
|
-
parentMultipartNode() {
|
|
672
|
-
return this.node._parentBoundaryOwner || false;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
686
|
/**
|
|
676
687
|
* @param {MimeNodeType | false} [parent]
|
|
677
688
|
* @returns {void}
|
|
@@ -44,6 +44,55 @@ function stripComments(value) {
|
|
|
44
44
|
.join('');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Lists the parameters a header value gives in RFC 2231 form (`name*=`, `name*0=` or
|
|
49
|
+
* `name*0*=`). libmime decodes those while parsing the header, so the result must not
|
|
50
|
+
* go through `decodeWords` as well: a value like `filename*=utf-8''a%3D%3Futf-8%3FQ%3F%3D2Eexe%3F%3D`
|
|
51
|
+
* would otherwise come out as `a.exe` while a parser that decodes once sees the encoded word.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} value Raw header value.
|
|
54
|
+
* @returns {Set<string>} Lowercased parameter names.
|
|
55
|
+
*/
|
|
56
|
+
function rfc2231ParamNames(value) {
|
|
57
|
+
let names = new Set();
|
|
58
|
+
let quote = false;
|
|
59
|
+
let escaped = false;
|
|
60
|
+
let start = 0;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {number} end End offset of the segment starting at `start`.
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
let addSegment = end => {
|
|
67
|
+
let segment = value.substring(start, end);
|
|
68
|
+
let eq = segment.indexOf('=');
|
|
69
|
+
if (eq < 0) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let match = segment.substring(0, eq).trim().toLowerCase().match(/^(.+?)\*(\d+\*?)?$/);
|
|
73
|
+
if (match) {
|
|
74
|
+
names.add(match[1]);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < value.length; i++) {
|
|
79
|
+
let chr = value.charAt(i);
|
|
80
|
+
if (escaped) {
|
|
81
|
+
escaped = false;
|
|
82
|
+
} else if (chr === '\\') {
|
|
83
|
+
escaped = true;
|
|
84
|
+
} else if (chr === '"') {
|
|
85
|
+
quote = !quote;
|
|
86
|
+
} else if (chr === ';' && !quote) {
|
|
87
|
+
addSegment(i);
|
|
88
|
+
start = i + 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
addSegment(value.length);
|
|
92
|
+
|
|
93
|
+
return names;
|
|
94
|
+
}
|
|
95
|
+
|
|
47
96
|
/**
|
|
48
97
|
* Parsed MIME node with mutable headers and transfer-encoding helpers.
|
|
49
98
|
*/
|
|
@@ -142,7 +191,8 @@ class MimeNode {
|
|
|
142
191
|
this.headers = new Headers(Buffer.concat(this._headersLines, this._headerlen), this.config);
|
|
143
192
|
let headers = this.headers;
|
|
144
193
|
|
|
145
|
-
|
|
194
|
+
let dispositionHeader = headers.getFirst('Content-Disposition');
|
|
195
|
+
this._parsedContentDisposition = this.libmime.parseHeaderValue(dispositionHeader);
|
|
146
196
|
let parsedContentDisposition = /** @type {StructuredHeader} */ (this._parsedContentDisposition);
|
|
147
197
|
|
|
148
198
|
// if content-type is missing default to plaintext
|
|
@@ -192,10 +242,16 @@ class MimeNode {
|
|
|
192
242
|
}
|
|
193
243
|
|
|
194
244
|
if (this.filename) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
245
|
+
// a parameter given in RFC 2231 form was decoded by libmime already
|
|
246
|
+
let decoded = parsedContentDisposition.params.filename
|
|
247
|
+
? rfc2231ParamNames(dispositionHeader).has('filename')
|
|
248
|
+
: rfc2231ParamNames(contentHeader).has('name');
|
|
249
|
+
if (!decoded) {
|
|
250
|
+
try {
|
|
251
|
+
this.filename = this.libmime.decodeWords(this.filename);
|
|
252
|
+
} catch (E) {
|
|
253
|
+
// failed to parse filename, keep as is (most probably an unknown charset is used)
|
|
254
|
+
}
|
|
199
255
|
}
|
|
200
256
|
}
|
|
201
257
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zone-eu/mailsplit",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.23",
|
|
4
4
|
"description": "Split email messages into an object stream",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"license": "(MIT OR EUPL-1.1+)",
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"libbase64": "1.3.3",
|
|
19
|
-
"libmime": "5.4.
|
|
20
|
-
"libqp": "2.1.
|
|
19
|
+
"libmime": "5.4.9",
|
|
20
|
+
"libqp": "2.1.3"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/eslint": "9.6.1",
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [5.4.9](https://github.com/nodemailer/libmime/compare/v5.4.8...v5.4.9) (2026-10-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* bump libqp to 2.1.3 ([6cee51c](https://github.com/nodemailer/libmime/commit/6cee51c9f39367e762e05e07046abbd7b0e34399))
|
|
9
|
+
* encode long non-ASCII words in linear time, refuse transfer-encoding codecs as charsets and mark RFC 2231 parameters as decoded ([60b3a88](https://github.com/nodemailer/libmime/commit/60b3a8881c2ff460573ee359e9d3148fcf766325))
|
|
10
|
+
|
|
3
11
|
## [5.4.8](https://github.com/nodemailer/libmime/compare/v5.4.7...v5.4.8) (2026-10-14)
|
|
4
12
|
|
|
5
13
|
|
|
@@ -5,6 +5,13 @@ const iconv = require('iconv-lite');
|
|
|
5
5
|
const encodingJapanese = require('encoding-japanese');
|
|
6
6
|
const charsets = require('./charsets');
|
|
7
7
|
|
|
8
|
+
// iconv-lite codecs that are not character sets a message could be written in. UTF-7 is a known
|
|
9
|
+
// way to smuggle markup past filters (=?utf-7?Q?+ADw-script+AD4-?= decodes to "<script>"), base64
|
|
10
|
+
// and hex are transfer encodings. A name resolving to one of them is treated like a charset
|
|
11
|
+
// iconv-lite does not know. Compared in the canonical form iconv-lite itself resolves names in
|
|
12
|
+
const UNSUPPORTED_CODECS = new Set(['utf7', 'utf7imap', 'base64', 'hex']);
|
|
13
|
+
const isUnsupportedCodec = name => UNSUPPORTED_CODECS.has(name.toLowerCase().replace(/:\d{4}$|[^0-9a-z]/g, ''));
|
|
14
|
+
|
|
8
15
|
/**
|
|
9
16
|
* Character set encoding and decoding functions
|
|
10
17
|
*/
|
|
@@ -32,10 +39,15 @@ const charset = (module.exports = {
|
|
|
32
39
|
decode(buf, fromCharset) {
|
|
33
40
|
fromCharset = charset.normalizeCharset(fromCharset || 'UTF-8');
|
|
34
41
|
|
|
35
|
-
if (/^(us-)?ascii|utf-8|7bit$/i.test(fromCharset)) {
|
|
42
|
+
if (/^(?:(?:us-)?ascii|utf-8|7bit)$/i.test(fromCharset)) {
|
|
36
43
|
return buf.toString('utf-8');
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
if (isUnsupportedCodec(fromCharset)) {
|
|
47
|
+
// enforce utf-8, the same as for a charset iconv-lite does not know
|
|
48
|
+
return buf.toString();
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
try {
|
|
40
52
|
if (/^jis|^iso-?2022-?jp|^EUCJP/i.test(fromCharset)) {
|
|
41
53
|
if (typeof buf === 'string') {
|
|
@@ -76,7 +88,7 @@ const charset = (module.exports = {
|
|
|
76
88
|
let bufString;
|
|
77
89
|
|
|
78
90
|
if (typeof data !== 'string') {
|
|
79
|
-
if (/^(us-)?ascii|utf-8|7bit$/i.test(fromCharset)) {
|
|
91
|
+
if (/^(?:(?:us-)?ascii|utf-8|7bit)$/i.test(fromCharset)) {
|
|
80
92
|
return data;
|
|
81
93
|
}
|
|
82
94
|
|
|
@@ -333,11 +333,6 @@ class Libmime {
|
|
|
333
333
|
if (!firstMatch) {
|
|
334
334
|
return decodedValue;
|
|
335
335
|
}
|
|
336
|
-
let lastMatch = decodedValue.match(/([\u0080-\uFFFF][^\s]*)[^\u0080-\uFFFF]*$/);
|
|
337
|
-
if (!lastMatch) {
|
|
338
|
-
// should not happen
|
|
339
|
-
return decodedValue;
|
|
340
|
-
}
|
|
341
336
|
let startIndex =
|
|
342
337
|
firstMatch.index +
|
|
343
338
|
(
|
|
@@ -345,7 +340,16 @@ class Libmime {
|
|
|
345
340
|
index: 0
|
|
346
341
|
}
|
|
347
342
|
).index;
|
|
348
|
-
|
|
343
|
+
|
|
344
|
+
// The encoded part ends with the word that holds the last non ascii char. Found with a scan,
|
|
345
|
+
// the regex that used to do this rescanned the rest of the word from every non ascii char in
|
|
346
|
+
// it, which made a long non ascii word quadratic
|
|
347
|
+
let lastIndex = decodedValue.length - 1;
|
|
348
|
+
while (lastIndex >= 0 && decodedValue.charCodeAt(lastIndex) < 0x80) {
|
|
349
|
+
lastIndex--;
|
|
350
|
+
}
|
|
351
|
+
let wordEnd = decodedValue.slice(lastIndex + 1).search(/\s/);
|
|
352
|
+
let endIndex = wordEnd < 0 ? decodedValue.length : lastIndex + 1 + wordEnd;
|
|
349
353
|
|
|
350
354
|
encodedValue =
|
|
351
355
|
(startIndex ? decodedValue.substr(0, startIndex) : '') +
|
|
@@ -515,6 +519,10 @@ class Libmime {
|
|
|
515
519
|
* }
|
|
516
520
|
* }
|
|
517
521
|
*
|
|
522
|
+
* The result also carries a non-enumerable `decodedParams` array that names the params
|
|
523
|
+
* assembled from rfc2231 segments (title*=, title*0*=, title*0=). Those values are already
|
|
524
|
+
* decoded, so a caller that runs decodeWords() over the params has to leave them out
|
|
525
|
+
*
|
|
518
526
|
* @param {String} str Header value
|
|
519
527
|
* @return {Object} Header value as a parsed structure
|
|
520
528
|
*/
|
|
@@ -523,6 +531,16 @@ class Libmime {
|
|
|
523
531
|
value: false,
|
|
524
532
|
params: {}
|
|
525
533
|
};
|
|
534
|
+
|
|
535
|
+
// names of the params assembled from rfc2231 segments, see the continuation handling below.
|
|
536
|
+
// Non-enumerable, so the {value, params} shape callers serialize and compare stays as it is
|
|
537
|
+
Object.defineProperty(response, 'decodedParams', {
|
|
538
|
+
value: [],
|
|
539
|
+
writable: true,
|
|
540
|
+
enumerable: false,
|
|
541
|
+
configurable: true
|
|
542
|
+
});
|
|
543
|
+
|
|
526
544
|
let key = false;
|
|
527
545
|
let value = '';
|
|
528
546
|
// Length of the significant part of `value`. Whitespace outside of quotes is
|
|
@@ -656,6 +674,12 @@ class Libmime {
|
|
|
656
674
|
.map(val => val.value)
|
|
657
675
|
.join('');
|
|
658
676
|
|
|
677
|
+
// Both branches below store a decoded value, so a caller that runs decodeWords() over the
|
|
678
|
+
// params has to leave this one out. A value that percent-encodes an encoded word would
|
|
679
|
+
// otherwise be decoded twice: filename*=utf-8''invoice%3D%3Futf-8%3FQ%3F%3D2Eexe%3F%3D
|
|
680
|
+
// names the file "invoice=?utf-8?Q?=2Eexe?=", not "invoice.exe"
|
|
681
|
+
response.decodedParams.push(key);
|
|
682
|
+
|
|
659
683
|
if (!continuation.charset) {
|
|
660
684
|
setOwnProperty(response.params, key, this.decodeWords(value));
|
|
661
685
|
continue;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libmime",
|
|
3
3
|
"description": "Encode and decode quoted printable and base64 strings",
|
|
4
|
-
"version": "5.4.
|
|
4
|
+
"version": "5.4.9",
|
|
5
5
|
"main": "lib/libmime.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"lib",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"encoding-japanese": "2.4.0",
|
|
28
28
|
"iconv-lite": "0.7.3",
|
|
29
29
|
"libbase64": "1.3.3",
|
|
30
|
-
"libqp": "2.1.
|
|
30
|
+
"libqp": "2.1.3"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"chai": "4.4.1",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: "libqp security vulnerability report"
|
|
2
|
+
description: "Report an exploitable security vulnerability in libqp"
|
|
3
|
+
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Before reporting, read our
|
|
9
|
+
[Security Policy](https://github.com/nodemailer/libqp/security/policy).
|
|
10
|
+
|
|
11
|
+
Reports must demonstrate a credible security impact.
|
|
12
|
+
Technically valid bugs without a realistic attack
|
|
13
|
+
scenario belong in the regular issue tracker.
|
|
14
|
+
|
|
15
|
+
- type: textarea
|
|
16
|
+
id: summary
|
|
17
|
+
attributes:
|
|
18
|
+
label: Vulnerability summary
|
|
19
|
+
description: Describe the vulnerability and affected versions.
|
|
20
|
+
validations:
|
|
21
|
+
required: true
|
|
22
|
+
|
|
23
|
+
- type: textarea
|
|
24
|
+
id: attacker
|
|
25
|
+
attributes:
|
|
26
|
+
label: Attacker capabilities
|
|
27
|
+
description: |
|
|
28
|
+
What can an attacker control without already having
|
|
29
|
+
privileged access to the application or its configuration?
|
|
30
|
+
validations:
|
|
31
|
+
required: true
|
|
32
|
+
min_length: 100
|
|
33
|
+
|
|
34
|
+
- type: textarea
|
|
35
|
+
id: exploit
|
|
36
|
+
attributes:
|
|
37
|
+
label: Realistic exploitation scenario
|
|
38
|
+
description: |
|
|
39
|
+
Describe how this can be exploited in a real application.
|
|
40
|
+
Explain the prerequisites and security boundary crossed.
|
|
41
|
+
validations:
|
|
42
|
+
required: true
|
|
43
|
+
min_length: 150
|
|
44
|
+
|
|
45
|
+
- type: textarea
|
|
46
|
+
id: poc
|
|
47
|
+
attributes:
|
|
48
|
+
label: Proof of concept
|
|
49
|
+
description: Provide reproducible steps or working code.
|
|
50
|
+
validations:
|
|
51
|
+
required: true
|
|
52
|
+
|
|
53
|
+
- type: textarea
|
|
54
|
+
id: impact
|
|
55
|
+
attributes:
|
|
56
|
+
label: Security impact
|
|
57
|
+
description: |
|
|
58
|
+
What can the attacker achieve? Who is affected?
|
|
59
|
+
Explain why this is a security vulnerability rather
|
|
60
|
+
than an ordinary bug.
|
|
61
|
+
validations:
|
|
62
|
+
required: true
|
|
63
|
+
min_length: 100
|
|
64
|
+
|
|
65
|
+
- type: checkboxes
|
|
66
|
+
id: confirmation
|
|
67
|
+
attributes:
|
|
68
|
+
label: Reporting requirements
|
|
69
|
+
options:
|
|
70
|
+
- label: I have read the Security Policy.
|
|
71
|
+
required: true
|
|
72
|
+
- label: I have verified the finding against a supported version.
|
|
73
|
+
required: true
|
|
74
|
+
- label: I have described a credible attacker-controlled scenario.
|
|
75
|
+
required: true
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.3](https://github.com/nodemailer/libqp/compare/v2.1.2...v2.1.3) (2026-10-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* decode trailing whitespace in linear time and never start an encoded line with a hyphen ([9ece8c1](https://github.com/nodemailer/libqp/commit/9ece8c11826b88509afd968d5bc5b83f5f447e18))
|
|
9
|
+
|
|
3
10
|
## [2.1.2](https://github.com/nodemailer/libqp/compare/v2.1.1...v2.1.2) (2026-09-28)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
libqp encodes and decodes quoted-printable strings and streams as defined in RFC
|
|
4
|
+
2045 (MIME). It is used to decode untrusted email content. We welcome
|
|
5
|
+
responsible reports of vulnerabilities that present a credible security risk to
|
|
6
|
+
users. Please distinguish security vulnerabilities from ordinary bugs, unusual
|
|
7
|
+
edge cases, and hardening suggestions.
|
|
8
|
+
|
|
9
|
+
## Supported Versions
|
|
10
|
+
|
|
11
|
+
Security fixes are released only against the latest major version. We do not
|
|
12
|
+
backport patches to older majors, so upgrading to the current release line is
|
|
13
|
+
the supported way to receive security updates.
|
|
14
|
+
|
|
15
|
+
| Version | Supported |
|
|
16
|
+
| ------- | ------------------ |
|
|
17
|
+
| 2.x | :white_check_mark: |
|
|
18
|
+
| < 2.0 | :x: |
|
|
19
|
+
|
|
20
|
+
If you are on an older major, please upgrade. See [CHANGELOG.md](CHANGELOG.md)
|
|
21
|
+
before updating.
|
|
22
|
+
|
|
23
|
+
## What Qualifies as a Security Vulnerability
|
|
24
|
+
|
|
25
|
+
A security report should identify a realistic way for an attacker to cross a
|
|
26
|
+
security boundary in a supported version of libqp. Unexpected behavior, a
|
|
27
|
+
failing test, or a technically correct proof of concept is not sufficient on its
|
|
28
|
+
own to establish security impact.
|
|
29
|
+
|
|
30
|
+
In particular, explain:
|
|
31
|
+
|
|
32
|
+
- **Attacker capabilities:** Who is the attacker, and what inputs, systems, or
|
|
33
|
+
responses can they control _without_ already having the privileges or secrets
|
|
34
|
+
they are trying to gain?
|
|
35
|
+
- **Attack path:** How does attacker-controlled input reach the affected code
|
|
36
|
+
through a supported public API or a realistic application integration?
|
|
37
|
+
- **Prerequisites:** What configuration and user actions are required? If the
|
|
38
|
+
attack depends on unusual options or deliberately malformed input, explain why
|
|
39
|
+
those conditions could arise in a real application.
|
|
40
|
+
- **Impact:** What confidentiality, integrity, or availability boundary is
|
|
41
|
+
crossed, and what can the attacker achieve as a result?
|
|
42
|
+
|
|
43
|
+
A vulnerability involving an optional or less common feature can still qualify
|
|
44
|
+
if these conditions are met. Conversely, the existence of a defensive check in
|
|
45
|
+
the code does not, by itself, establish that every bypass of that check has a
|
|
46
|
+
realistic security impact.
|
|
47
|
+
|
|
48
|
+
## Reporting a Vulnerability
|
|
49
|
+
|
|
50
|
+
**Do not disclose suspected exploitable vulnerabilities through public GitHub
|
|
51
|
+
issues, pull requests, or discussions before coordinating with the
|
|
52
|
+
maintainers.**
|
|
53
|
+
|
|
54
|
+
Report privately through one of the following channels:
|
|
55
|
+
|
|
56
|
+
1. **GitHub Security Advisories (preferred):**
|
|
57
|
+
<https://github.com/nodemailer/libqp/security/advisories/new>.
|
|
58
|
+
2. **Email:** **andris@reinman.eu**. Encrypt sensitive details if possible (PGP
|
|
59
|
+
key fingerprint `5D952A46E1D8C931F6364E01DC6C83F4D584D364`).
|
|
60
|
+
|
|
61
|
+
### Required Information
|
|
62
|
+
|
|
63
|
+
Please provide enough information to evaluate both the defect and its security
|
|
64
|
+
impact. Reports should include:
|
|
65
|
+
|
|
66
|
+
1. The affected libqp version or commit, Node.js version, and relevant
|
|
67
|
+
configuration.
|
|
68
|
+
2. A concise description of the issue and the affected public API or code path.
|
|
69
|
+
3. A minimal, tested reproduction, including steps to run it, expected behavior,
|
|
70
|
+
and observed behavior.
|
|
71
|
+
4. A concrete threat model: attacker control, prerequisites, trust boundary, and
|
|
72
|
+
resulting impact. Clearly distinguish what your reproduction proves from what
|
|
73
|
+
you believe might be possible.
|
|
74
|
+
5. An explanation of why the issue affects real deployments beyond a
|
|
75
|
+
deliberately constructed test setup, where this is not obvious.
|
|
76
|
+
|
|
77
|
+
A suggested fix is welcome but does not replace an explanation of
|
|
78
|
+
exploitability. A complete real-world exploit is not always necessary, but the
|
|
79
|
+
report must make a credible security impact clear.
|
|
80
|
+
|
|
81
|
+
### Reports That May Be Treated as Ordinary Bugs
|
|
82
|
+
|
|
83
|
+
The following generally do **not** qualify for private security handling unless
|
|
84
|
+
you can demonstrate an additional, meaningful security boundary crossing:
|
|
85
|
+
|
|
86
|
+
- Issues that require the attacker to control trusted application code,
|
|
87
|
+
administrator-only configuration, or the secret they supposedly extract.
|
|
88
|
+
- Problems caused solely by application-level misuse, exposed credentials, or
|
|
89
|
+
insecure deployment choices outside libqp's control.
|
|
90
|
+
- Contrived proof-of-concept setups where the tester controls both sides of a
|
|
91
|
+
request, without explaining how a separate attacker could trigger the issue.
|
|
92
|
+
- Parser quirks, unexpected behavior, and bypasses of internal checks with no
|
|
93
|
+
demonstrated security impact.
|
|
94
|
+
- Speculative findings based only on static analysis or an automated tool's
|
|
95
|
+
assessment, without a verified reproduction and credible attack scenario.
|
|
96
|
+
|
|
97
|
+
These may still be valid bugs and may be considered through the normal issue
|
|
98
|
+
tracker. Please coordinate before making any potentially exploitable details
|
|
99
|
+
public. Classification as a bug does not guarantee a fix or a particular
|
|
100
|
+
priority.
|
|
101
|
+
|
|
102
|
+
### Automated and AI-Assisted Reports
|
|
103
|
+
|
|
104
|
+
Automated analysis and AI tools are welcome as research aids, but the person
|
|
105
|
+
submitting a report is responsible for verifying its claims. **Do not submit
|
|
106
|
+
bulk, unverified, or speculative AI-generated findings.** Test each finding
|
|
107
|
+
against a supported release and establish the actual security impact. Reports
|
|
108
|
+
that do not meet the requirements above may be closed without further
|
|
109
|
+
investigation or individual feedback; they can be resubmitted with stronger
|
|
110
|
+
evidence.
|
|
111
|
+
|
|
112
|
+
## Triage and Disclosure
|
|
113
|
+
|
|
114
|
+
libqp is maintained by a single person, and there is **no guaranteed response or
|
|
115
|
+
resolution time**. Reports are prioritized based on demonstrated impact,
|
|
116
|
+
exploitability, affected usage, and available maintenance resources.
|
|
117
|
+
|
|
118
|
+
A private report does not automatically become a security advisory. Confirmed
|
|
119
|
+
vulnerabilities may be fixed and disclosed through GitHub Security Advisories;
|
|
120
|
+
other findings may be closed, deferred, or handled as ordinary bugs. Please do
|
|
121
|
+
not disclose details of an unresolved, potentially exploitable issue publicly
|
|
122
|
+
without coordinating with the maintainers.
|
|
123
|
+
|
|
124
|
+
## CVEs and Credit
|
|
125
|
+
|
|
126
|
+
We do not request or manage CVE identifiers. A report being acknowledged, a bug
|
|
127
|
+
being fixed, or a pull request being merged does **not** mean that libqp has
|
|
128
|
+
confirmed a security vulnerability or endorsed a CVE assignment. We do not
|
|
129
|
+
guarantee advisories, CVE identifiers, or public credit for reports. For
|
|
130
|
+
confirmed vulnerabilities disclosed through an advisory, substantive reporters
|
|
131
|
+
may be credited if they wish.
|
|
132
|
+
|
|
133
|
+
## Scope
|
|
134
|
+
|
|
135
|
+
In scope: the libqp source in this repository: the encoder and decoder functions
|
|
136
|
+
and their stream variants. Of particular interest are inputs that crash the
|
|
137
|
+
process, corrupt output beyond the malformed input, or consume resources out of
|
|
138
|
+
proportion to their size.
|
|
139
|
+
|
|
140
|
+
Out of scope:
|
|
141
|
+
|
|
142
|
+
- Resource consumption that is proportional to a legitimately large input.
|
|
143
|
+
- Vulnerabilities in your own application code, including how it handles decoded
|
|
144
|
+
data.
|
|
145
|
+
- Social-engineering reports.
|
|
146
|
+
|
|
147
|
+
Thank you for helping keep libqp and its users safe.
|