@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
@@ -82,27 +82,121 @@ linkify.add('@', {
82
82
  }
83
83
  });
84
84
 
85
- // Linear equivalent of /<[^<>]+@[^<>]+>/.test(str). The regex backtracks
86
- // quadratically on a long run of @ characters after a lone <, and the input is a
87
- // decoded encoded-word bounded only by the header size limit.
88
- function hasBracketedAddress(str) {
89
- let start = -1;
90
- // first @ after the current < that has at least one character before it
91
- let at = -1;
92
- for (let i = 0, len = str.length; i < len; i++) {
93
- let chr = str.charAt(i);
94
- if (chr === '<') {
95
- start = i;
96
- at = -1;
97
- } else if (chr === '>') {
98
- if (start >= 0 && at >= 0 && at < i - 1) {
99
- return true;
85
+ // Lists the parameters a header value gives in RFC 2231 form (name*=, name*0= or
86
+ // name*0*=). libmime decodes those while parsing the header, so the result must not go
87
+ // through decodeWords as well: filename*=utf-8''a%3D%3Futf-8%3FQ%3F%3D2Eexe%3F%3D would
88
+ // otherwise come out as "a.exe" while a parser that decodes once sees the encoded word.
89
+ function rfc2231ParamNames(value) {
90
+ let names = new Set();
91
+ let quote = false;
92
+ let escaped = false;
93
+ let start = 0;
94
+
95
+ let addSegment = end => {
96
+ let segment = value.substring(start, end);
97
+ let eq = segment.indexOf('=');
98
+ if (eq < 0) {
99
+ return;
100
+ }
101
+ let match = segment
102
+ .substring(0, eq)
103
+ .trim()
104
+ .toLowerCase()
105
+ .match(/^(.+?)\*(\d+\*?)?$/);
106
+ if (match) {
107
+ names.add(match[1]);
108
+ }
109
+ };
110
+
111
+ for (let i = 0; i < value.length; i++) {
112
+ let chr = value.charAt(i);
113
+ if (escaped) {
114
+ escaped = false;
115
+ } else if (chr === '\\') {
116
+ escaped = true;
117
+ } else if (chr === '"') {
118
+ quote = !quote;
119
+ } else if (chr === ';' && !quote) {
120
+ addSegment(i);
121
+ start = i + 1;
122
+ }
123
+ }
124
+ addSegment(value.length);
125
+
126
+ return names;
127
+ }
128
+
129
+ // Deepest element nesting html-to-text is asked to convert. htmlparser2 keeps its open
130
+ // element stack at the front of an array, so parsing alone is quadratic in the nesting
131
+ // depth (200k nested <b> took 9 s), and html-to-text's recursive walk overflows the call
132
+ // stack at about 2000 levels. Real markup stays far below this: Chrome's own parser
133
+ // flattens anything nested deeper than 512 levels.
134
+ const MAX_HTML_NESTING_DEPTH = 1000;
135
+
136
+ // Elements that never hold content, so a start tag does not open a level
137
+ const HTML_VOID_ELEMENTS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
138
+
139
+ // Elements the HTML parser closes implicitly when a sibling opens (li after li, td after
140
+ // td, p after p), so an unclosed run of them does not nest. They are left out of the depth
141
+ // count: a report with thousands of unclosed table cells is sloppy but common markup
142
+ const HTML_IMPLIED_END_ELEMENTS = new Set(['p', 'li', 'dt', 'dd', 'option', 'optgroup', 'tr', 'td', 'th', 'thead', 'tbody', 'tfoot', 'rt', 'rp']);
143
+
144
+ function isTagNameChar(code) {
145
+ return (
146
+ (code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a) || (code >= 0x30 && code <= 0x39) || code === 0x2d || code === 0x3a || code === 0x5f
147
+ );
148
+ }
149
+
150
+ // Reports whether `html` nests elements deeper than `maxDepth`, in one linear pass that
151
+ // follows what htmlparser2 builds: a start tag opens an element (a trailing "/" does not
152
+ // close it in HTML mode), an end tag closes down to the nearest open element of that name
153
+ // and is ignored otherwise, comments are skipped. Returns as soon as the limit is crossed.
154
+ function exceedsHtmlNestingDepth(html, maxDepth) {
155
+ let stack = [];
156
+ let open = new Map(); // element name -> how many of them are on the stack
157
+ let len = html.length;
158
+ let i = html.indexOf('<');
159
+ while (i >= 0) {
160
+ let pos = i + 1;
161
+ if (html.startsWith('!--', pos)) {
162
+ let end = html.indexOf('-->', pos + 3);
163
+ i = end < 0 ? -1 : html.indexOf('<', end + 3);
164
+ continue;
165
+ }
166
+ let closing = html.charCodeAt(pos) === 0x2f; // "/"
167
+ if (closing) {
168
+ pos++;
169
+ }
170
+ let nameStart = pos;
171
+ while (pos < len && isTagNameChar(html.charCodeAt(pos))) {
172
+ pos++;
173
+ }
174
+ if (pos === nameStart) {
175
+ // not a tag, "<" is text here
176
+ i = html.indexOf('<', pos);
177
+ continue;
178
+ }
179
+ let name = html.slice(nameStart, pos).toLowerCase();
180
+ let end = html.indexOf('>', pos);
181
+ if (end < 0) {
182
+ break;
183
+ }
184
+ if (!HTML_VOID_ELEMENTS.has(name) && !HTML_IMPLIED_END_ELEMENTS.has(name)) {
185
+ if (!closing) {
186
+ stack.push(name);
187
+ open.set(name, (open.get(name) || 0) + 1);
188
+ if (stack.length > maxDepth) {
189
+ return true;
190
+ }
191
+ } else if (open.get(name)) {
192
+ let popped;
193
+ do {
194
+ popped = stack.pop();
195
+ open.set(popped, open.get(popped) - 1);
196
+ } while (popped !== name);
100
197
  }
101
- start = -1;
102
- at = -1;
103
- } else if (chr === '@' && start >= 0 && at < 0 && i > start + 1) {
104
- at = i;
105
198
  }
199
+ i = html.indexOf('<', end + 1);
106
200
  }
107
201
  return false;
108
202
  }
@@ -379,12 +473,17 @@ class MailParser extends Transform {
379
473
  switch (key) {
380
474
  case 'content-type':
381
475
  case 'content-disposition':
382
- case 'dkim-signature':
476
+ case 'dkim-signature': {
477
+ // a parameter given in RFC 2231 form is decoded by parseHeaderValue already
478
+ let rfc2231 = rfc2231ParamNames(value);
383
479
  value = this.libmime.parseHeaderValue(value);
384
480
  if (value.value) {
385
481
  value.value = this.libmime.decodeWords(value.value);
386
482
  }
387
483
  Object.keys((value && value.params) || {}).forEach(key => {
484
+ if (rfc2231.has(key)) {
485
+ return;
486
+ }
388
487
  try {
389
488
  value.params[key] = this.libmime.decodeWords(value.params[key]);
390
489
  } catch (E) {
@@ -392,6 +491,7 @@ class MailParser extends Transform {
392
491
  }
393
492
  });
394
493
  break;
494
+ }
395
495
  case 'date': {
396
496
  let dateValue = new Date(value);
397
497
  if (isNaN(dateValue)) {
@@ -464,7 +564,12 @@ class MailParser extends Transform {
464
564
  if (!headers.has(key)) {
465
565
  headers.set(key, [].concat(value || []));
466
566
  } else if (Array.isArray(value)) {
467
- headers.set(key, headers.get(key).concat(value));
567
+ // appended in place: rebuilding the list with concat for every repeated
568
+ // header was quadratic in the number of References lines
569
+ let list = headers.get(key);
570
+ for (let entry of value) {
571
+ list.push(entry);
572
+ }
468
573
  } else {
469
574
  headers.get(key).push(value);
470
575
  }
@@ -495,7 +600,9 @@ class MailParser extends Transform {
495
600
  headers.forEach((value, key) => {
496
601
  if (Array.isArray(value)) {
497
602
  if (singleKeys.includes(key) && value.length) {
498
- headers.set(key, value[value.length - 1]);
603
+ // the first occurrence: the one mailsplit built the MIME structure from,
604
+ // and the one mail clients display when the header is duplicated
605
+ headers.set(key, value[0]);
499
606
  } else if (value.length === 1) {
500
607
  headers.set(key, value[0]);
501
608
  }
@@ -593,38 +700,14 @@ class MailParser extends Transform {
593
700
  }
594
701
 
595
702
  decodeAddresses(addresses) {
596
- let processedAddress = new WeakSet();
597
703
  for (let i = 0; i < addresses.length; i++) {
598
704
  let address = addresses[i];
599
705
  address.name = (address.name || '').toString().trim();
600
706
 
601
- if (!address.address && /^(=\?([^?]+)\?[Bb]\?[^?]*\?=)(\s*=\?([^?]+)\?[Bb]\?[^?]*\?=)*$/.test(address.name) && !processedAddress.has(address)) {
602
- let decoded = this.libmime.decodeWords(address.name);
603
- // Security fix: only re-parse if decoded text contains angle-bracket-delimited
604
- // addresses (e.g. "Name <user@domain>"). Bare decoded text like "attacker@evil.com"
605
- // must not be re-interpreted as an address -- RFC 2047 Section 5 prohibits
606
- // encoded-words in addr-spec, and re-parsing fabricates addresses not present
607
- // in the original header.
608
- if (hasBracketedAddress(decoded)) {
609
- let parsed = addressparser(decoded);
610
- if (parsed.length) {
611
- parsed.forEach(entry => {
612
- processedAddress.add(entry);
613
- addresses.push(entry);
614
- });
615
- }
616
-
617
- // remove current element
618
- addresses.splice(i, 1);
619
- i--;
620
- continue;
621
- } else {
622
- // Treat decoded content as display name only
623
- address.name = decoded;
624
- continue;
625
- }
626
- }
627
-
707
+ // An encoded word decodes into the display name and nothing else, whatever it
708
+ // decodes to. RFC 2047 section 5 keeps encoded words out of the addr-spec, so a
709
+ // name that decodes to "CEO" <ceo@bank.com> is still only a name: parsing it
710
+ // again would report an address the header never carried
628
711
  if (address.name) {
629
712
  try {
630
713
  address.name = this.libmime.decodeWords(address.name);
@@ -762,6 +845,7 @@ class MailParser extends Transform {
762
845
  getTextContent() {
763
846
  let text = [];
764
847
  let html = [];
848
+ let textConversionError = null;
765
849
  let processNode = (alternative, level, node) => {
766
850
  if (node.showMeta) {
767
851
  let meta = ['From', 'Subject', 'Date', 'To', 'Cc', 'Bcc']
@@ -845,27 +929,34 @@ class MailParser extends Transform {
845
929
  html.push(this.textToHtml(node.textContent));
846
930
  }
847
931
  } else if (node.contentType === 'text/html') {
848
- let failedToParseHtml = false;
849
932
  if ((!alternative && this.hasText) || (node.root && !this.hasText)) {
850
933
  if (this.options.skipHtmlToText) {
851
934
  text.push('');
852
- } else if (node.textContent.length > this.options.maxHtmlLengthToParse) {
853
- this.emit('error', new Error(`HTML too long for parsing ${node.textContent.length} bytes`));
854
- text.push('Invalid HTML content (too long)');
855
- failedToParseHtml = true;
856
935
  } else {
857
- try {
858
- text.push(htmlToText(node.textContent));
859
- } catch (err) {
860
- this.emit('error', new Error('Failed to parse HTML'));
861
- text.push('Invalid HTML content');
862
- failedToParseHtml = true;
936
+ // HTML that can not be converted is no reason to lose the message:
937
+ // the text stays empty, the HTML is kept and the reason travels with
938
+ // the result as textConversionError. An 'error' event here used to
939
+ // reject the whole simpleParser() promise, headers and attachments
940
+ // included, after burning the CPU the conversion attempt took
941
+ let err = null;
942
+ if (node.textContent.length > this.options.maxHtmlLengthToParse) {
943
+ err = new Error(`HTML too long for parsing ${node.textContent.length} bytes`);
944
+ } else if (exceedsHtmlNestingDepth(node.textContent, MAX_HTML_NESTING_DEPTH)) {
945
+ err = new Error(`HTML nested deeper than ${MAX_HTML_NESTING_DEPTH} levels`);
946
+ } else {
947
+ try {
948
+ text.push(htmlToText(node.textContent, { limits: { maxDepth: MAX_HTML_NESTING_DEPTH } }));
949
+ } catch (cause) {
950
+ err = new Error('Failed to parse HTML', { cause });
951
+ }
952
+ }
953
+ if (err) {
954
+ text.push('');
955
+ textConversionError = textConversionError || err;
863
956
  }
864
957
  }
865
958
  }
866
- if (!failedToParseHtml) {
867
- html.push(node.textContent);
868
- }
959
+ html.push(node.textContent);
869
960
  }
870
961
  }
871
962
  alternative = alternative || node.contentType === 'multipart/alternative';
@@ -888,6 +979,9 @@ class MailParser extends Transform {
888
979
  this.text = response.text = text.join('\n');
889
980
  this.textAsHtml = response.textAsHtml = text.map(part => this.textToHtml(part)).join('<br/>\n');
890
981
  }
982
+ if (textConversionError) {
983
+ response.textConversionError = textConversionError;
984
+ }
891
985
  return response;
892
986
  }
893
987
 
@@ -1147,7 +1241,8 @@ class MailParser extends Transform {
1147
1241
  .map(address => {
1148
1242
  let str = '';
1149
1243
  if (address.name) {
1150
- str += `"${address.name}"` + (address.group ? ': ' : '');
1244
+ // a quoted-string, so a quote or backslash inside the name is escaped
1245
+ str += `"${address.name.replace(/["\\]/g, '\\$&')}"` + (address.group ? ': ' : '');
1151
1246
  }
1152
1247
  if (address.address) {
1153
1248
  let link = address.address;
@@ -19,8 +19,8 @@ module.exports = (input, options, callback) => {
19
19
  });
20
20
  }
21
21
 
22
- // MailParser may emit a non-fatal 'error' (eg. HTML too long to convert to text) and
23
- // still reach 'end', so settle only once: the first error or the result wins
22
+ // MailParser may emit a non-fatal 'error' and still reach 'end', so settle only once:
23
+ // the first error or the result wins
24
24
  let settled = false;
25
25
  let settle = callback;
26
26
  callback = (...args) => {
@@ -62,7 +62,7 @@ module.exports = (input, options, callback) => {
62
62
 
63
63
  if (data.type === 'text') {
64
64
  Object.keys(data).forEach(key => {
65
- if (['text', 'html', 'textAsHtml'].includes(key)) {
65
+ if (['text', 'html', 'textAsHtml', 'textConversionError'].includes(key)) {
66
66
  mail[key] = data[key];
67
67
  }
68
68
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailparser",
3
- "version": "3.9.39",
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.8",
26
+ "libmime": "5.4.9",
26
27
  "linkify-it": "5.0.2",
27
- "@zone-eu/mailsplit": "5.4.21",
28
- "nodemailer": "10.1.1",
28
+ "nodemailer": "10.1.2",
29
29
  "punycode.js": "2.3.1",
30
30
  "tlds": "1.261.0"
31
31
  },
@@ -1,5 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [10.1.2](https://github.com/nodemailer/nodemailer/compare/v10.1.1...v10.1.2) (2026-10-16)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * bound the work a message can make the mailer do and keep per-message auth off the transport's token endpoint ([e347fa2](https://github.com/nodemailer/nodemailer/commit/e347fa2260a7e342e772a7e187937aa87ac96364))
9
+
3
10
  ## [10.1.1](https://github.com/nodemailer/nodemailer/compare/v10.1.0...v10.1.1) (2026-10-14)
4
11
 
5
12
 
@@ -133,8 +133,9 @@ class DKIMSigner {
133
133
  }
134
134
  createWriteCache() {
135
135
  this.output.usingCache = true;
136
- // pipe remainings to cache file
137
- this.cache = node_fs_1.default.createWriteStream(this.cachePath);
136
+ // pipe remainings to cache file. It holds the message body and cacheDir is often a
137
+ // shared directory such as /tmp, so nobody but the owner gets to read it
138
+ this.cache = node_fs_1.default.createWriteStream(this.cachePath, { mode: 0o600 });
138
139
  this.cache.once('error', err => {
139
140
  // drain input
140
141
  this.relaxedBody.unpipe(this.cache);
@@ -27,6 +27,8 @@ export interface FetchOptions {
27
27
  } | undefined;
28
28
  /** Socket inactivity timeout in milliseconds, defaults to 60000, 0 disables it */
29
29
  timeout?: number | undefined;
30
+ /** Time limit for the whole request in milliseconds, redirects included, defaults to 120000, 0 disables it */
31
+ maxTime?: number | undefined;
30
32
  /** Maximum size of the (decoded) response body in bytes, defaults to 64 MB, Infinity disables the limit */
31
33
  maxBytes?: number | undefined;
32
34
  /** Maximum number of redirects to follow (default 5) */
@@ -37,6 +39,8 @@ export interface FetchOptions {
37
39
  redirects?: number | undefined;
38
40
  /** Response stream shared across redirects, set by nmfetch itself */
39
41
  fetchRes?: FetchResponse | undefined;
42
+ /** Timestamp the whole request has to be done by, shared across redirects, set by nmfetch itself */
43
+ deadline?: number | undefined;
40
44
  }
41
45
  /**
42
46
  * The stream nmfetch returns. The response body is piped into it, the status code and
@@ -50,6 +54,7 @@ declare function nmfetch(url: string, options?: FetchOptions): FetchResponse;
50
54
  declare namespace nmfetch {
51
55
  var Cookies: typeof import("./cookies.js").default;
52
56
  var DEFAULT_TIMEOUT: number;
57
+ var DEFAULT_MAX_TIME: number;
53
58
  }
54
59
  type CookiesJar = Cookies;
55
60
  /**
@@ -49,6 +49,10 @@ const objects_js_1 = require("../shared/objects.js");
49
49
  const MAX_REDIRECTS = 5;
50
50
  // a stalled server would otherwise hold the request, and whatever waits on it, open forever
51
51
  const DEFAULT_TIMEOUT = 60 * 1000;
52
+ // the inactivity timeout above is reset by every byte, so a server dripping one within each
53
+ // window could still hold the request, and the message being built from it, open for as
54
+ // long as it liked. The whole request, redirects included, has to be done within this
55
+ const DEFAULT_MAX_TIME = 2 * 60 * 1000;
52
56
  // the body is usually buffered in memory by the caller, so an unbounded download is refused
53
57
  const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
54
58
  // Only genuine TLS settings are taken from options.tls. That object reaches us straight
@@ -126,6 +130,12 @@ function nmfetch(url, options) {
126
130
  options.cookies = options.cookies || new cookies_js_1.default();
127
131
  options.redirects = options.redirects || 0;
128
132
  options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
133
+ if (!options.deadline) {
134
+ options.maxTime = typeof options.maxTime === 'number' && options.maxTime >= 0 ? options.maxTime : DEFAULT_MAX_TIME;
135
+ // the one deadline is shared with the requests the redirects lead to, every hop
136
+ // gets the time that is left rather than starting over
137
+ options.deadline = options.maxTime ? Date.now() + options.maxTime : 0;
138
+ }
129
139
  const fetchRes = options.fetchRes;
130
140
  const parsed = parseFetchUrl(url);
131
141
  if (!parsed) {
@@ -271,12 +281,22 @@ function nmfetch(url, options) {
271
281
  });
272
282
  return fetchRes;
273
283
  }
284
+ // the whole request has to be done by options.deadline, see maxTime. The timer is
285
+ // released on every way out of the request, so it never outlives it
286
+ let deadlineTimer;
287
+ const clearDeadline = () => {
288
+ if (deadlineTimer) {
289
+ clearTimeout(deadlineTimer);
290
+ deadlineTimer = undefined;
291
+ }
292
+ };
274
293
  // reports the first failure of this request on fetchRes and releases the request
275
294
  const fail = (err, sourceUrl = url) => {
276
295
  if (finished) {
277
296
  return;
278
297
  }
279
298
  finished = true;
299
+ clearDeadline();
280
300
  setFetchCode(err);
281
301
  err.sourceUrl = sourceUrl;
282
302
  fetchRes.emit('error', err);
@@ -286,10 +306,16 @@ function nmfetch(url, options) {
286
306
  if (timeout) {
287
307
  req.setTimeout(timeout, () => fail(new Error('Request Timeout')));
288
308
  }
309
+ if (options.deadline) {
310
+ deadlineTimer = setTimeout(() => fail(new Error('Request exceeded the allowed ' + options.maxTime + ' ms')), Math.max(options.deadline - Date.now(), 1));
311
+ // a pending deadline is no reason to keep the process alive
312
+ deadlineTimer.unref();
313
+ }
289
314
  req.on('error', (err) => fail(err));
290
315
  // a consumer that destroys the response stream before it ended does not want the rest of
291
316
  // the body, release the request now instead of when the timeout fires
292
317
  fetchRes.once('close', () => {
318
+ clearDeadline();
293
319
  if (finished || fetchRes.readableEnded) {
294
320
  return;
295
321
  }
@@ -358,6 +384,7 @@ function nmfetch(url, options) {
358
384
  // this request is done with, release it so its socket and timeout do not
359
385
  // outlive it and report a late error on the shared fetchRes
360
386
  finished = true;
387
+ clearDeadline();
361
388
  res.resume();
362
389
  req.abort();
363
390
  return nmfetch(redirectUrl, options);
@@ -368,6 +395,8 @@ function nmfetch(url, options) {
368
395
  return fail(new Error('Invalid status code ' + res.statusCode));
369
396
  }
370
397
  res.on('error', (err) => fail(err));
398
+ // the response is in, whatever is left of it is in memory and no longer on the network
399
+ res.once('end', () => clearDeadline());
371
400
  const maxBytes = typeof options.maxBytes === 'number' && options.maxBytes > 0 ? options.maxBytes : DEFAULT_MAX_BYTES;
372
401
  const source = inflate || res;
373
402
  let received = 0;
@@ -405,6 +434,7 @@ function nmfetch(url, options) {
405
434
  }
406
435
  nmfetch.Cookies = cookies_js_1.default;
407
436
  nmfetch.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
437
+ nmfetch.DEFAULT_MAX_TIME = DEFAULT_MAX_TIME;
408
438
  exports.default = nmfetch;
409
439
  module.exports = exports.default;
410
440
  Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
@@ -398,9 +398,12 @@ class Mail extends node_events_1.EventEmitter {
398
398
  return callback(err);
399
399
  }
400
400
  let cidCounter = 0;
401
+ // the media type is a bounded token: a quote, whitespace, '>' or '<' ends the
402
+ // attribute value and with it the type, so one img tag never scans ahead to a
403
+ // ';' that belongs to a later tag, which was quadratic
401
404
  html = (html || '')
402
405
  .toString()
403
- .replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
406
+ .replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;"'<>\s]{1,255});[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
404
407
  const cid = node_crypto_1.default.randomBytes(10).toString('hex') + '@localhost';
405
408
  if (!mail.data.attachments) {
406
409
  mail.data.attachments = [];
@@ -23,7 +23,11 @@ export interface SendMailOptions extends MailComposerOptions {
23
23
  dsn?: SMTPEnvelopeDsn | undefined;
24
24
  /** SMTP transports: RFC 8689, send the REQUIRETLS parameter with MAIL FROM */
25
25
  requireTLSExtensionEnabled?: boolean | undefined;
26
- /** SMTP transports: per-message authentication settings, used instead of the transport level auth */
26
+ /**
27
+ * SMTP transports: per-message authentication settings, used instead of the transport level auth.
28
+ * Only the credential fields are taken from it (type, user, pass, accessToken, refreshToken, expires),
29
+ * the token endpoint and client settings stay those of the transport
30
+ */
27
31
  auth?: SMTPTransportAuthOptions | undefined;
28
32
  /** SES transport: extra SendEmailCommand parameters merged into the API call */
29
33
  ses?: SESSendEmailRequest | undefined;
@@ -186,26 +186,38 @@ function encodeWord(data, mimeWordEncoding, maxLength) {
186
186
  */
187
187
  function encodeWords(value, mimeWordEncoding, maxLength, encodeAll) {
188
188
  maxLength = maxLength || 0;
189
- // find first word with a non-printable ascii or special symbol in it
190
- const firstMatch = value.match(/(?:^|\s)([^\s]*["\u0080-\uFFFF])/);
191
- if (!firstMatch) {
189
+ // find the first and the last character that needs encoding, a quote or anything outside
190
+ // printable ascii. A scan rather than a regex: matching the last such word with a
191
+ // backtracking pattern took quadratic time on a long word of non-ascii characters
192
+ let firstSpecial = -1;
193
+ let lastSpecial = -1;
194
+ for (let i = 0; i < value.length; i++) {
195
+ const code = value.charCodeAt(i);
196
+ if (code === 0x22 || code >= 0x80) {
197
+ if (firstSpecial < 0) {
198
+ firstSpecial = i;
199
+ }
200
+ lastSpecial = i;
201
+ }
202
+ }
203
+ if (firstSpecial < 0) {
192
204
  return value;
193
205
  }
194
206
  if (encodeAll) {
195
207
  // if it is requested to encode everything or the string contains something that resebles encoded word, then encode everything
196
208
  return encodeWord(value, mimeWordEncoding, maxLength);
197
209
  }
198
- // find the last word with a non-printable ascii in it
199
- const lastMatch = value.match(/(["\u0080-\uFFFF][^\s]*)[^"\u0080-\uFFFF]*$/);
200
- if (!lastMatch) {
201
- // should not happen
202
- return value;
210
+ // encode from the start of the word holding the first such character to the end of the
211
+ // word holding the last one, so the encoded words stay separated from the plain text by
212
+ // the whitespace that was already there
213
+ let startIndex = firstSpecial;
214
+ while (startIndex > 0 && !/\s/.test(value.charAt(startIndex - 1))) {
215
+ startIndex--;
216
+ }
217
+ let endIndex = lastSpecial + 1;
218
+ while (endIndex < value.length && !/\s/.test(value.charAt(endIndex))) {
219
+ endIndex++;
203
220
  }
204
- const startIndex = firstMatch.index +
205
- (firstMatch[0].match(/[^\s]/) || {
206
- index: 0
207
- }).index;
208
- const endIndex = lastMatch.index + (lastMatch[1] || '').length;
209
221
  return ((startIndex ? value.substr(0, startIndex) : '') +
210
222
  encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) +
211
223
  (endIndex < value.length ? value.substr(endIndex) : ''));