nodemailer 9.0.0 → 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 CHANGED
@@ -1,5 +1,23 @@
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
+
14
+ ## [9.0.1](https://github.com/nodemailer/nodemailer/compare/v9.0.0...v9.0.1) (2026-06-17)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * enforce disableFileAccess/disableUrlAccess for raw message option ([a82e060](https://github.com/nodemailer/nodemailer/commit/a82e060d978f27e5f41369a9a9807b1e3dedc2e2))
20
+
3
21
  ## [9.0.0](https://github.com/nodemailer/nodemailer/compare/v8.0.11...v9.0.0) (2026-06-14)
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
@@ -32,7 +32,11 @@ class MailComposer {
32
32
 
33
33
  // Compose MIME tree
34
34
  if (this.mail.raw) {
35
- this.message = new MimeNode('message/rfc822', { newline: this.mail.newline }).setRaw(this.mail.raw);
35
+ this.message = new MimeNode('message/rfc822', {
36
+ newline: this.mail.newline,
37
+ disableUrlAccess: this.mail.disableUrlAccess,
38
+ disableFileAccess: this.mail.disableFileAccess
39
+ }).setRaw(this.mail.raw);
36
40
  } else if (this._useMixed) {
37
41
  this.message = this._createMixed();
38
42
  } else if (this._useAlternative) {
@@ -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
- // promise
47
- return this.ses.sesClient.config
48
- .region()
49
- .then(region => cb(null, region))
50
- .catch(err => cb(err));
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
- const command = new this.ses.SendEmailCommand(sesMessage);
154
- const sendPromise = this.ses.sesClient.send(command);
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
- callback(null, {
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
- const command = new this.ses.SendEmailCommand(sesMessage);
226
- const sendPromise = this.ses.sesClient.send(command);
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
- let str = (this.lastServerResponse = decodeServerResponse((this._responseQueue.shift() || '').toString()));
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
- // keep waiting for the final part of multiline response
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: If the envelope requests REQUIRETLS extension
1229
- // then append REQUIRETLS keyword to the MAIL FROM command
1230
- // Note: REQUIRETLS can only be used over TLS connections and requires server support
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "9.0.0",
3
+ "version": "9.0.2",
4
4
  "description": "Easy as cake e-mail sending from your Node.js applications",
5
5
  "main": "lib/nodemailer.js",
6
6
  "scripts": {
@@ -27,10 +27,10 @@
27
27
  },
28
28
  "homepage": "https://nodemailer.com/",
29
29
  "devDependencies": {
30
- "@aws-sdk/client-sesv2": "3.1065.0",
30
+ "@aws-sdk/client-sesv2": "3.1068.0",
31
31
  "bunyan": "1.8.15",
32
32
  "c8": "11.0.0",
33
- "eslint": "10.4.1",
33
+ "eslint": "10.5.0",
34
34
  "eslint-config-prettier": "10.1.8",
35
35
  "globals": "17.6.0",
36
36
  "libbase64": "1.3.0",
@@ -39,7 +39,7 @@
39
39
  "prettier": "3.8.4",
40
40
  "proxy": "1.0.2",
41
41
  "proxy-test-server": "1.0.0",
42
- "smtp-server": "3.18.5"
42
+ "smtp-server": "3.19.0"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=6.0.0"