nodemailer 9.0.1 → 9.0.3
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.3](https://github.com/nodemailer/nodemailer/compare/v9.0.2...v9.0.3) (2026-06-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **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))
|
|
9
|
+
|
|
10
|
+
## [9.0.2](https://github.com/nodemailer/nodemailer/compare/v9.0.1...v9.0.2) (2026-06-29)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **addressparser:** keep operator chars inside an address-literal as text ([#1829](https://github.com/nodemailer/nodemailer/issues/1829)) ([9ba1064](https://github.com/nodemailer/nodemailer/commit/9ba1064f3b115cd60cb63aa954a3c0961e86fbb7))
|
|
16
|
+
* harden smtp-connection low-severity issues ([22ddcea](https://github.com/nodemailer/nodemailer/commit/22ddcea8ed043e4ea23b8971b75a2df196b5a581))
|
|
17
|
+
* harden smtp-connection response parsing and socket lifecycle ([68860b9](https://github.com/nodemailer/nodemailer/commit/68860b94311b5b6837754e5e790f186ca8a00b70))
|
|
18
|
+
* prevent SES transport callback double-invocation and hang on sync errors ([#1831](https://github.com/nodemailer/nodemailer/issues/1831)) ([9517bc5](https://github.com/nodemailer/nodemailer/commit/9517bc5dc94e77907bb157e3466bf73a2b327f5c))
|
|
19
|
+
* reject CRLF in HTTP proxy CONNECT destination to prevent request injection ([6347b47](https://github.com/nodemailer/nodemailer/commit/6347b47c7d12f9d3acf53d391b921e836f400640))
|
|
20
|
+
|
|
3
21
|
## [9.0.1](https://github.com/nodemailer/nodemailer/compare/v9.0.0...v9.0.1) (2026-06-17)
|
|
4
22
|
|
|
5
23
|
|
|
@@ -181,6 +181,7 @@ class Tokenizer {
|
|
|
181
181
|
this.operatorExpecting = '';
|
|
182
182
|
this.node = null;
|
|
183
183
|
this.escaped = false;
|
|
184
|
+
this.inDomainLiteral = false;
|
|
184
185
|
|
|
185
186
|
this.list = [];
|
|
186
187
|
/**
|
|
@@ -232,6 +233,21 @@ class Tokenizer {
|
|
|
232
233
|
* @param {String} chr Character from the address field
|
|
233
234
|
*/
|
|
234
235
|
checkChar(chr, nextChr) {
|
|
236
|
+
// Track RFC 5322 domain-literals ("[" *dtext "]"). Operator characters such
|
|
237
|
+
// as the ":" of an IPv6 address-literal (user@[IPv6:2001:db8::1]) are dtext
|
|
238
|
+
// and must not be treated as the group delimiter while inside the brackets.
|
|
239
|
+
// Quoted strings and comments are handled separately via operatorExpecting,
|
|
240
|
+
// so only enter this state when no operator is open. The list separators ","
|
|
241
|
+
// and ";" are the exception: they always end the literal (and split the
|
|
242
|
+
// address list) so that an unclosed "[" cannot swallow later recipients.
|
|
243
|
+
if (!this.escaped && !this.operatorExpecting) {
|
|
244
|
+
if (!this.inDomainLiteral && chr === '[') {
|
|
245
|
+
this.inDomainLiteral = true;
|
|
246
|
+
} else if (this.inDomainLiteral && (chr === ']' || chr === ',' || chr === ';')) {
|
|
247
|
+
this.inDomainLiteral = false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
235
251
|
if (this.escaped) {
|
|
236
252
|
// ignore next condition blocks
|
|
237
253
|
} else if (chr === this.operatorExpecting) {
|
|
@@ -250,7 +266,7 @@ class Tokenizer {
|
|
|
250
266
|
this.escaped = false;
|
|
251
267
|
|
|
252
268
|
return;
|
|
253
|
-
} else if (!this.operatorExpecting && chr in this.operators) {
|
|
269
|
+
} else if (!this.operatorExpecting && !this.inDomainLiteral && chr in this.operators) {
|
|
254
270
|
this.node = {
|
|
255
271
|
type: 'operator',
|
|
256
272
|
value: chr
|
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
|
|
|
@@ -43,11 +43,12 @@ class SESTransport extends EventEmitter {
|
|
|
43
43
|
|
|
44
44
|
getRegion(cb) {
|
|
45
45
|
if (this.ses.sesClient.config && typeof this.ses.sesClient.config.region === 'function') {
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
// Resolve the region provider. Use the two-argument form of then() so that a
|
|
47
|
+
// synchronous throw from cb is not recaught here and used to invoke cb a second time.
|
|
48
|
+
return this.ses.sesClient.config.region().then(
|
|
49
|
+
region => cb(null, region),
|
|
50
|
+
err => cb(err)
|
|
51
|
+
);
|
|
51
52
|
}
|
|
52
53
|
return cb(null, false);
|
|
53
54
|
}
|
|
@@ -150,8 +151,27 @@ class SESTransport extends EventEmitter {
|
|
|
150
151
|
region = 'us-east-1';
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
let sendPromise;
|
|
155
|
+
try {
|
|
156
|
+
// command construction or dispatch can throw synchronously on a
|
|
157
|
+
// misconfigured SDK; surface it as a single error callback instead
|
|
158
|
+
// of letting it escape into getRegion's promise chain
|
|
159
|
+
const command = new this.ses.SendEmailCommand(sesMessage);
|
|
160
|
+
sendPromise = this.ses.sesClient.send(command);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
tagSesError(err);
|
|
163
|
+
this.logger.error(
|
|
164
|
+
{
|
|
165
|
+
err,
|
|
166
|
+
tnx: 'send'
|
|
167
|
+
},
|
|
168
|
+
'Send error for %s: %s',
|
|
169
|
+
messageId,
|
|
170
|
+
err.message
|
|
171
|
+
);
|
|
172
|
+
setImmediate(() => callback(err));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
155
175
|
|
|
156
176
|
sendPromise
|
|
157
177
|
.then(data => {
|
|
@@ -159,7 +179,7 @@ class SESTransport extends EventEmitter {
|
|
|
159
179
|
region = 'email';
|
|
160
180
|
}
|
|
161
181
|
|
|
162
|
-
|
|
182
|
+
const info = {
|
|
163
183
|
envelope: {
|
|
164
184
|
from: envelope.from,
|
|
165
185
|
to: envelope.to
|
|
@@ -167,7 +187,11 @@ class SESTransport extends EventEmitter {
|
|
|
167
187
|
messageId: '<' + data.MessageId + (!/@/.test(data.MessageId) ? '@' + region + '.amazonses.com' : '') + '>',
|
|
168
188
|
response: data.MessageId,
|
|
169
189
|
raw
|
|
170
|
-
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// invoke the callback outside the promise chain so a throw from it
|
|
193
|
+
// is not recaught by .catch() and used to call it a second time
|
|
194
|
+
setImmediate(() => callback(null, info));
|
|
171
195
|
})
|
|
172
196
|
.catch(err => {
|
|
173
197
|
tagSesError(err);
|
|
@@ -180,7 +204,7 @@ class SESTransport extends EventEmitter {
|
|
|
180
204
|
messageId,
|
|
181
205
|
err.message
|
|
182
206
|
);
|
|
183
|
-
callback(err);
|
|
207
|
+
setImmediate(() => callback(err));
|
|
184
208
|
});
|
|
185
209
|
});
|
|
186
210
|
})
|
|
@@ -222,10 +246,16 @@ class SESTransport extends EventEmitter {
|
|
|
222
246
|
// the region value is not used for anything when verifying, but the lookup
|
|
223
247
|
// exercises the client configuration the same way as send() does
|
|
224
248
|
this.getRegion(() => {
|
|
225
|
-
|
|
226
|
-
|
|
249
|
+
let sendPromise;
|
|
250
|
+
try {
|
|
251
|
+
const command = new this.ses.SendEmailCommand(sesMessage);
|
|
252
|
+
sendPromise = this.ses.sesClient.send(command);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
setImmediate(() => cb(err));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
227
257
|
|
|
228
|
-
sendPromise.then(() => cb(null)).catch(err => cb(err));
|
|
258
|
+
sendPromise.then(() => setImmediate(() => cb(null))).catch(err => setImmediate(() => cb(err)));
|
|
229
259
|
});
|
|
230
260
|
|
|
231
261
|
return promise;
|
|
@@ -9,6 +9,10 @@ const tls = require('tls');
|
|
|
9
9
|
const urllib = require('../shared/url');
|
|
10
10
|
const errors = require('../errors');
|
|
11
11
|
|
|
12
|
+
// Cap the CONNECT response we buffer before the header terminator, so a proxy that
|
|
13
|
+
// never sends \r\n\r\n cannot grow memory unboundedly before the socket times out.
|
|
14
|
+
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* Establishes proxied connection to destinationPort
|
|
14
18
|
*
|
|
@@ -29,6 +33,16 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
|
|
29
33
|
}
|
|
30
34
|
tlsOptions = tlsOptions || {};
|
|
31
35
|
|
|
36
|
+
// Reject CRLF in the destination before it reaches the CONNECT request line
|
|
37
|
+
// and Host header. A tainted host/port could otherwise inject additional
|
|
38
|
+
// request headers into the proxy connection (HTTP request splitting).
|
|
39
|
+
destinationPort = Number(destinationPort) || 0;
|
|
40
|
+
if (!destinationPort || /[\r\n]/.test(destinationHost)) {
|
|
41
|
+
const err = new Error('Invalid proxy destination');
|
|
42
|
+
err.code = errors.EPROXY;
|
|
43
|
+
return setImmediate(() => callback(err));
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
const proxy = urllib.parse(proxyUrl);
|
|
33
47
|
|
|
34
48
|
const connectOptions = {
|
|
@@ -140,6 +154,13 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
|
|
140
154
|
|
|
141
155
|
return callback(null, socket);
|
|
142
156
|
}
|
|
157
|
+
|
|
158
|
+
if (headers.length > MAX_RESPONSE_HEADER_BYTES) {
|
|
159
|
+
socket.removeListener('data', onSocketData);
|
|
160
|
+
const err = new Error('Proxy response headers too large');
|
|
161
|
+
err.code = errors.EPROXY;
|
|
162
|
+
return tempSocketErr(err);
|
|
163
|
+
}
|
|
143
164
|
};
|
|
144
165
|
socket.on('data', onSocketData);
|
|
145
166
|
});
|
|
@@ -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();
|
|
@@ -365,6 +379,14 @@ class SMTPConnection extends EventEmitter {
|
|
|
365
379
|
* @param {Boolean} secure Whether to use TLS
|
|
366
380
|
*/
|
|
367
381
|
_connectToHost(opts, secure) {
|
|
382
|
+
// If the client was closed while DNS resolution was in flight, do not open
|
|
383
|
+
// a socket here: close() ran with this._socket still unset and so had
|
|
384
|
+
// nothing to tear down, and _onConnect's remedial close() is a no-op once
|
|
385
|
+
// _closing is set — the freshly connected socket would leak.
|
|
386
|
+
if (this._destroyed || this._closing) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
368
390
|
this._connectionAttemptId++;
|
|
369
391
|
const currentAttemptId = this._connectionAttemptId;
|
|
370
392
|
|
|
@@ -431,6 +453,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
431
453
|
if (this._socket) {
|
|
432
454
|
try {
|
|
433
455
|
this._socket.removeListener('error', this._onConnectionSocketError);
|
|
456
|
+
// Absorb any late teardown error (e.g. a TLS fallback socket emitting
|
|
457
|
+
// after destroy), mirroring the guard used in close()
|
|
458
|
+
this._socket.on('error', TEARDOWN_NOOP);
|
|
434
459
|
this._socket.destroy();
|
|
435
460
|
} catch (_E) {
|
|
436
461
|
// ignore
|
|
@@ -757,6 +782,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
757
782
|
* @param {Function} callback Callback to return once connection is reset
|
|
758
783
|
*/
|
|
759
784
|
reset(callback) {
|
|
785
|
+
const isDestroyedMessage = this._isDestroyedMessage('reset');
|
|
786
|
+
if (isDestroyedMessage) {
|
|
787
|
+
return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
|
|
788
|
+
}
|
|
789
|
+
|
|
760
790
|
this._sendCommand('RSET');
|
|
761
791
|
this._responseActions.push(str => {
|
|
762
792
|
if (str.charAt(0) !== '2') {
|
|
@@ -805,6 +835,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
805
835
|
this._socket.removeListener('end', this._onSocketEnd);
|
|
806
836
|
// Switch from connection-phase error handler to normal error handler
|
|
807
837
|
this._socket.removeListener('error', this._onConnectionSocketError);
|
|
838
|
+
// _upgradeConnection (options.connection + secure) may already have attached
|
|
839
|
+
// the normal handler; remove it first so we never end up with a duplicate
|
|
840
|
+
this._socket.removeListener('error', this._onSocketError);
|
|
808
841
|
|
|
809
842
|
this._socket.on('error', this._onSocketError);
|
|
810
843
|
this._socket.on('data', this._onSocketData);
|
|
@@ -994,6 +1027,8 @@ class SMTPConnection extends EventEmitter {
|
|
|
994
1027
|
return;
|
|
995
1028
|
}
|
|
996
1029
|
this._destroyed = true;
|
|
1030
|
+
// keep the documented public flag in sync with the private state
|
|
1031
|
+
this.destroyed = true;
|
|
997
1032
|
this.emit('end');
|
|
998
1033
|
}
|
|
999
1034
|
|
|
@@ -1004,6 +1039,15 @@ class SMTPConnection extends EventEmitter {
|
|
|
1004
1039
|
* has been secured
|
|
1005
1040
|
*/
|
|
1006
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
|
+
|
|
1007
1051
|
// do not remove all listeners or it breaks node v0.10 as there's
|
|
1008
1052
|
// apparently a 'finish' event set that would be cleared as well
|
|
1009
1053
|
|
|
@@ -1032,6 +1076,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
1032
1076
|
socketPlain.removeListener('close', this._onSocketClose);
|
|
1033
1077
|
socketPlain.removeListener('end', this._onSocketEnd);
|
|
1034
1078
|
socketPlain.removeListener('error', this._onSocketError);
|
|
1079
|
+
// the connection-phase handler is attached when upgrading a pre-opened
|
|
1080
|
+
// options.connection socket; strip it so nothing lingers on the plain socket
|
|
1081
|
+
socketPlain.removeListener('error', this._onConnectionSocketError);
|
|
1035
1082
|
};
|
|
1036
1083
|
|
|
1037
1084
|
this.upgrading = true;
|
|
@@ -1064,18 +1111,27 @@ class SMTPConnection extends EventEmitter {
|
|
|
1064
1111
|
|
|
1065
1112
|
/**
|
|
1066
1113
|
* Processes queued responses from the server
|
|
1067
|
-
*
|
|
1068
|
-
* @param {Boolean} force If true, ignores _processing flag
|
|
1069
1114
|
*/
|
|
1070
1115
|
_processResponse() {
|
|
1071
1116
|
if (!this._responseQueue.length) {
|
|
1072
1117
|
return false;
|
|
1073
1118
|
}
|
|
1074
1119
|
|
|
1075
|
-
|
|
1120
|
+
const raw = (this._responseQueue.shift() || '').toString();
|
|
1121
|
+
|
|
1122
|
+
// Skip unexpected empty lines without consuming a response action or
|
|
1123
|
+
// overwriting lastServerResponse; reprocess whatever else is queued.
|
|
1124
|
+
if (!raw.trim()) {
|
|
1125
|
+
setImmediate(() => this._processResponse());
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
let str = (this.lastServerResponse = decodeServerResponse(raw));
|
|
1076
1130
|
|
|
1077
1131
|
if (/^\d+-/.test(str.split('\n').pop())) {
|
|
1078
|
-
//
|
|
1132
|
+
// last line is still a continuation: put the partial response back on the
|
|
1133
|
+
// queue and wait for the rest rather than dropping it
|
|
1134
|
+
this._responseQueue.unshift(raw);
|
|
1079
1135
|
return;
|
|
1080
1136
|
}
|
|
1081
1137
|
|
|
@@ -1088,11 +1144,6 @@ class SMTPConnection extends EventEmitter {
|
|
|
1088
1144
|
);
|
|
1089
1145
|
}
|
|
1090
1146
|
|
|
1091
|
-
if (!str.trim()) {
|
|
1092
|
-
// skip unexpected empty lines
|
|
1093
|
-
setImmediate(() => this._processResponse());
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
1147
|
const action = this._responseActions.shift();
|
|
1097
1148
|
|
|
1098
1149
|
if (typeof action === 'function') {
|
|
@@ -1189,6 +1240,23 @@ class SMTPConnection extends EventEmitter {
|
|
|
1189
1240
|
}
|
|
1190
1241
|
}
|
|
1191
1242
|
|
|
1243
|
+
// RFC 8689: validate REQUIRETLS eligibility before queuing the MAIL FROM
|
|
1244
|
+
// response action, so a rejection here cannot leave an orphaned action in
|
|
1245
|
+
// _responseActions (which would consume the next reply and desync a reused
|
|
1246
|
+
// connection).
|
|
1247
|
+
if (this._envelope.requireTLSExtensionEnabled) {
|
|
1248
|
+
if (!this.secure) {
|
|
1249
|
+
return callback(
|
|
1250
|
+
this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
if (!this._supportedExtensions.includes('REQUIRETLS')) {
|
|
1254
|
+
return callback(
|
|
1255
|
+
this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1192
1260
|
this._responseActions.push(str => {
|
|
1193
1261
|
this._actionMAIL(str, callback);
|
|
1194
1262
|
});
|
|
@@ -1225,20 +1293,10 @@ class SMTPConnection extends EventEmitter {
|
|
|
1225
1293
|
}
|
|
1226
1294
|
}
|
|
1227
1295
|
|
|
1228
|
-
// RFC 8689:
|
|
1229
|
-
//
|
|
1230
|
-
//
|
|
1296
|
+
// RFC 8689: append the REQUIRETLS keyword to MAIL FROM. Eligibility
|
|
1297
|
+
// (TLS connection + server support) was already validated above, before
|
|
1298
|
+
// the response action was queued.
|
|
1231
1299
|
if (this._envelope.requireTLSExtensionEnabled) {
|
|
1232
|
-
if (!this.secure) {
|
|
1233
|
-
return callback(
|
|
1234
|
-
this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
|
1235
|
-
);
|
|
1236
|
-
}
|
|
1237
|
-
if (!this._supportedExtensions.includes('REQUIRETLS')) {
|
|
1238
|
-
return callback(
|
|
1239
|
-
this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
|
1240
|
-
);
|
|
1241
|
-
}
|
|
1242
1300
|
args.push('REQUIRETLS');
|
|
1243
1301
|
}
|
|
1244
1302
|
|