@mavogel/awscdk-rootmail 0.1.15 → 0.1.16

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.
Files changed (65) hide show
  1. package/.jsii +4 -4
  2. package/lib/rootmail.js +1 -1
  3. package/lib/ses-receive.js +1 -1
  4. package/node_modules/mailparser/.release-please-manifest.json +1 -1
  5. package/node_modules/mailparser/CHANGELOG.md +7 -0
  6. package/node_modules/mailparser/package.json +2 -2
  7. package/node_modules/nodemailer/CHANGELOG.md +27 -0
  8. package/node_modules/nodemailer/dist/cjs/base64/index.d.ts +3 -0
  9. package/node_modules/nodemailer/dist/cjs/base64/index.js +80 -41
  10. package/node_modules/nodemailer/dist/cjs/dkim/index.js +43 -29
  11. package/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +2 -2
  12. package/node_modules/nodemailer/dist/cjs/errors.d.ts +17 -0
  13. package/node_modules/nodemailer/dist/cjs/errors.js +12 -1
  14. package/node_modules/nodemailer/dist/cjs/fetch/index.js +23 -3
  15. package/node_modules/nodemailer/dist/cjs/mailer/index.js +23 -7
  16. package/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +5 -0
  17. package/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +20 -0
  18. package/node_modules/nodemailer/dist/cjs/mime-node/index.d.ts +1 -1
  19. package/node_modules/nodemailer/dist/cjs/mime-node/index.js +165 -105
  20. package/node_modules/nodemailer/dist/cjs/nodemailer.js +12 -9
  21. package/node_modules/nodemailer/dist/cjs/package-info.d.ts +1 -1
  22. package/node_modules/nodemailer/dist/cjs/package-info.js +1 -1
  23. package/node_modules/nodemailer/dist/cjs/qp/index.d.ts +2 -0
  24. package/node_modules/nodemailer/dist/cjs/qp/index.js +103 -33
  25. package/node_modules/nodemailer/dist/cjs/sendmail-transport/index.js +54 -14
  26. package/node_modules/nodemailer/dist/cjs/shared/index.d.ts +32 -1
  27. package/node_modules/nodemailer/dist/cjs/shared/index.js +212 -115
  28. package/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +3 -1
  29. package/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +88 -68
  30. package/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +7 -1
  31. package/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +348 -136
  32. package/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +3 -0
  33. package/node_modules/nodemailer/dist/cjs/smtp-pool/index.js +7 -1
  34. package/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.js +106 -17
  35. package/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +8 -2
  36. package/node_modules/nodemailer/dist/esm/base64/index.d.ts +3 -0
  37. package/node_modules/nodemailer/dist/esm/base64/index.js +80 -41
  38. package/node_modules/nodemailer/dist/esm/dkim/index.js +43 -29
  39. package/node_modules/nodemailer/dist/esm/dkim/message-parser.js +2 -2
  40. package/node_modules/nodemailer/dist/esm/errors.d.ts +17 -0
  41. package/node_modules/nodemailer/dist/esm/errors.js +10 -0
  42. package/node_modules/nodemailer/dist/esm/fetch/index.js +23 -3
  43. package/node_modules/nodemailer/dist/esm/mailer/index.js +23 -7
  44. package/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +5 -0
  45. package/node_modules/nodemailer/dist/esm/mailer/mail-message.js +20 -0
  46. package/node_modules/nodemailer/dist/esm/mime-node/index.d.ts +1 -1
  47. package/node_modules/nodemailer/dist/esm/mime-node/index.js +166 -106
  48. package/node_modules/nodemailer/dist/esm/nodemailer.js +12 -9
  49. package/node_modules/nodemailer/dist/esm/package-info.d.ts +1 -1
  50. package/node_modules/nodemailer/dist/esm/package-info.js +1 -1
  51. package/node_modules/nodemailer/dist/esm/qp/index.d.ts +2 -0
  52. package/node_modules/nodemailer/dist/esm/qp/index.js +103 -33
  53. package/node_modules/nodemailer/dist/esm/sendmail-transport/index.js +54 -14
  54. package/node_modules/nodemailer/dist/esm/shared/index.d.ts +32 -1
  55. package/node_modules/nodemailer/dist/esm/shared/index.js +207 -115
  56. package/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +3 -1
  57. package/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.js +88 -68
  58. package/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +7 -1
  59. package/node_modules/nodemailer/dist/esm/smtp-connection/index.js +348 -136
  60. package/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +3 -0
  61. package/node_modules/nodemailer/dist/esm/smtp-pool/index.js +7 -1
  62. package/node_modules/nodemailer/dist/esm/smtp-pool/pool-resource.js +106 -17
  63. package/node_modules/nodemailer/dist/esm/smtp-transport/index.js +8 -2
  64. package/node_modules/nodemailer/package.json +1 -1
  65. package/package.json +3 -3
@@ -45,12 +45,30 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
45
45
  const data_stream_js_1 = __importDefault(require("./data-stream.js"));
46
46
  const node_stream_1 = require("node:stream");
47
47
  const shared = __importStar(require("../shared/index.js"));
48
+ const errors_js_1 = require("../errors.js");
48
49
  // default timeout values in ms
49
50
  const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
50
51
  const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
51
52
  const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
52
53
  const DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
