@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.
Files changed (53) hide show
  1. package/.jsii +4 -4
  2. package/lib/rootmail.js +1 -1
  3. package/lib/ses-receive.js +1 -1
  4. package/node_modules/@zone-eu/mailsplit/lib/headers.js +15 -4
  5. package/node_modules/@zone-eu/mailsplit/lib/message-splitter.js +34 -23
  6. package/node_modules/@zone-eu/mailsplit/lib/mime-node.js +61 -5
  7. package/node_modules/@zone-eu/mailsplit/package.json +3 -3
  8. package/node_modules/libmime/CHANGELOG.md +8 -0
  9. package/node_modules/libmime/lib/charset.js +14 -2
  10. package/node_modules/libmime/lib/libmime.js +30 -6
  11. package/node_modules/libmime/package.json +2 -2
  12. package/node_modules/libqp/.github/VULNERABILITY_REPORT.yml +75 -0
  13. package/node_modules/libqp/CHANGELOG.md +7 -0
  14. package/node_modules/libqp/SECURITY.md +147 -0
  15. package/node_modules/libqp/lib/libqp.js +96 -24
  16. package/node_modules/libqp/package.json +1 -1
  17. package/node_modules/libqp/test/libqp-test.js +108 -0
  18. package/node_modules/mailparser/.release-please-manifest.json +1 -1
  19. package/node_modules/mailparser/CHANGELOG.md +9 -0
  20. package/node_modules/mailparser/README.md +2 -1
  21. package/node_modules/mailparser/lib/mail-parser.js +160 -65
  22. package/node_modules/mailparser/lib/simple-parser.js +3 -3
  23. package/node_modules/mailparser/package.json +4 -4
  24. package/node_modules/nodemailer/CHANGELOG.md +7 -0
  25. package/node_modules/nodemailer/dist/cjs/dkim/index.js +3 -2
  26. package/node_modules/nodemailer/dist/cjs/fetch/index.d.ts +5 -0
  27. package/node_modules/nodemailer/dist/cjs/fetch/index.js +30 -0
  28. package/node_modules/nodemailer/dist/cjs/mailer/index.js +4 -1
  29. package/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +5 -1
  30. package/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +25 -13
  31. package/node_modules/nodemailer/dist/cjs/mime-node/index.js +49 -6
  32. package/node_modules/nodemailer/dist/cjs/package-info.d.ts +1 -1
  33. package/node_modules/nodemailer/dist/cjs/package-info.js +1 -1
  34. package/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +16 -1
  35. package/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +12 -1
  36. package/node_modules/nodemailer/dist/cjs/well-known/services.js +7 -0
  37. package/node_modules/nodemailer/dist/cjs/xoauth2/index.js +8 -2
  38. package/node_modules/nodemailer/dist/esm/dkim/index.js +3 -2
  39. package/node_modules/nodemailer/dist/esm/fetch/index.d.ts +5 -0
  40. package/node_modules/nodemailer/dist/esm/fetch/index.js +30 -0
  41. package/node_modules/nodemailer/dist/esm/mailer/index.js +4 -1
  42. package/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +5 -1
  43. package/node_modules/nodemailer/dist/esm/mime-funcs/index.js +25 -13
  44. package/node_modules/nodemailer/dist/esm/mime-node/index.js +49 -6
  45. package/node_modules/nodemailer/dist/esm/package-info.d.ts +1 -1
  46. package/node_modules/nodemailer/dist/esm/package-info.js +1 -1
  47. package/node_modules/nodemailer/dist/esm/smtp-connection/index.js +16 -1
  48. package/node_modules/nodemailer/dist/esm/smtp-transport/index.js +12 -1
  49. package/node_modules/nodemailer/dist/esm/well-known/services.js +7 -0
  50. package/node_modules/nodemailer/dist/esm/xoauth2/index.js +8 -2
  51. package/node_modules/nodemailer/dist/well-known/services.json +8 -0
  52. package/node_modules/nodemailer/package.json +1 -1
  53. 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.39",
17
+ "mailparser": "^3.9.40",
18
18
  "uuid": "^14.0.3"
19
19
  },
20
20
  "dependencies": {
21
- "@mavogel/mvc-projen": "^0.0.60",
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.22",
9624
- "fingerprint": "yVkAiOChDR563/v1x8I2vx1yVpLq5RvYhIUE5purNYA="
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.22" };
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
  */
@@ -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.22" };
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
- entry.line += '\r\n' + currentLine;
401
- if (!entry.key) {
405
+ if (keyPending) {
402
406
  // the key comes from the first colon, which may only appear in a continuation
403
- entry.key = this._normalizeHeader(entry.line.substr(0, entry.line.indexOf(':')));
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
- lines.push({ key: this._normalizeHeader(currentLine.substr(0, currentLine.indexOf(':'))), line: currentLine });
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
- if (this.node._parentBoundary && (boundary = this.compareBoundary(line, startpos, this.node._parentBoundary))) {
456
- // 3: next sibling
457
- // 4: parent end
458
- return /** @type {3 | 4} */ (boundary + 2);
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
- let maxBoundaryLength = Math.max(
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.parentMultipartNode());
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.parentMultipartNode();
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
- this._parsedContentDisposition = this.libmime.parseHeaderValue(headers.getFirst('Content-Disposition'));
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
- try {
196
- this.filename = this.libmime.decodeWords(this.filename);
197
- } catch (E) {
198
- // failed to parse filename, keep as is (most probably an unknown charset is used)
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.21",
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.8",
20
- "libqp": "2.1.2"
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
- let endIndex = lastMatch.index + (lastMatch[1] || '').length;
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.8",
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.2"
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.