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 +18 -0
- package/lib/dkim/relaxed-body.js +1 -1
- package/lib/mime-funcs/index.js +21 -8
- package/lib/mime-node/index.js +10 -3
- package/lib/smtp-connection/index.js +23 -0
- package/lib/well-known/services.json +14 -0
- package/package.json +8 -8
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
|
|
package/lib/dkim/relaxed-body.js
CHANGED
|
@@ -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 || '
|
|
14
|
+
this.bodyHash = crypto.createHash(options.hashAlgo || 'sha256');
|
|
15
15
|
this.remainder = '';
|
|
16
16
|
this.byteLength = 0;
|
|
17
17
|
|
package/lib/mime-funcs/index.js
CHANGED
|
@@ -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-\
|
|
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 (/[\
|
|
84
|
-
//
|
|
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
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
//
|
|
604
|
-
|
|
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
|
package/lib/mime-node/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
30
|
+
"@aws-sdk/client-sesv2": "3.1096.0",
|
|
31
31
|
"bunyan": "1.8.15",
|
|
32
|
-
"c8": "
|
|
33
|
-
"eslint": "10.
|
|
32
|
+
"c8": "12.0.0",
|
|
33
|
+
"eslint": "10.8.0",
|
|
34
34
|
"eslint-config-prettier": "10.1.8",
|
|
35
|
-
"globals": "17.
|
|
35
|
+
"globals": "17.8.0",
|
|
36
36
|
"libbase64": "1.3.0",
|
|
37
|
-
"libmime": "5.
|
|
37
|
+
"libmime": "5.4.1",
|
|
38
38
|
"libqp": "2.1.1",
|
|
39
|
-
"prettier": "3.
|
|
39
|
+
"prettier": "3.9.6",
|
|
40
40
|
"proxy": "1.0.2",
|
|
41
41
|
"proxy-test-server": "1.0.0",
|
|
42
|
-
"smtp-server": "3.19.
|
|
42
|
+
"smtp-server": "3.19.2"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=6.0.0"
|