nodemailer 9.0.3 → 9.0.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [9.0.4](https://github.com/nodemailer/nodemailer/compare/v9.0.3...v9.0.4) (2026-08-04)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **mime-funcs:** do not let an unpaired surrogate consume the next character ([9797f7f](https://github.com/nodemailer/nodemailer/commit/9797f7f57d47b1e27d8ae050550ba93600dbf9f4))
9
+ * **mime-funcs:** keep any surrogate pair intact when chunking base64 mime words ([#1838](https://github.com/nodemailer/nodemailer/issues/1838)) ([5bd3a65](https://github.com/nodemailer/nodemailer/commit/5bd3a657be2d12df1ac2838a1673cb4885e99016))
10
+ * **mime-funcs:** percent encode unpaired surrogates in header parameter values ([78f4aa2](https://github.com/nodemailer/nodemailer/commit/78f4aa253d0ebb9c5ba91301adacef5fb6cf5493))
11
+ * **mime-node:** escape backslash and quote in the Content-Type name parameter ([#1837](https://github.com/nodemailer/nodemailer/issues/1837)) ([adcfc4f](https://github.com/nodemailer/nodemailer/commit/adcfc4f46445edc8a14136b2bf3f775943425c13))
12
+ * **mime:** encode HT/CR/LF in header parameter values instead of quoting them ([#1840](https://github.com/nodemailer/nodemailer/issues/1840)) ([5bc9cab](https://github.com/nodemailer/nodemailer/commit/5bc9cabddcb8d18d16244701ae9facc8fba942a3))
13
+
3
14
  ## [9.0.3](https://github.com/nodemailer/nodemailer/compare/v9.0.2...v9.0.3) (2026-06-30)
4
15
 
5
16
 
@@ -10,11 +10,18 @@ module.exports = {
10
10
  /**
11
11
  * Checks if a value is plaintext string (uses only printable 7bit chars)
12
12
  *
13
+ * When isParam is set the value is destined for a header parameter, so HT, CR and LF
14
+ * are not plaintext either: a header parameter has no way to carry them. HT is a valid
15
+ * fold point, so folding and unfolding a header would rewrite it as a space, and CR/LF
16
+ * cannot appear in a header value at all. Such values have to go through the rfc2231
17
+ * parameter continuation encoding instead, the same way a quote already does.
18
+ *
13
19
  * @param {String} value String to be tested
20
+ * @param {Boolean} [isParam] Set to true if the value is a header parameter value
14
21
  * @returns {Boolean} true if it is a plaintext string
15
22
  */
16
23
  isPlainText(value, isParam) {
17
- const re = isParam ? /[\x00-\x08\x0b\x0c\x0e-\x1f"\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/;
24
+ const re = isParam ? /[\x00-\x1f"\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/;
18
25
  return typeof value === 'string' && !re.test(value);
19
26
  },
20
27
 
@@ -80,8 +87,10 @@ module.exports = {
80
87
  for (let i = 0, len = encodedStr.length; i < len; i++) {
81
88
  let chr = encodedStr.charAt(i);
82
89
 
83
- if (/[\ud83c\ud83d\ud83e]/.test(chr) && i < len - 1) {
84
- // composite emoji byte, so add the next byte as well
90
+ if (/[\ud800-\udbff]/.test(chr) && /[\udc00-\udfff]/.test(encodedStr.charAt(i + 1))) {
91
+ // leading surrogate, so add the trailing surrogate as well
92
+ // an unpaired one must not swallow the next unit, that would destroy
93
+ // a valid pair following it
85
94
  chr += encodedStr.charAt(++i);
86
95
  }
87
96
 
@@ -208,7 +217,7 @@ module.exports = {
208
217
  buildHeaderParam(key, data, maxLength) {
209
218
  const list = [];
210
219
  let encodedStr = typeof data === 'string' ? data : (data || '').toString();
211
- let chr, ord;
220
+ let chr;
212
221
  let line;
213
222
  let startPos = 0;
214
223
  let i, len;
@@ -245,8 +254,9 @@ module.exports = {
245
254
  const encodedStrArr = [];
246
255
  for (i = 0, len = encodedStr.length; i < len; i++) {
247
256
  chr = encodedStr.charAt(i);
248
- ord = chr.charCodeAt(0);
249
- if (ord >= 0xd800 && ord <= 0xdbff && i < len - 1) {
257
+ if (/[\ud800-\udbff]/.test(chr) && /[\udc00-\udfff]/.test(encodedStr.charAt(i + 1))) {
258
+ // an unpaired leading surrogate must not consume the next unit, that
259
+ // would tear apart a valid pair following it
250
260
  chr += encodedStr.charAt(i + 1);
251
261
  encodedStrArr.push(chr);
252
262
  i++;
@@ -600,8 +610,11 @@ module.exports = {
600
610
  // might throw if we try to encode invalid sequences, eg. partial emoji
601
611
  str = encodeURIComponent(str);
602
612
  } catch (_E) {
603
- // should never run
604
- return str.replace(/[^\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]+/g, '');
613
+ // an unpaired surrogate has no utf-8 representation, so run the value through a
614
+ // utf-8 roundtrip to get the same U+FFFD every other encoder here produces and
615
+ // retry. the value must never come back unencoded, it goes into a header parameter
616
+ // where a bare quote or semicolon would break it out into a parameter of its own
617
+ str = encodeURIComponent(Buffer.from(str, 'utf-8').toString('utf-8'));
605
618
  }
606
619
 
607
620
  // ensure chars that are not handled by encodeURICompent are converted as well
@@ -568,11 +568,18 @@ class MimeNode {
568
568
  // add support for non-compliant clients like QQ webmail
569
569
  // we can't build the value with buildHeaderValue as the value is non standard and
570
570
  // would be converted to parameter continuation encoding that we do not want
571
- param = this._encodeWords(this.filename);
571
+ // control chars can not be quoted here: HT is a fold point that unfolding would
572
+ // turn into a space and CR/LF can not appear in a header at all, so force the
573
+ // mime encoded word that a non-ascii filename would get anyway
574
+ param = /[\x00-\x1f]/.test(this.filename)
575
+ ? mimeFuncs.encodeWord(this.filename, this._getTextEncoding(this.filename), 52)
576
+ : this._encodeWords(this.filename);
572
577
 
573
578
  if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
574
- // include value in quotes if needed
575
- param = '"' + param + '"';
579
+ // include value in quotes if needed, escaping backslashes and quotes as
580
+ // quoted-pairs exactly like buildHeaderValue does for filename=, otherwise
581
+ // a trailing backslash would escape the closing quote
582
+ param = JSON.stringify(param);
576
583
  }
577
584
  value += '; name=' + param;
578
585
  }
@@ -577,6 +577,20 @@
577
577
  "port": 587
578
578
  },
579
579
 
580
+ "TurboSMTP": {
581
+ "description": "TurboSMTP",
582
+ "host": "pro.turbo-smtp.com",
583
+ "port": 465,
584
+ "secure": true
585
+ },
586
+
587
+ "TurboSMTP-EU": {
588
+ "description": "TurboSMTP (EU region)",
589
+ "host": "pro.eu.turbo-smtp.com",
590
+ "port": 465,
591
+ "secure": true
592
+ },
593
+
580
594
  "Tutanota": {
581
595
  "description": "Tutanota (Tuta Mail)",
582
596
  "domains": ["tutanota.com", "tuta.com", "tutanota.de", "tuta.io"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "9.0.3",
3
+ "version": "9.0.4",
4
4
  "description": "Easy as cake e-mail sending from your Node.js applications",
5
5
  "main": "lib/nodemailer.js",
6
6
  "scripts": {
@@ -27,19 +27,19 @@
27
27
  },
28
28
  "homepage": "https://nodemailer.com/",
29
29
  "devDependencies": {
30
- "@aws-sdk/client-sesv2": "3.1068.0",
30
+ "@aws-sdk/client-sesv2": "3.1096.0",
31
31
  "bunyan": "1.8.15",
32
- "c8": "11.0.0",
33
- "eslint": "10.5.0",
32
+ "c8": "12.0.0",
33
+ "eslint": "10.8.0",
34
34
  "eslint-config-prettier": "10.1.8",
35
- "globals": "17.6.0",
35
+ "globals": "17.8.0",
36
36
  "libbase64": "1.3.0",
37
- "libmime": "5.3.8",
37
+ "libmime": "5.4.1",
38
38
  "libqp": "2.1.1",
39
- "prettier": "3.8.4",
39
+ "prettier": "3.9.6",
40
40
  "proxy": "1.0.2",
41
41
  "proxy-test-server": "1.0.0",
42
- "smtp-server": "3.19.0"
42
+ "smtp-server": "3.19.2"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=6.0.0"