nodemailer 9.0.1 → 9.0.2
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 +11 -0
- package/lib/addressparser/index.js +17 -1
- package/lib/ses-transport/index.js +43 -13
- package/lib/smtp-connection/http-proxy-client.js +21 -0
- package/lib/smtp-connection/index.js +57 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [9.0.2](https://github.com/nodemailer/nodemailer/compare/v9.0.1...v9.0.2) (2026-06-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **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))
|
|
9
|
+
* harden smtp-connection low-severity issues ([22ddcea](https://github.com/nodemailer/nodemailer/commit/22ddcea8ed043e4ea23b8971b75a2df196b5a581))
|
|
10
|
+
* harden smtp-connection response parsing and socket lifecycle ([68860b9](https://github.com/nodemailer/nodemailer/commit/68860b94311b5b6837754e5e790f186ca8a00b70))
|
|
11
|
+
* 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))
|
|
12
|
+
* reject CRLF in HTTP proxy CONNECT destination to prevent request injection ([6347b47](https://github.com/nodemailer/nodemailer/commit/6347b47c7d12f9d3acf53d391b921e836f400640))
|
|
13
|
+
|
|
3
14
|
## [9.0.1](https://github.com/nodemailer/nodemailer/compare/v9.0.0...v9.0.1) (2026-06-17)
|
|
4
15
|
|
|
5
16
|
|
|
@@ -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
|
|
@@ -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
|
});
|
|
@@ -365,6 +365,14 @@ class SMTPConnection extends EventEmitter {
|
|
|
365
365
|
* @param {Boolean} secure Whether to use TLS
|
|
366
366
|
*/
|
|
367
367
|
_connectToHost(opts, secure) {
|
|
368
|
+
// If the client was closed while DNS resolution was in flight, do not open
|
|
369
|
+
// a socket here: close() ran with this._socket still unset and so had
|
|
370
|
+
// nothing to tear down, and _onConnect's remedial close() is a no-op once
|
|
371
|
+
// _closing is set — the freshly connected socket would leak.
|
|
372
|
+
if (this._destroyed || this._closing) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
368
376
|
this._connectionAttemptId++;
|
|
369
377
|
const currentAttemptId = this._connectionAttemptId;
|
|
370
378
|
|
|
@@ -431,6 +439,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
431
439
|
if (this._socket) {
|
|
432
440
|
try {
|
|
433
441
|
this._socket.removeListener('error', this._onConnectionSocketError);
|
|
442
|
+
// Absorb any late teardown error (e.g. a TLS fallback socket emitting
|
|
443
|
+
// after destroy), mirroring the guard used in close()
|
|
444
|
+
this._socket.on('error', TEARDOWN_NOOP);
|
|
434
445
|
this._socket.destroy();
|
|
435
446
|
} catch (_E) {
|
|
436
447
|
// ignore
|
|
@@ -757,6 +768,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
757
768
|
* @param {Function} callback Callback to return once connection is reset
|
|
758
769
|
*/
|
|
759
770
|
reset(callback) {
|
|
771
|
+
const isDestroyedMessage = this._isDestroyedMessage('reset');
|
|
772
|
+
if (isDestroyedMessage) {
|
|
773
|
+
return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
|
|
774
|
+
}
|
|
775
|
+
|
|
760
776
|
this._sendCommand('RSET');
|
|
761
777
|
this._responseActions.push(str => {
|
|
762
778
|
if (str.charAt(0) !== '2') {
|
|
@@ -805,6 +821,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
805
821
|
this._socket.removeListener('end', this._onSocketEnd);
|
|
806
822
|
// Switch from connection-phase error handler to normal error handler
|
|
807
823
|
this._socket.removeListener('error', this._onConnectionSocketError);
|
|
824
|
+
// _upgradeConnection (options.connection + secure) may already have attached
|
|
825
|
+
// the normal handler; remove it first so we never end up with a duplicate
|
|
826
|
+
this._socket.removeListener('error', this._onSocketError);
|
|
808
827
|
|
|
809
828
|
this._socket.on('error', this._onSocketError);
|
|
810
829
|
this._socket.on('data', this._onSocketData);
|
|
@@ -994,6 +1013,8 @@ class SMTPConnection extends EventEmitter {
|
|
|
994
1013
|
return;
|
|
995
1014
|
}
|
|
996
1015
|
this._destroyed = true;
|
|
1016
|
+
// keep the documented public flag in sync with the private state
|
|
1017
|
+
this.destroyed = true;
|
|
997
1018
|
this.emit('end');
|
|
998
1019
|
}
|
|
999
1020
|
|
|
@@ -1032,6 +1053,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
1032
1053
|
socketPlain.removeListener('close', this._onSocketClose);
|
|
1033
1054
|
socketPlain.removeListener('end', this._onSocketEnd);
|
|
1034
1055
|
socketPlain.removeListener('error', this._onSocketError);
|
|
1056
|
+
// the connection-phase handler is attached when upgrading a pre-opened
|
|
1057
|
+
// options.connection socket; strip it so nothing lingers on the plain socket
|
|
1058
|
+
socketPlain.removeListener('error', this._onConnectionSocketError);
|
|
1035
1059
|
};
|
|
1036
1060
|
|
|
1037
1061
|
this.upgrading = true;
|
|
@@ -1064,18 +1088,27 @@ class SMTPConnection extends EventEmitter {
|
|
|
1064
1088
|
|
|
1065
1089
|
/**
|
|
1066
1090
|
* Processes queued responses from the server
|
|
1067
|
-
*
|
|
1068
|
-
* @param {Boolean} force If true, ignores _processing flag
|
|
1069
1091
|
*/
|
|
1070
1092
|
_processResponse() {
|
|
1071
1093
|
if (!this._responseQueue.length) {
|
|
1072
1094
|
return false;
|
|
1073
1095
|
}
|
|
1074
1096
|
|
|
1075
|
-
|
|
1097
|
+
const raw = (this._responseQueue.shift() || '').toString();
|
|
1098
|
+
|
|
1099
|
+
// Skip unexpected empty lines without consuming a response action or
|
|
1100
|
+
// overwriting lastServerResponse; reprocess whatever else is queued.
|
|
1101
|
+
if (!raw.trim()) {
|
|
1102
|
+
setImmediate(() => this._processResponse());
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
let str = (this.lastServerResponse = decodeServerResponse(raw));
|
|
1076
1107
|
|
|
1077
1108
|
if (/^\d+-/.test(str.split('\n').pop())) {
|
|
1078
|
-
//
|
|
1109
|
+
// last line is still a continuation: put the partial response back on the
|
|
1110
|
+
// queue and wait for the rest rather than dropping it
|
|
1111
|
+
this._responseQueue.unshift(raw);
|
|
1079
1112
|
return;
|
|
1080
1113
|
}
|
|
1081
1114
|
|
|
@@ -1088,11 +1121,6 @@ class SMTPConnection extends EventEmitter {
|
|
|
1088
1121
|
);
|
|
1089
1122
|
}
|
|
1090
1123
|
|
|
1091
|
-
if (!str.trim()) {
|
|
1092
|
-
// skip unexpected empty lines
|
|
1093
|
-
setImmediate(() => this._processResponse());
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
1124
|
const action = this._responseActions.shift();
|
|
1097
1125
|
|
|
1098
1126
|
if (typeof action === 'function') {
|
|
@@ -1189,6 +1217,23 @@ class SMTPConnection extends EventEmitter {
|
|
|
1189
1217
|
}
|
|
1190
1218
|
}
|
|
1191
1219
|
|
|
1220
|
+
// RFC 8689: validate REQUIRETLS eligibility before queuing the MAIL FROM
|
|
1221
|
+
// response action, so a rejection here cannot leave an orphaned action in
|
|
1222
|
+
// _responseActions (which would consume the next reply and desync a reused
|
|
1223
|
+
// connection).
|
|
1224
|
+
if (this._envelope.requireTLSExtensionEnabled) {
|
|
1225
|
+
if (!this.secure) {
|
|
1226
|
+
return callback(
|
|
1227
|
+
this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
if (!this._supportedExtensions.includes('REQUIRETLS')) {
|
|
1231
|
+
return callback(
|
|
1232
|
+
this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1192
1237
|
this._responseActions.push(str => {
|
|
1193
1238
|
this._actionMAIL(str, callback);
|
|
1194
1239
|
});
|
|
@@ -1225,20 +1270,10 @@ class SMTPConnection extends EventEmitter {
|
|
|
1225
1270
|
}
|
|
1226
1271
|
}
|
|
1227
1272
|
|
|
1228
|
-
// RFC 8689:
|
|
1229
|
-
//
|
|
1230
|
-
//
|
|
1273
|
+
// RFC 8689: append the REQUIRETLS keyword to MAIL FROM. Eligibility
|
|
1274
|
+
// (TLS connection + server support) was already validated above, before
|
|
1275
|
+
// the response action was queued.
|
|
1231
1276
|
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
1277
|
args.push('REQUIRETLS');
|
|
1243
1278
|
}
|
|
1244
1279
|
|