nodemailer 9.0.2 → 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,23 @@
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
+
14
+ ## [9.0.3](https://github.com/nodemailer/nodemailer/compare/v9.0.2...v9.0.3) (2026-06-30)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **smtp-connection:** harden STARTTLS upgrade and secure socket handling ([#1835](https://github.com/nodemailer/nodemailer/issues/1835)) ([07d8253](https://github.com/nodemailer/nodemailer/commit/07d8253326ecefff9f7d92c157429ce8bc7335f8))
20
+
3
21
  ## [9.0.2](https://github.com/nodemailer/nodemailer/compare/v9.0.1...v9.0.2) (2026-06-29)
4
22
 
5
23
 
@@ -11,7 +11,7 @@ class RelaxedBody extends Transform {
11
11
  options = options || {};
12
12
  this.chunkBuffer = [];
13
13
  this.chunkBufferLen = 0;
14
- this.bodyHash = crypto.createHash(options.hashAlgo || 'sha1');
14
+ this.bodyHash = crypto.createHash(options.hashAlgo || 'sha256');
15
15
  this.remainder = '';
16
16
  this.byteLength = 0;
17
17
 
@@ -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
  }
@@ -298,6 +298,20 @@ class SMTPConnection extends EventEmitter {
298
298
  try {
299
299
  this._socket.connect(this.port, this.host, () => {
300
300
  this._socket.setKeepAlive(true);
301
+
302
+ // a `secure` connection over a caller-provided socket must still
303
+ // perform the TLS handshake, otherwise AUTH and the message body
304
+ // would be sent in cleartext despite the caller requesting TLS
305
+ if (this.secureConnection && !this.alreadySecured) {
306
+ return this._upgradeConnection(err => {
307
+ if (err) {
308
+ this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN');
309
+ return;
310
+ }
311
+ this._onConnect();
312
+ });
313
+ }
314
+
301
315
  this._onConnect();
302
316
  });
303
317
  this._setupConnectionHandlers();
@@ -1025,6 +1039,15 @@ class SMTPConnection extends EventEmitter {
1025
1039
  * has been secured
1026
1040
  */
1027
1041
  _upgradeConnection(callback) {
1042
+ // RFC 3207 section 6: the client MUST discard any knowledge obtained from
1043
+ // the server that was not received over the TLS-protected session. Drop any
1044
+ // buffered input received before the handshake so a man-in-the-middle cannot
1045
+ // inject plaintext bytes after the "220" reply (e.g. a CRLF-free fragment that
1046
+ // would otherwise be prepended to the first post-TLS response and parsed as
1047
+ // part of the secured EHLO capabilities). STARTTLS response injection.
1048
+ this._remainder = '';
1049
+ this._responseQueue = [];
1050
+
1028
1051
  // do not remove all listeners or it breaks node v0.10 as there's
1029
1052
  // apparently a 'finish' event set that would be cleared as well
1030
1053
 
@@ -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.2",
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"