54
+ const CLOSE_TIMEOUT = 5 * 1000; // how much to wait for the server to close its side after we closed ours
55
+ const KEEPALIVE_DELAY = 30 * 1000; // idle time before TCP keepalive probes start, keeps NAT mappings of idle connections alive
53
56
  const TEARDOWN_NOOP = () => { }; // reusable no-op handler for absorbing errors during socket teardown
57
+ // Random order, so that connections spread over the addresses of a host
58
+ function shuffle(list) {
59
+ const result = list.slice();
60
+ for (let i = result.length - 1; i > 0; i--) {
61
+ const j = Math.floor(Math.random() * (i + 1));
62
+ [result[i], result[j]] = [result[j], result[i]];
63
+ }
64
+ return result;
65
+ }
66
+ // Every timeout is reported with the ETIMEDOUT code, timeoutType tells which one it was
67
+ function timeoutError(message, timeoutType) {
68
+ const err = new Error(message);
69
+ err.timeoutType = timeoutType;
70
+ return err;
71
+ }
54
72
  // how many bytes a single server response may occupy while it is still being received.
55
73
  // Generous compared to any real reply, it only stops a peer that never completes one
56
74
  const MAX_RESPONSE_SIZE = 1024 * 1024;
@@ -173,8 +191,8 @@ class SMTPConnection extends node_events_1.EventEmitter {
173
191
  this._maxAllowedSize = 0;
174
192
  this._responseActions = [];
175
193
  this._recipientQueue = [];
176
- this._greetingTimeout = false;
177
- this._connectionTimeout = false;
194
+ this._phaseTimer = false;
195
+ this._plaintextEhlo = false;
178
196
  this._destroyed = false;
179
197
  this._closing = false;
180
198
  this._currentDataStream = false;
@@ -185,14 +203,25 @@ class SMTPConnection extends node_events_1.EventEmitter {
185
203
  this._onSocketClose = () => this._onClose();
186
204
  this._onSocketEnd = () => this._onEnd();
187
205
  this._onSocketTimeout = () => this._onTimeout();
188
- this._onConnectionSocketError = err => this._onConnectionError(err, 'ESOCKET');
189
- this._connectionAttemptId = 0;
206
+ this._onConnectionSocketError = err => this._onError(err, 'ESOCKET', false, 'CONN');
190
207
  }
191
208
  /**
192
209
  * Creates a connection to a SMTP server and sets up connection
193
210
  * listener
194
211
  */
195
212
  connect(connectCallback) {
213
+ if (this._connectCalled && !this._destroyed) {
214
+ // A connection is opened once. A second call would open a second socket over the
215
+ // first one and run the session handlers of both against the same state
216
+ const err = this._formatError('Cannot connect - connect() was already called for this connection', 'ECONNECTION', false, 'API');
217
+ if (typeof connectCallback === 'function') {
218
+ setImmediate(() => connectCallback(err));
219
+ return;
220
+ }
221
+ this.logger.warn({ tnx: 'smtp' }, '%s', err.message);
222
+ return;
223
+ }
224
+ this._connectCalled = true;
196
225
  if (typeof connectCallback === 'function') {
197
226
  this._connectCallback = connectCallback;
198
227
  this.once('connect', () => {
@@ -207,11 +236,15 @@ class SMTPConnection extends node_events_1.EventEmitter {
207
236
  return connectCallback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'CONN'));
208
237
  }
209
238
  }
239
+ // connectionTimeout covers the whole of connecting: the DNS lookup and every address tried
240
+ const connectionTimeout = this.options.connectionTimeout || CONNECTION_TIMEOUT;
241
+ // a transport that first opened a proxy connection for this one sets when that started
242
+ this._connectionDeadline = (this._connectStartedAt || Date.now()) + connectionTimeout;
210
243
  let opts = {
211
244
  port: this.port,
212
245
  host: this.host,
213
246
  allowInternalNetworkInterfaces: this.allowInternalNetworkInterfaces,
214
- timeout: this.options.dnsTimeout || DNS_TIMEOUT
247
+ timeout: Math.min(this.options.dnsTimeout || DNS_TIMEOUT, connectionTimeout)
215
248
  };
216
249
  if (this.options.localAddress) {
217
250
  opts.localAddress = this.options.localAddress;
@@ -240,7 +273,6 @@ class SMTPConnection extends node_events_1.EventEmitter {
240
273
  return this._resolveAndConnect(opts, _resolved => {
241
274
  try {
242
275
  this._socket.connect(this.port, this.host, () => {
243
- this._socket.setKeepAlive(true);
244
276
  // a `secure` connection over a caller-provided socket must still
245
277
  // perform the TLS handshake, otherwise AUTH and the message body
246
278
  // would be sent in cleartext despite the caller requesting TLS
@@ -272,9 +304,40 @@ class SMTPConnection extends node_events_1.EventEmitter {
272
304
  }
273
305
  }
274
306
  return this._resolveAndConnect(opts, resolved => {
275
- // Store fallback addresses for retry on connection failure
276
- this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
277
- this._connectOpts = Object.assign({}, opts);
307
+ let addresses = resolved._addresses || [];
308
+ if (opts.localAddress) {
309
+ // a socket bound to an address of one family can not reach the other one
310
+ const localFamily = node_net_1.default.isIPv6(opts.localAddress) ? 6 : 4;
311
+ const sameFamily = addresses.filter(addr => node_net_1.default.isIP(addr) === localFamily);
312
+ addresses = sameFamily.length ? sameFamily : addresses;
313
+ }
314
+ if (addresses.length > 1) {
315
+ // net.connect tries the addresses in turn and moves on to the next one when an
316
+ // attempt fails or takes too long. With both families it starts on IPv6 and
317
+ // alternates (RFC 8305), so a host with a broken IPv6 path costs a fraction of
318
+ // a second instead of a whole connection timeout
319
+ const ipv6 = addresses.filter(addr => node_net_1.default.isIPv6(addr));
320
+ const ipv4 = addresses.filter(addr => !node_net_1.default.isIPv6(addr));
321
+ const ordered = shuffle(ipv6).concat(shuffle(ipv4));
322
+ opts.host = this.host;
323
+ opts.autoSelectFamily = true;
324
+ if (!ipv6.length || !ipv4.length) {
325
+ // a slow address of the only family gets its share of the time, not the
326
+ // quarter second meant for an address family that does not work
327
+ const remaining = this._connectionDeadline - Date.now();
328
+ opts.autoSelectFamilyAttemptTimeout = Math.max(Math.floor(remaining / ordered.length), 10);
329
+ }
330
+ opts.lookup = ((hostname, lookupOptions, callback) => {
331
+ if (lookupOptions && lookupOptions.all) {
332
+ const all = ordered.map(address => ({ address, family: node_net_1.default.isIPv6(address) ? 6 : 4 }));
333
+ return setImmediate(() => callback(null, all));
334
+ }
335
+ setImmediate(() => callback(null, ordered[0], node_net_1.default.isIPv6(ordered[0]) ? 6 : 4));
336
+ });
337
+ }
338
+ else if (addresses.length) {
339
+ opts.host = addresses[0];
340
+ }
278
341
  this._connectToHost(opts, this.secureConnection);
279
342
  });
280
343
  }
@@ -321,20 +384,11 @@ class SMTPConnection extends node_events_1.EventEmitter {
321
384
  if (this._destroyed || this._closing) {
322
385
  return;
323
386
  }
324
- this._connectionAttemptId++;
325
- const currentAttemptId = this._connectionAttemptId;
326
387
  const connectFn = secure
327
388
  ? node_tls_1.default.connect
328
389
  : node_net_1.default.connect;
329
390
  try {
330
- this._socket = connectFn(opts, () => {
331
- // Ignore callback if this is a stale connection attempt
332
- if (this._connectionAttemptId !== currentAttemptId) {
333
- return;
334
- }
335
- this._socket.setKeepAlive(true);
336
- this._onConnect();
337
- });
391
+ this._socket = connectFn(opts, () => this._onConnect());
338
392
  this._setupConnectionHandlers();
339
393
  }
340
394
  catch (E) {
@@ -347,51 +401,37 @@ class SMTPConnection extends node_events_1.EventEmitter {
347
401
  * @internal
348
402
  */
349
403
  _setupConnectionHandlers() {
350
- this._connectionTimeout = setTimeout(() => {
351
- this._onConnectionError('Connection timeout', 'ETIMEDOUT');
352
- }, this.options.connectionTimeout || CONNECTION_TIMEOUT);
404
+ this._startPhase(Math.max((this._connectionDeadline || Date.now()) - Date.now(), 0), timeoutError('Connection timeout', 'CONNECT_TIMEOUT'));
353
405
  this._socket.on('error', this._onConnectionSocketError);
354
406
  }
355
407
  /**
356
- * Handles connection errors with fallback to alternative addresses
408
+ * Starts the timer of a connection phase: connecting, waiting for the greeting or a TLS
409
+ * upgrade. The phases follow one another, so starting one ends the one before
357
410
  *
358
- * @param err Error object or message
359
- * @param code Error code
411
+ * @param timeout Time the phase may take
412
+ * @param err Error to fail with when it takes longer
360
413
  * @internal
361
414
  */
362
- _onConnectionError(err, code) {
363
- clearTimeout(this._connectionTimeout);
364
- // Check if we have fallback addresses to try
365
- const canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
366
- if (!canFallback) {
367
- // No more fallback addresses, report the error
368
- this._onError(err, code, false, 'CONN');
369
- return;
370
- }
371
- const nextHost = this._fallbackAddresses.shift();
372
- this.logger.info({
373
- tnx: 'network',
374
- failedHost: this._connectOpts.host,
375
- nextHost,
376
- error: err.message || err
377
- }, 'Connection to %s failed, trying %s', this._connectOpts.host, nextHost);
378
- // Clean up current socket
379
- if (this._socket) {
380
- try {
381
- this._socket.removeListener('error', this._onConnectionSocketError);
382
- // Absorb any late teardown error (e.g. a TLS fallback socket emitting
383
- // after destroy), mirroring the guard used in close()
384
- this._socket.on('error', TEARDOWN_NOOP);
385
- this._socket.destroy();
386
- }
387
- catch (_E) {
388
- // ignore
389
- }
390
- this._socket = null;
391
- }
392
- // Update host and retry
393
- this._connectOpts.host = nextHost;
394
- this._connectToHost(this._connectOpts, this.secureConnection);
415
+ _startPhase(timeout, err) {
416
+ this._clearPhase();
417
+ this._phaseTimer = setTimeout(() => {
418
+ this._phaseTimer = false;
419
+ this._onError(err, 'ETIMEDOUT', false, 'CONN');
420
+ }, timeout);
421
+ }
422
+ /**
423
+ * Time the greeting or a STARTTLS upgrade may take: greetingTimeout, cut short by what is left
424
+ * of connectionTimeout
425
+ *
426
+ * @internal
427
+ */
428
+ _remainingSetupTime() {
429
+ return Math.min(this.options.greetingTimeout || GREETING_TIMEOUT, Math.max((this._connectionDeadline || Infinity) - Date.now(), 1));
430
+ }
431
+ /** @internal */
432
+ _clearPhase() {
433
+ clearTimeout(this._phaseTimer);
434
+ this._phaseTimer = false;
395
435
  }
396
436
  /**
397
437
  * Sends QUIT
@@ -404,8 +444,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
404
444
  * Closes the connection to the server
405
445
  */
406
446
  close() {
407
- clearTimeout(this._connectionTimeout);
408
- clearTimeout(this._greetingTimeout);
447
+ this._clearPhase();
409
448
  this._responseActions = [];
410
449
  // allow to run this function only once
411
450
  if (this._closing) {
@@ -428,13 +467,15 @@ class SMTPConnection extends node_events_1.EventEmitter {
428
467
  }
429
468
  this._currentDataStream = false;
430
469
  }
431
- // Detach from the message stream as well. The listener is swapped for a no-op rather than
432
- // removed, a stream destroyed with an error later on would otherwise throw it as unhandled
470
+ // Detach from the message stream as well and release whatever it reads from, the message
471
+ // can not be sent over this connection anymore. The listener is swapped for a no-op rather
472
+ // than removed, a stream destroyed with an error would otherwise throw it as unhandled
433
473
  if (this._pendingSend) {
434
474
  const { stream, onStreamError } = this._pendingSend;
435
475
  if (stream) {
436
476
  stream.removeListener('error', onStreamError);
437
477
  stream.on('error', TEARDOWN_NOOP);
478
+ stream.destroy();
438
479
  }
439
480
  this._pendingSend = false;
440
481
  }
@@ -453,6 +494,15 @@ class SMTPConnection extends node_events_1.EventEmitter {
453
494
  // sending cleartext after TLS shutdown triggers ERR_SSL_BAD_RECORD_TYPE)
454
495
  socket.on('error', TEARDOWN_NOOP);
455
496
  socket[closeMethod]();
497
+ if (closeMethod === 'end') {
498
+ // end() only closes our side, a server that never closes its own would keep
499
+ // the socket, and the process with it, around for good
500
+ const closeTimer = setTimeout(() => socket.destroy(), CLOSE_TIMEOUT);
501
+ if (typeof closeTimer.unref === 'function') {
502
+ closeTimer.unref();
503
+ }
504
+ socket.once('close', () => clearTimeout(closeTimer));
505
+ }
456
506
  }
457
507
  catch (_E) {
458
508
  // just ignore
@@ -657,14 +707,9 @@ class SMTPConnection extends node_events_1.EventEmitter {
657
707
  const startTime = Date.now();
658
708
  this._setEnvelope(envelope, (err, info) => {
659
709
  if (err) {
660
- // create passthrough stream to consume to prevent OOM
661
- const stream = new node_stream_1.PassThrough();
662
- if (typeof message.pipe === 'function') {
663
- message.pipe(stream);
664
- }
665
- else {
666
- stream.write(message);
667
- stream.end();
710
+ // the message is not going to be sent, release whatever the stream reads from
711
+ if (typeof message.destroy === 'function') {
712
+ message.destroy();
668
713
  }
669
714
  return callback(err);
670
715
  }
@@ -718,7 +763,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
718
763
  */
719
764
  _onConnect() {
720
765
  const socket = this._socket;
721
- clearTimeout(this._connectionTimeout);
766
+ this._clearPhase();
722
767
  this.logger.info({
723
768
  tnx: 'network',
724
769
  localAddress: socket.localAddress,
@@ -748,16 +793,42 @@ class SMTPConnection extends node_events_1.EventEmitter {
748
793
  socket.once('end', this._onSocketEnd);
749
794
  socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
750
795
  socket.on('timeout', this._onSocketTimeout);
751
- this._greetingTimeout = setTimeout(() => {
752
- // if still waiting for greeting, give up
753
- if (this._socket && !this._destroyed && this._responseActions[0] === this._actionGreeting) {
754
- this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN');
755
- }
756
- }, this.options.greetingTimeout || GREETING_TIMEOUT);
796
+ // keepalive also covers sockets handed over by a proxy or by the caller
797
+ if (typeof socket.setKeepAlive === 'function') {
798
+ socket.setKeepAlive(true, KEEPALIVE_DELAY);
799
+ }
800
+ // Commands are written in the batches they belong to (see cork() for PIPELINING), Nagle
801
+ // would only hold a write back until the server acknowledged the previous one. Against a
802
+ // server that delays its ACKs that costs 40ms on every message
803
+ if (typeof socket.setNoDelay === 'function') {
804
+ socket.setNoDelay(true);
805
+ }
806
+ // bounded by greetingTimeout and by what is left of connectionTimeout, which covers setting
807
+ // the session up from the DNS lookup to the end of a STARTTLS upgrade
808
+ this._startPhase(this._remainingSetupTime(), timeoutError('Greeting never received', 'GREETING_TIMEOUT'));
757
809
  this._responseActions.push(this._actionGreeting);
758
810
  // we have a 'data' listener set up so resume socket if it was paused
759
811
  socket.resume();
760
812
  }
813
+ /**
814
+ * Ends the session after a 421 reply. The replies queued for commands sent along with the
815
+ * answered one are not going to come, so the message in flight is failed here
816
+ *
817
+ * @param str The 421 reply
818
+ * @internal
819
+ */
820
+ _onServerClosing(str) {
821
+ if (this._destroyed) {
822
+ return;
823
+ }
824
+ const pendingSend = this._pendingSend;
825
+ const envelope = this._envelope;
826
+ this._responseActions = [];
827
+ this.close();
828
+ if (pendingSend) {
829
+ pendingSend.callback((envelope && envelope.mailError) || this._formatError('Server closed the connection', 'ECONNECTION', str, 'CONN'));
830
+ }
831
+ }
761
832
  /**
762
833
  * 'data' listener for data coming from the server
763
834
  *
@@ -832,8 +903,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
832
903
  * @internal
833
904
  */
834
905
  _onError(err, type, data, command) {
835
- clearTimeout(this._connectionTimeout);
836
- clearTimeout(this._greetingTimeout);
906
+ this._clearPhase();
837
907
  if (this._destroyed) {
838
908
  // just ignore, already closed
839
909
  // this might happen when a socket is canceled because of reached timeout
@@ -841,12 +911,12 @@ class SMTPConnection extends node_events_1.EventEmitter {
841
911
  return;
842
912
  }
843
913
  err = this._formatError(err, type, data, command);
844
- const transientCodes = ['ETIMEDOUT', 'ESOCKET', 'ECONNECTION'];
845
- if (transientCodes.includes(err.code)) {
846
- this.logger.warn(data, err.message);
914
+ // the message carries the server response, it is an argument and not the format string
915
+ if ((0, errors_js_1.isTransientError)(err)) {
916
+ this.logger.warn({ tnx: 'smtp', err }, '%s', err.message);
847
917
  }
848
918
  else {
849
- this.logger.error(data, err.message);
919
+ this.logger.error({ tnx: 'smtp', err }, '%s', err.message);
850
920
  }
851
921
  // close() forgets the send in flight, it is completed with this same error afterwards so
852
922
  // a late message stream error has nothing left to report
@@ -866,7 +936,12 @@ class SMTPConnection extends node_events_1.EventEmitter {
866
936
  else {
867
937
  err = new Error(message);
868
938
  }
869
- if (type && type !== 'Error') {
939
+ // a permission model denial keeps its own code, see ERR_ACCESS_DENIED
940
+ if (type && type !== 'Error' && err.code !== errors_js_1.ERR_ACCESS_DENIED) {
941
+ // the code of a system error, such as ECONNREFUSED, still tells what happened
942
+ if (err.code && err.code !== type && !err.originalCode) {
943
+ err.originalCode = err.code;
944
+ }
870
945
  err.code = type;
871
946
  }
872
947
  if (response) {
@@ -918,6 +993,19 @@ class SMTPConnection extends node_events_1.EventEmitter {
918
993
  this.close();
919
994
  return;
920
995
  }
996
+ if (!failureResponse &&
997
+ this.stage === 'connected' &&
998
+ !this._responseActions.length &&
999
+ !this._pendingSend &&
1000
+ !this._destroyed &&
1001
+ !this._closing) {
1002
+ // nothing was waiting for the server, so this is the server ending an idle session
1003
+ // (usually after its idle timeout), not a failure
1004
+ this.logger.info({
1005
+ tnx: 'network'
1006
+ }, 'Server closed the idle connection');
1007
+ return this._destroy();
1008
+ }
921
1009
  if (failureResponse || (this._responseActions[0] !== this.close && !this._destroyed)) {
922
1010
  return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', failureResponse, 'CONN');
923
1011
  }
@@ -943,7 +1031,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
943
1031
  * @internal
944
1032
  */
945
1033
  _onTimeout() {
946
- return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN');
1034
+ return this._onError(timeoutError('Timeout', 'SOCKET_TIMEOUT'), 'ETIMEDOUT', false, 'CONN');
947
1035
  }
948
1036
  /**
949
1037
  * Destroys the client, emits 'end'
@@ -958,10 +1046,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
958
1046
  this.destroyed = true;
959
1047
  // a connection the server dropped before the greeting would otherwise keep
960
1048
  // the greeting timer, and with it the process, alive until it fires
961
- clearTimeout(this._connectionTimeout);
962
- clearTimeout(this._greetingTimeout);
963
- this._connectionTimeout = false;
964
- this._greetingTimeout = false;
1049
+ this._clearPhase();
965
1050
  this.emit('end');
966
1051
  }
967
1052
  /**
@@ -978,6 +1063,15 @@ class SMTPConnection extends node_events_1.EventEmitter {
978
1063
  // inject plaintext bytes after the "220" reply (e.g. a CRLF-free fragment that
979
1064
  // would otherwise be prepended to the first post-TLS response and parsed as
980
1065
  // part of the secured EHLO capabilities). STARTTLS response injection.
1066
+ const discarded = this._remainder.length + this._responseQueue.reduce((total, response) => total + response.length, 0);
1067
+ if (discarded) {
1068
+ // a server does not send anything here on its own, this is worth knowing about
1069
+ this.logger.warn({
1070
+ tnx: 'smtp',
1071
+ discarded
1072
+ }, 'Discarded %s bytes received in plaintext after the STARTTLS response', discarded);
1073
+ }
1074
+ this._plaintextEhlo = false;
981
1075
  this._remainder = '';
982
1076
  this._responseQueue = [];
983
1077
  this._responsePartial = false;
@@ -987,6 +1081,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
987
1081
  const socketPlain = this._socket;
988
1082
  socketPlain.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards
989
1083
  socketPlain.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object
1084
+ socketPlain.setTimeout(0);
990
1085
  const opts = Object.assign({
991
1086
  socket: socketPlain,
992
1087
  host: this.host
@@ -1007,9 +1102,14 @@ class SMTPConnection extends node_events_1.EventEmitter {
1007
1102
  socketPlain.removeListener('error', this._onConnectionSocketError);
1008
1103
  };
1009
1104
  this.upgrading = true;
1105
+ // the socket timeout only notices a server that sends nothing at all, a handshake that
1106
+ // trickles along would otherwise hold the connection for as long as the server likes
1107
+ // STARTTLS is the last step of setting the session up
1108
+ this._startPhase(this._remainingSetupTime(), timeoutError('TLS handshake timed out', 'UPGRADE_TIMEOUT'));
1010
1109
  // tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
1011
1110
  try {
1012
1111
  this._socket = node_tls_1.default.connect(opts, () => {
1112
+ this._clearPhase();
1013
1113
  this.secure = true;
1014
1114
  this.upgrading = false;
1015
1115
  this._socket.on('data', this._onSocketData);
@@ -1018,6 +1118,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
1018
1118
  });
1019
1119
  }
1020
1120
  catch (err) {
1121
+ this._clearPhase();
1021
1122
  removePlainSocketListeners();
1022
1123
  return callback(err);
1023
1124
  }
@@ -1058,10 +1159,24 @@ class SMTPConnection extends node_events_1.EventEmitter {
1058
1159
  }, str.replace(/\r?\n$/, ''));
1059
1160
  }
1060
1161
  const action = this._responseActions.shift();
1162
+ // RFC 5321 4.2: 421 means the server is about to close the connection, whatever it answers
1163
+ const closing = /^421[ -]/.test(str);
1061
1164
  if (typeof action === 'function') {
1165
+ // the command gets its own error first, the code tells which step failed
1062
1166
  action.call(this, str);
1167
+ if (closing) {
1168
+ return this._onServerClosing(str);
1169
+ }
1063
1170
  setImmediate(() => this._processResponse());
1064
1171
  }
1172
+ else if (closing && !this._pendingSend && this.stage === 'connected') {
1173
+ // RFC 5321 4.2: a server may send 421 at any time when it is about to close the
1174
+ // connection. Nothing was waiting for a reply, so this ends an idle session
1175
+ this.logger.info({
1176
+ tnx: 'smtp'
1177
+ }, 'Server closed the idle connection: %s', str);
1178
+ this.close();
1179
+ }
1065
1180
  else {
1066
1181
  return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN');
1067
1182
  }
@@ -1156,6 +1271,9 @@ class SMTPConnection extends node_events_1.EventEmitter {
1156
1271
  return callback(this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM'));
1157
1272
  }
1158
1273
  }
1274
+ // RFC 2920: with PIPELINING the whole envelope and DATA go out without waiting for the
1275
+ // replies in between, which saves two round trips for every message
1276
+ this._envelope.pipelined = this._supportedExtensions.includes('PIPELINING');
1159
1277
  this._responseActions.push(str => {
1160
1278
  this._actionMAIL(str, callback);
1161
1279
  });
@@ -1193,7 +1311,38 @@ class SMTPConnection extends node_events_1.EventEmitter {
1193
1311
  if (this._envelope.requireTLSExtensionEnabled) {
1194
1312
  args.push('REQUIRETLS');
1195
1313
  }
1196
- this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : ''));
1314
+ const mailFrom = 'MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : '');
1315
+ this._recipientQueue = [];
1316
+ if (!this._envelope.pipelined) {
1317
+ this._sendCommand(mailFrom);
1318
+ return;
1319
+ }
1320
+ // corked, so the batch leaves in one segment instead of the first command alone
1321
+ const socket = this._socket;
1322
+ socket.cork();
1323
+ this._sendCommand(mailFrom);
1324
+ while (this._envelope.rcptQueue.length) {
1325
+ this._sendRcpt(this._envelope.rcptQueue.shift(), callback);
1326
+ }
1327
+ this._responseActions.push(str => {
1328
+ this._actionDATA(str, callback);
1329
+ });
1330
+ this._sendCommand('DATA');
1331
+ socket.uncork();
1332
+ }
1333
+ /**
1334
+ * Sends RCPT TO for a recipient and queues the handler for the reply
1335
+ *
1336
+ * @param recipient Recipient address
1337
+ * @param callback Callback to run once the envelope is processed
1338
+ * @internal
1339
+ */
1340
+ _sendRcpt(recipient, callback) {
1341
+ this._recipientQueue.push(recipient);
1342
+ this._responseActions.push(str => {
1343
+ this._actionRCPT(str, callback);
1344
+ });
1345
+ this._sendCommand('RCPT TO:<' + recipient + '>' + this._getDsnRcptToArgs());
1197
1346
  }
1198
1347
  /** @internal */
1199
1348
  _setDsnEnvelope(params) {
@@ -1309,7 +1458,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
1309
1458
  * @internal
1310
1459
  */
1311
1460
  _actionGreeting(str) {
1312
- clearTimeout(this._greetingTimeout);
1461
+ this._clearPhase();
1313
1462
  if (str.substr(0, 3) !== '220') {
1314
1463
  this._onError(new Error('Invalid greeting. response=' + str), 'EPROTOCOL', str, 'CONN');
1315
1464
  return;
@@ -1347,7 +1496,6 @@ class SMTPConnection extends node_events_1.EventEmitter {
1347
1496
  * @internal
1348
1497
  */
1349
1498
  _actionEHLO(str) {
1350
- let match;
1351
1499
  if (str.substr(0, 3) === '421') {
1352
1500
  this._onError(new Error('Server terminates connection. response=' + str), 'ECONNECTION', str, 'EHLO');
1353
1501
  return;
@@ -1362,17 +1510,30 @@ class SMTPConnection extends node_events_1.EventEmitter {
1362
1510
  this._sendCommand('HELO ' + this.name);
1363
1511
  return;
1364
1512
  }
1365
- this._ehloLines = str
1366
- .split(/\r?\n/)
1367
- .map(line => line.replace(/^\d+[ -]/, '').trim())
1368
- .filter(line => line)
1369
- .slice(1);
1370
1513
  // Detect if the server supports STARTTLS
1371
1514
  if (!this.secure && !this.options.ignoreTLS && (/[ -]STARTTLS\b/im.test(str) || this.options.requireTLS)) {
1515
+ // kept for opportunisticTLS, a session that stays in plaintext still has these extensions
1516
+ this._plaintextEhlo = str;
1372
1517
  this._sendCommand('STARTTLS');
1373
1518
  this._responseActions.push(this._actionSTARTTLS);
1374
1519
  return;
1375
1520
  }
1521
+ this._parseEhloExtensions(str);
1522
+ this.emit('connect');
1523
+ }
1524
+ /**
1525
+ * Reads the extensions and the authentication mechanisms out of an EHLO response
1526
+ *
1527
+ * @param str EHLO response from the server
1528
+ * @internal
1529
+ */
1530
+ _parseEhloExtensions(str) {
1531
+ let match;
1532
+ this._ehloLines = str
1533
+ .split(/\r?\n/)
1534
+ .map(line => line.replace(/^\d+[ -]/, '').trim())
1535
+ .filter(line => line)
1536
+ .slice(1);
1376
1537
  // Detect if the server supports SMTPUTF8
1377
1538
  if (/[ -]SMTPUTF8\b/im.test(str)) {
1378
1539
  this._supportedExtensions.push('SMTPUTF8');
@@ -1422,7 +1583,6 @@ class SMTPConnection extends node_events_1.EventEmitter {
1422
1583
  this._supportedExtensions.push('SIZE');
1423
1584
  this._maxAllowedSize = Number(match[1]) || 0;
1424
1585
  }
1425
- this.emit('connect');
1426
1586
  }
1427
1587
  /**
1428
1588
  * Handles server response for HELO command. If it yielded in
@@ -1454,6 +1614,14 @@ class SMTPConnection extends node_events_1.EventEmitter {
1454
1614
  this.logger.info({
1455
1615
  tnx: 'smtp'
1456
1616
  }, 'Failed STARTTLS upgrade, continuing unencrypted');
1617
+ // the plaintext session goes on with what the server announced for it, except for
1618
+ // AUTH: credentials are not sent over a connection that failed to encrypt
1619
+ if (this._plaintextEhlo) {
1620
+ this._parseEhloExtensions(this._plaintextEhlo);
1621
+ this._plaintextEhlo = false;
1622
+ this.allowsAuth = false;
1623
+ this._supportedAuth = [];
1624
+ }
1457
1625
  this.emit('connect');
1458
1626
  return;
1459
1627
  }
@@ -1634,21 +1802,9 @@ class SMTPConnection extends node_events_1.EventEmitter {
1634
1802
  const message = this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(envelope.from)
1635
1803
  ? 'Internationalized mailbox name not allowed'
1636
1804
  : 'Mail command failed';
1637
- return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
1638
- }
1639
- if (!envelope.rcptQueue.length) {
1640
- return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API'));
1805
+ envelope.mailError = this._formatError(message, 'EENVELOPE', str, 'MAIL FROM');
1641
1806
  }
1642
- this._recipientQueue = [];
1643
- const usePipelining = this._supportedExtensions.includes('PIPELINING');
1644
- do {
1645
- const curRecipient = envelope.rcptQueue.shift();
1646
- this._recipientQueue.push(curRecipient);
1647
- this._responseActions.push(str => {
1648
- this._actionRCPT(str, callback);
1649
- });
1650
- this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
1651
- } while (usePipelining && envelope.rcptQueue.length);
1807
+ this._advanceEnvelope(str, callback);
1652
1808
  }
1653
1809
  /**
1654
1810
  * Handle response for a RCPT TO: command
@@ -1675,32 +1831,67 @@ class SMTPConnection extends node_events_1.EventEmitter {
1675
1831
  else {
1676
1832
  envelope.accepted.push(curRecipient);
1677
1833
  }
1678
- if (!envelope.rcptQueue.length && !this._recipientQueue.length) {
1679
- if (envelope.rejected.length < envelope.to.length) {
1680
- this._responseActions.push(str => {
1681
- this._actionDATA(str, callback);
1682
- });
1683
- this._sendCommand('DATA');
1684
- }
1685
- else {
1686
- // report a temporary rejection when there is one, taking the last reply would mark the
1687
- // whole message as permanently failed although some recipients were only deferred
1688
- const deferred = envelope.rejectedErrors.find(rejectedErr => rejectedErr.responseCode && rejectedErr.responseCode < 500);
1689
- const reply = deferred?.response ?? str;
1690
- err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', reply, 'RCPT TO');
1691
- err.rejected = envelope.rejected;
1692
- err.rejectedErrors = envelope.rejectedErrors;
1693
- return callback(err);
1694
- }
1834
+ this._advanceEnvelope(str, callback);
1835
+ }
1836
+ /**
1837
+ * Moves the envelope on after a reply to MAIL FROM or RCPT TO. A pipelined envelope sent every
1838
+ * command at once and is decided by the reply to DATA. Otherwise the commands go one at a
1839
+ * time: the next recipient, or once every reply is in, DATA or the error that ends the message
1840
+ *
1841
+ * @param str The reply that was handled
1842
+ * @param callback Callback to run once the envelope is processed
1843
+ * @internal
1844
+ */
1845
+ _advanceEnvelope(str, callback) {
1846
+ const envelope = this._envelope;
1847
+ if (envelope.pipelined) {
1848
+ return;
1695
1849
  }
1696
- else if (envelope.rcptQueue.length) {
1697
- const nextRecipient = envelope.rcptQueue.shift();
1698
- this._recipientQueue.push(nextRecipient);
1699
- this._responseActions.push(str => {
1700
- this._actionRCPT(str, callback);
1701
- });
1702
- this._sendCommand('RCPT TO:<' + nextRecipient + '>' + this._getDsnRcptToArgs());
1850
+ if (envelope.mailError) {
1851
+ return callback(envelope.mailError);
1852
+ }
1853
+ if (envelope.rcptQueue.length) {
1854
+ return this._sendRcpt(envelope.rcptQueue.shift(), callback);
1855
+ }
1856
+ if (this._recipientQueue.length) {
1857
+ // replies still to come
1858
+ return;
1859
+ }
1860
+ const err = this._envelopeError(str);
1861
+ if (err) {
1862
+ return callback(err);
1703
1863
  }
1864
+ this._responseActions.push(str => {
1865
+ this._actionDATA(str, callback);
1866
+ });
1867
+ this._sendCommand('DATA');
1868
+ }
1869
+ /**
1870
+ * Decides how the envelope went once every reply to MAIL FROM and RCPT TO is in. Called after
1871
+ * the last RCPT TO reply, or with PIPELINING on the reply to the DATA command sent along
1872
+ *
1873
+ * @param str The reply being handled
1874
+ * @returns The error to fail the message with, or null when DATA can go ahead
1875
+ * @internal
1876
+ */
1877
+ _envelopeError(str) {
1878
+ const envelope = this._envelope;
1879
+ if (envelope.mailError) {
1880
+ return envelope.mailError;
1881
+ }
1882
+ if (envelope.accepted.length) {
1883
+ return null;
1884
+ }
1885
+ // report a temporary rejection when there is one, taking the last reply would mark the
1886
+ // whole message as permanently failed although some recipients were only deferred
1887
+ const deferred = envelope.rejectedErrors.find(rejectedErr => rejectedErr.responseCode && rejectedErr.responseCode < 500);
1888
+ const lastRejected = envelope.rejectedErrors[envelope.rejectedErrors.length - 1];
1889
+ const reply = deferred?.response ?? (envelope.pipelined && lastRejected ? lastRejected.response : str);
1890
+ // every recipient was rejected
1891
+ const err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', reply, 'RCPT TO');
1892
+ err.rejected = envelope.rejected;
1893
+ err.rejectedErrors = envelope.rejectedErrors;
1894
+ return err;
1704
1895
  }
1705
1896
  /**
1706
1897
  * Handle response for a DATA command
@@ -1711,6 +1902,27 @@ class SMTPConnection extends node_events_1.EventEmitter {
1711
1902
  */
1712
1903
  _actionDATA(str, callback) {
1713
1904
  const envelope = this._envelope;
1905
+ if (envelope.pipelined) {
1906
+ const err = this._envelopeError(str);
1907
+ if (err) {
1908
+ if (/^3/.test(str)) {
1909
+ if (envelope.mailError) {
1910
+ // A server that refused the sender has no transaction to end, one that
1911
+ // took DATA anyway can not be trusted with an empty message either.
1912
+ // Drop the connection instead of answering it
1913
+ this.close();
1914
+ return callback(err);
1915
+ }
1916
+ // A server must refuse DATA without an accepted recipient, this one took it
1917
+ // anyway. End the empty message, it has nobody to go to, so the session
1918
+ // stays usable
1919
+ this._responseActions.push(() => callback(err));
1920
+ this._sendCommand('.');
1921
+ return;
1922
+ }
1923
+ return callback(err);
1924
+ }
1925
+ }
1714
1926
  // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
1715
1927
  // some servers might use 250 instead, so lets check for 2 or 3 as the first digit
1716
1928
  if (!/^[23]/.test(str)) {