@mavogel/awscdk-rootmail 0.1.14 → 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 (168) hide show
  1. package/.jsii +10 -10
  2. package/lib/rootmail.js +1 -1
  3. package/lib/ses-receive.js +1 -1
  4. package/node_modules/@aws-sdk/checksums/package.json +3 -3
  5. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-cjs/index.js +23 -1
  6. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-es/models/CloudWatchLogsServiceException.js +1 -0
  7. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-es/models/errors.js +21 -0
  8. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/models/CloudWatchLogsServiceException.d.ts +1 -0
  9. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/models/errors.d.ts +21 -0
  10. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/ts3.4/models/CloudWatchLogsServiceException.d.ts +1 -0
  11. package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/ts3.4/models/errors.d.ts +21 -0
  12. package/node_modules/@aws-sdk/client-cloudwatch-logs/package.json +4 -4
  13. package/node_modules/@aws-sdk/client-route-53/dist-cjs/index.js +71 -1
  14. package/node_modules/@aws-sdk/client-route-53/dist-es/models/Route53ServiceException.js +1 -0
  15. package/node_modules/@aws-sdk/client-route-53/dist-es/models/errors.js +69 -0
  16. package/node_modules/@aws-sdk/client-route-53/dist-types/models/Route53ServiceException.d.ts +1 -0
  17. package/node_modules/@aws-sdk/client-route-53/dist-types/models/errors.d.ts +69 -0
  18. package/node_modules/@aws-sdk/client-route-53/dist-types/ts3.4/models/Route53ServiceException.d.ts +1 -0
  19. package/node_modules/@aws-sdk/client-route-53/dist-types/ts3.4/models/errors.d.ts +69 -0
  20. package/node_modules/@aws-sdk/client-route-53/package.json +4 -4
  21. package/node_modules/@aws-sdk/client-s3/dist-cjs/index.js +23 -1
  22. package/node_modules/@aws-sdk/client-s3/dist-es/models/S3ServiceException.js +1 -0
  23. package/node_modules/@aws-sdk/client-s3/dist-es/models/errors.js +21 -0
  24. package/node_modules/@aws-sdk/client-s3/dist-types/models/S3ServiceException.d.ts +1 -0
  25. package/node_modules/@aws-sdk/client-s3/dist-types/models/errors.d.ts +21 -0
  26. package/node_modules/@aws-sdk/client-s3/dist-types/ts3.4/models/S3ServiceException.d.ts +1 -0
  27. package/node_modules/@aws-sdk/client-s3/dist-types/ts3.4/models/errors.d.ts +21 -0
  28. package/node_modules/@aws-sdk/client-s3/package.json +8 -8
  29. package/node_modules/@aws-sdk/client-ses/dist-cjs/index.js +36 -1
  30. package/node_modules/@aws-sdk/client-ses/dist-es/models/SESServiceException.js +1 -0
  31. package/node_modules/@aws-sdk/client-ses/dist-es/models/errors.js +34 -0
  32. package/node_modules/@aws-sdk/client-ses/dist-types/models/SESServiceException.d.ts +1 -0
  33. package/node_modules/@aws-sdk/client-ses/dist-types/models/errors.d.ts +34 -0
  34. package/node_modules/@aws-sdk/client-ses/dist-types/ts3.4/models/SESServiceException.d.ts +1 -0
  35. package/node_modules/@aws-sdk/client-ses/dist-types/ts3.4/models/errors.d.ts +34 -0
  36. package/node_modules/@aws-sdk/client-ses/package.json +4 -4
  37. package/node_modules/@aws-sdk/client-ssm/dist-cjs/index.js +142 -1
  38. package/node_modules/@aws-sdk/client-ssm/dist-es/models/SSMServiceException.js +1 -0
  39. package/node_modules/@aws-sdk/client-ssm/dist-es/models/errors.js +140 -0
  40. package/node_modules/@aws-sdk/client-ssm/dist-types/models/SSMServiceException.d.ts +1 -0
  41. package/node_modules/@aws-sdk/client-ssm/dist-types/models/errors.d.ts +140 -0
  42. package/node_modules/@aws-sdk/client-ssm/dist-types/ts3.4/models/SSMServiceException.d.ts +1 -0
  43. package/node_modules/@aws-sdk/client-ssm/dist-types/ts3.4/models/errors.d.ts +140 -0
  44. package/node_modules/@aws-sdk/client-ssm/package.json +4 -4
  45. package/node_modules/@aws-sdk/core/package.json +3 -3
  46. package/node_modules/@aws-sdk/credential-provider-env/package.json +3 -3
  47. package/node_modules/@aws-sdk/credential-provider-http/package.json +3 -3
  48. package/node_modules/@aws-sdk/credential-provider-ini/package.json +10 -10
  49. package/node_modules/@aws-sdk/credential-provider-login/package.json +4 -4
  50. package/node_modules/@aws-sdk/credential-provider-node/package.json +8 -8
  51. package/node_modules/@aws-sdk/credential-provider-process/package.json +3 -3
  52. package/node_modules/@aws-sdk/credential-provider-sso/package.json +5 -5
  53. package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +4 -4
  54. package/node_modules/@aws-sdk/middleware-sdk-s3/package.json +4 -4
  55. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/cognito-identity/index.js +11 -1
  56. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/signin/index.js +6 -1
  57. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso/index.js +6 -1
  58. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso-oidc/index.js +13 -1
  59. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sts/index.js +9 -1
  60. package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/cognito-identity/models/CognitoIdentityServiceException.js +1 -0
  61. package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/cognito-identity/models/errors.js +9 -0
  62. package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/signin/models/SigninServiceException.js +1 -0
  63. package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/signin/models/errors.js +4 -0
  64. package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/sso/models/SSOServiceException.js +1 -0
  65. package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/sso/models/errors.js +4 -0
  66. package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/sso-oidc/models/SSOOIDCServiceException.js +1 -0
  67. package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/sso-oidc/models/errors.js +11 -0
  68. package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/sts/models/STSServiceException.js +1 -0
  69. package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/sts/models/errors.js +7 -0
  70. package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/cognito-identity/models/CognitoIdentityServiceException.d.ts +1 -0
  71. package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/cognito-identity/models/errors.d.ts +9 -0
  72. package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/signin/models/SigninServiceException.d.ts +1 -0
  73. package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/signin/models/errors.d.ts +4 -0
  74. package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/sso/models/SSOServiceException.d.ts +1 -0
  75. package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/sso/models/errors.d.ts +4 -0
  76. package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/sso-oidc/models/SSOOIDCServiceException.d.ts +1 -0
  77. package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/sso-oidc/models/errors.d.ts +11 -0
  78. package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/sts/models/STSServiceException.d.ts +1 -0
  79. package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/sts/models/errors.d.ts +7 -0
  80. package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/cognito-identity/models/CognitoIdentityServiceException.d.ts +1 -0
  81. package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/cognito-identity/models/errors.d.ts +9 -0
  82. package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/signin/models/SigninServiceException.d.ts +1 -0
  83. package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/signin/models/errors.d.ts +4 -0
  84. package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/sso/models/SSOServiceException.d.ts +1 -0
  85. package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/sso/models/errors.d.ts +4 -0
  86. package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/sso-oidc/models/SSOOIDCServiceException.d.ts +1 -0
  87. package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/sso-oidc/models/errors.d.ts +11 -0
  88. package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/sts/models/STSServiceException.d.ts +1 -0
  89. package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/sts/models/errors.d.ts +7 -0
  90. package/node_modules/@aws-sdk/nested-clients/package.json +4 -4
  91. package/node_modules/@aws-sdk/signature-v4-multi-region/package.json +2 -2
  92. package/node_modules/@aws-sdk/token-providers/package.json +4 -4
  93. package/node_modules/@smithy/core/dist-cjs/submodules/client/index.js +33 -11
  94. package/node_modules/@smithy/core/dist-cjs/submodules/endpoints/index.browser.js +2 -1
  95. package/node_modules/@smithy/core/dist-cjs/submodules/endpoints/index.js +2 -1
  96. package/node_modules/@smithy/core/dist-cjs/submodules/schema/index.js +3 -0
  97. package/node_modules/@smithy/core/dist-es/submodules/client/smithy-client/exceptions.js +33 -11
  98. package/node_modules/@smithy/core/dist-es/submodules/endpoints/util-endpoints/cache/EndpointCache.js +2 -1
  99. package/node_modules/@smithy/core/dist-es/submodules/schema/TypeRegistry.js +3 -0
  100. package/node_modules/@smithy/core/dist-types/submodules/client/smithy-client/exceptions.d.ts +1 -0
  101. package/node_modules/@smithy/core/dist-types/ts3.4/submodules/client/smithy-client/exceptions.d.ts +1 -0
  102. package/node_modules/@smithy/core/package.json +1 -1
  103. package/node_modules/@smithy/signature-v4/dist-cjs/index.js +4 -1
  104. package/node_modules/@smithy/signature-v4/dist-es/getCanonicalHeaders.js +4 -1
  105. package/node_modules/@smithy/signature-v4/package.json +3 -3
  106. package/node_modules/mailparser/.release-please-manifest.json +1 -1
  107. package/node_modules/mailparser/CHANGELOG.md +7 -0
  108. package/node_modules/mailparser/package.json +2 -2
  109. package/node_modules/nodemailer/CHANGELOG.md +27 -0
  110. package/node_modules/nodemailer/dist/cjs/base64/index.d.ts +3 -0
  111. package/node_modules/nodemailer/dist/cjs/base64/index.js +80 -41
  112. package/node_modules/nodemailer/dist/cjs/dkim/index.js +43 -29
  113. package/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +2 -2
  114. package/node_modules/nodemailer/dist/cjs/errors.d.ts +17 -0
  115. package/node_modules/nodemailer/dist/cjs/errors.js +12 -1
  116. package/node_modules/nodemailer/dist/cjs/fetch/index.js +23 -3
  117. package/node_modules/nodemailer/dist/cjs/mailer/index.js +23 -7
  118. package/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +5 -0
  119. package/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +20 -0
  120. package/node_modules/nodemailer/dist/cjs/mime-node/index.d.ts +1 -1
  121. package/node_modules/nodemailer/dist/cjs/mime-node/index.js +165 -105
  122. package/node_modules/nodemailer/dist/cjs/nodemailer.js +12 -9
  123. package/node_modules/nodemailer/dist/cjs/package-info.d.ts +1 -1
  124. package/node_modules/nodemailer/dist/cjs/package-info.js +1 -1
  125. package/node_modules/nodemailer/dist/cjs/qp/index.d.ts +2 -0
  126. package/node_modules/nodemailer/dist/cjs/qp/index.js +103 -33
  127. package/node_modules/nodemailer/dist/cjs/sendmail-transport/index.js +54 -14
  128. package/node_modules/nodemailer/dist/cjs/shared/index.d.ts +32 -1
  129. package/node_modules/nodemailer/dist/cjs/shared/index.js +212 -115
  130. package/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +3 -1
  131. package/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +88 -68
  132. package/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +7 -1
  133. package/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +348 -136
  134. package/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +3 -0
  135. package/node_modules/nodemailer/dist/cjs/smtp-pool/index.js +7 -1
  136. package/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.js +106 -17
  137. package/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +8 -2
  138. package/node_modules/nodemailer/dist/esm/base64/index.d.ts +3 -0
  139. package/node_modules/nodemailer/dist/esm/base64/index.js +80 -41
  140. package/node_modules/nodemailer/dist/esm/dkim/index.js +43 -29
  141. package/node_modules/nodemailer/dist/esm/dkim/message-parser.js +2 -2
  142. package/node_modules/nodemailer/dist/esm/errors.d.ts +17 -0
  143. package/node_modules/nodemailer/dist/esm/errors.js +10 -0
  144. package/node_modules/nodemailer/dist/esm/fetch/index.js +23 -3
  145. package/node_modules/nodemailer/dist/esm/mailer/index.js +23 -7
  146. package/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +5 -0
  147. package/node_modules/nodemailer/dist/esm/mailer/mail-message.js +20 -0
  148. package/node_modules/nodemailer/dist/esm/mime-node/index.d.ts +1 -1
  149. package/node_modules/nodemailer/dist/esm/mime-node/index.js +166 -106
  150. package/node_modules/nodemailer/dist/esm/nodemailer.js +12 -9
  151. package/node_modules/nodemailer/dist/esm/package-info.d.ts +1 -1
  152. package/node_modules/nodemailer/dist/esm/package-info.js +1 -1
  153. package/node_modules/nodemailer/dist/esm/qp/index.d.ts +2 -0
  154. package/node_modules/nodemailer/dist/esm/qp/index.js +103 -33
  155. package/node_modules/nodemailer/dist/esm/sendmail-transport/index.js +54 -14
  156. package/node_modules/nodemailer/dist/esm/shared/index.d.ts +32 -1
  157. package/node_modules/nodemailer/dist/esm/shared/index.js +207 -115
  158. package/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +3 -1
  159. package/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.js +88 -68
  160. package/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +7 -1
  161. package/node_modules/nodemailer/dist/esm/smtp-connection/index.js +348 -136
  162. package/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +3 -0
  163. package/node_modules/nodemailer/dist/esm/smtp-pool/index.js +7 -1
  164. package/node_modules/nodemailer/dist/esm/smtp-pool/pool-resource.js +106 -17
  165. package/node_modules/nodemailer/dist/esm/smtp-transport/index.js +8 -2
  166. package/node_modules/nodemailer/package.json +1 -1
  167. package/node_modules/uuid/package.json +17 -17
  168. package/package.json +9 -9
@@ -7,12 +7,30 @@ import crypto from 'node:crypto';
7
7
  import DataStream from './data-stream.js';
8
8
  import { PassThrough } from 'node:stream';
9
9
  import * as shared from '../shared/index.js';
10
+ import { ERR_ACCESS_DENIED, isTransientError } from '../errors.js';
10
11
  // default timeout values in ms
11
12
  const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
12
13
  const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
13
14
  const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
14
15
  const DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
16
+ const CLOSE_TIMEOUT = 5 * 1000; // how much to wait for the server to close its side after we closed ours
17
+ const KEEPALIVE_DELAY = 30 * 1000; // idle time before TCP keepalive probes start, keeps NAT mappings of idle connections alive
15
18
  const TEARDOWN_NOOP = () => { }; // reusable no-op handler for absorbing errors during socket teardown
19
+ // Random order, so that connections spread over the addresses of a host
20
+ function shuffle(list) {
21
+ const result = list.slice();
22
+ for (let i = result.length - 1; i > 0; i--) {
23
+ const j = Math.floor(Math.random() * (i + 1));
24
+ [result[i], result[j]] = [result[j], result[i]];
25
+ }
26
+ return result;
27
+ }
28
+ // Every timeout is reported with the ETIMEDOUT code, timeoutType tells which one it was
29
+ function timeoutError(message, timeoutType) {
30
+ const err = new Error(message);
31
+ err.timeoutType = timeoutType;
32
+ return err;
33
+ }
16
34
  // how many bytes a single server response may occupy while it is still being received.
17
35
  // Generous compared to any real reply, it only stops a peer that never completes one
18
36
  const MAX_RESPONSE_SIZE = 1024 * 1024;
@@ -135,8 +153,8 @@ class SMTPConnection extends EventEmitter {
135
153
  this._maxAllowedSize = 0;
136
154
  this._responseActions = [];
137
155
  this._recipientQueue = [];
138
- this._greetingTimeout = false;
139
- this._connectionTimeout = false;
156
+ this._phaseTimer = false;
157
+ this._plaintextEhlo = false;
140
158
  this._destroyed = false;
141
159
  this._closing = false;
142
160
  this._currentDataStream = false;
@@ -147,14 +165,25 @@ class SMTPConnection extends EventEmitter {
147
165
  this._onSocketClose = () => this._onClose();
148
166
  this._onSocketEnd = () => this._onEnd();
149
167
  this._onSocketTimeout = () => this._onTimeout();
150
- this._onConnectionSocketError = err => this._onConnectionError(err, 'ESOCKET');
151
- this._connectionAttemptId = 0;
168
+ this._onConnectionSocketError = err => this._onError(err, 'ESOCKET', false, 'CONN');
152
169
  }
153
170
  /**
154
171
  * Creates a connection to a SMTP server and sets up connection
155
172
  * listener
156
173
  */
157
174
  connect(connectCallback) {
175
+ if (this._connectCalled && !this._destroyed) {
176
+ // A connection is opened once. A second call would open a second socket over the
177
+ // first one and run the session handlers of both against the same state
178
+ const err = this._formatError('Cannot connect - connect() was already called for this connection', 'ECONNECTION', false, 'API');
179
+ if (typeof connectCallback === 'function') {
180
+ setImmediate(() => connectCallback(err));
181
+ return;
182
+ }
183
+ this.logger.warn({ tnx: 'smtp' }, '%s', err.message);
184
+ return;
185
+ }
186
+ this._connectCalled = true;
158
187
  if (typeof connectCallback === 'function') {
159
188
  this._connectCallback = connectCallback;
160
189
  this.once('connect', () => {
@@ -169,11 +198,15 @@ class SMTPConnection extends EventEmitter {
169
198
  return connectCallback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'CONN'));
170
199
  }
171
200
  }
201
+ // connectionTimeout covers the whole of connecting: the DNS lookup and every address tried
202
+ const connectionTimeout = this.options.connectionTimeout || CONNECTION_TIMEOUT;
203
+ // a transport that first opened a proxy connection for this one sets when that started
204
+ this._connectionDeadline = (this._connectStartedAt || Date.now()) + connectionTimeout;
172
205
  let opts = {
173
206
  port: this.port,
174
207
  host: this.host,
175
208
  allowInternalNetworkInterfaces: this.allowInternalNetworkInterfaces,
176
- timeout: this.options.dnsTimeout || DNS_TIMEOUT
209
+ timeout: Math.min(this.options.dnsTimeout || DNS_TIMEOUT, connectionTimeout)
177
210
  };
178
211
  if (this.options.localAddress) {
179
212
  opts.localAddress = this.options.localAddress;
@@ -202,7 +235,6 @@ class SMTPConnection extends EventEmitter {
202
235
  return this._resolveAndConnect(opts, _resolved => {
203
236
  try {
204
237
  this._socket.connect(this.port, this.host, () => {
205
- this._socket.setKeepAlive(true);
206
238
  // a `secure` connection over a caller-provided socket must still
207
239
  // perform the TLS handshake, otherwise AUTH and the message body
208
240
  // would be sent in cleartext despite the caller requesting TLS
@@ -234,9 +266,40 @@ class SMTPConnection extends EventEmitter {
234
266
  }
235
267
  }
236
268
  return this._resolveAndConnect(opts, resolved => {
237
- // Store fallback addresses for retry on connection failure
238
- this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
239
- this._connectOpts = Object.assign({}, opts);
269
+ let addresses = resolved._addresses || [];
270
+ if (opts.localAddress) {
271
+ // a socket bound to an address of one family can not reach the other one
272
+ const localFamily = net.isIPv6(opts.localAddress) ? 6 : 4;
273
+ const sameFamily = addresses.filter(addr => net.isIP(addr) === localFamily);
274
+ addresses = sameFamily.length ? sameFamily : addresses;
275
+ }
276
+ if (addresses.length > 1) {
277
+ // net.connect tries the addresses in turn and moves on to the next one when an
278
+ // attempt fails or takes too long. With both families it starts on IPv6 and
279
+ // alternates (RFC 8305), so a host with a broken IPv6 path costs a fraction of
280
+ // a second instead of a whole connection timeout
281
+ const ipv6 = addresses.filter(addr => net.isIPv6(addr));
282
+ const ipv4 = addresses.filter(addr => !net.isIPv6(addr));
283
+ const ordered = shuffle(ipv6).concat(shuffle(ipv4));
284
+ opts.host = this.host;
285
+ opts.autoSelectFamily = true;
286
+ if (!ipv6.length || !ipv4.length) {
287
+ // a slow address of the only family gets its share of the time, not the
288
+ // quarter second meant for an address family that does not work
289
+ const remaining = this._connectionDeadline - Date.now();
290
+ opts.autoSelectFamilyAttemptTimeout = Math.max(Math.floor(remaining / ordered.length), 10);
291
+ }
292
+ opts.lookup = ((hostname, lookupOptions, callback) => {
293
+ if (lookupOptions && lookupOptions.all) {
294
+ const all = ordered.map(address => ({ address, family: net.isIPv6(address) ? 6 : 4 }));
295
+ return setImmediate(() => callback(null, all));
296
+ }
297
+ setImmediate(() => callback(null, ordered[0], net.isIPv6(ordered[0]) ? 6 : 4));
298
+ });
299
+ }
300
+ else if (addresses.length) {
301
+ opts.host = addresses[0];
302
+ }
240
303
  this._connectToHost(opts, this.secureConnection);
241
304
  });
242
305
  }
@@ -283,20 +346,11 @@ class SMTPConnection extends EventEmitter {
283
346
  if (this._destroyed || this._closing) {
284
347
  return;
285
348
  }
286
- this._connectionAttemptId++;
287
- const currentAttemptId = this._connectionAttemptId;
288
349
  const connectFn = secure
289
350
  ? tls.connect
290
351
  : net.connect;
291
352
  try {
292
- this._socket = connectFn(opts, () => {
293
- // Ignore callback if this is a stale connection attempt
294
- if (this._connectionAttemptId !== currentAttemptId) {
295
- return;
296
- }
297
- this._socket.setKeepAlive(true);
298
- this._onConnect();
299
- });
353
+ this._socket = connectFn(opts, () => this._onConnect());
300
354
  this._setupConnectionHandlers();
301
355
  }
302
356
  catch (E) {
@@ -309,51 +363,37 @@ class SMTPConnection extends EventEmitter {
309
363
  * @internal
310
364
  */
311
365
  _setupConnectionHandlers() {
312
- this._connectionTimeout = setTimeout(() => {
313
- this._onConnectionError('Connection timeout', 'ETIMEDOUT');
314
- }, this.options.connectionTimeout || CONNECTION_TIMEOUT);
366
+ this._startPhase(Math.max((this._connectionDeadline || Date.now()) - Date.now(), 0), timeoutError('Connection timeout', 'CONNECT_TIMEOUT'));
315
367
  this._socket.on('error', this._onConnectionSocketError);
316
368
  }
317
369
  /**
318
- * Handles connection errors with fallback to alternative addresses
370
+ * Starts the timer of a connection phase: connecting, waiting for the greeting or a TLS
371
+ * upgrade. The phases follow one another, so starting one ends the one before
319
372
  *
320
- * @param err Error object or message
321
- * @param code Error code
373
+ * @param timeout Time the phase may take
374
+ * @param err Error to fail with when it takes longer
322
375
  * @internal
323
376
  */
324
- _onConnectionError(err, code) {
325
- clearTimeout(this._connectionTimeout);
326
- // Check if we have fallback addresses to try
327
- const canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
328
- if (!canFallback) {
329
- // No more fallback addresses, report the error
330
- this._onError(err, code, false, 'CONN');
331
- return;
332
- }
333
- const nextHost = this._fallbackAddresses.shift();
334
- this.logger.info({
335
- tnx: 'network',
336
- failedHost: this._connectOpts.host,
337
- nextHost,
338
- error: err.message || err
339
- }, 'Connection to %s failed, trying %s', this._connectOpts.host, nextHost);
340
- // Clean up current socket
341
- if (this._socket) {
342
- try {
343
- this._socket.removeListener('error', this._onConnectionSocketError);
344
- // Absorb any late teardown error (e.g. a TLS fallback socket emitting
345
- // after destroy), mirroring the guard used in close()
346
- this._socket.on('error', TEARDOWN_NOOP);
347
- this._socket.destroy();
348
- }
349
- catch (_E) {
350
- // ignore
351
- }
352
- this._socket = null;
353
- }
354
- // Update host and retry
355
- this._connectOpts.host = nextHost;
356
- this._connectToHost(this._connectOpts, this.secureConnection);
377
+ _startPhase(timeout, err) {
378
+ this._clearPhase();
379
+ this._phaseTimer = setTimeout(() => {
380
+ this._phaseTimer = false;
381
+ this._onError(err, 'ETIMEDOUT', false, 'CONN');
382
+ }, timeout);
383
+ }
384
+ /**
385
+ * Time the greeting or a STARTTLS upgrade may take: greetingTimeout, cut short by what is left
386
+ * of connectionTimeout
387
+ *
388
+ * @internal
389
+ */
390
+ _remainingSetupTime() {
391
+ return Math.min(this.options.greetingTimeout || GREETING_TIMEOUT, Math.max((this._connectionDeadline || Infinity) - Date.now(), 1));
392
+ }
393
+ /** @internal */
394
+ _clearPhase() {
395
+ clearTimeout(this._phaseTimer);
396
+ this._phaseTimer = false;
357
397
  }
358
398
  /**
359
399
  * Sends QUIT
@@ -366,8 +406,7 @@ class SMTPConnection extends EventEmitter {
366
406
  * Closes the connection to the server
367
407
  */
368
408
  close() {
369
- clearTimeout(this._connectionTimeout);
370
- clearTimeout(this._greetingTimeout);
409
+ this._clearPhase();
371
410
  this._responseActions = [];
372
411
  // allow to run this function only once
373
412
  if (this._closing) {
@@ -390,13 +429,15 @@ class SMTPConnection extends EventEmitter {
390
429
  }
391
430
  this._currentDataStream = false;
392
431
  }
393
- // Detach from the message stream as well. The listener is swapped for a no-op rather than
394
- // removed, a stream destroyed with an error later on would otherwise throw it as unhandled
432
+ // Detach from the message stream as well and release whatever it reads from, the message
433
+ // can not be sent over this connection anymore. The listener is swapped for a no-op rather
434
+ // than removed, a stream destroyed with an error would otherwise throw it as unhandled
395
435
  if (this._pendingSend) {
396
436
  const { stream, onStreamError } = this._pendingSend;
397
437
  if (stream) {
398
438
  stream.removeListener('error', onStreamError);
399
439
  stream.on('error', TEARDOWN_NOOP);
440
+ stream.destroy();
400
441
  }
401
442
  this._pendingSend = false;
402
443
  }
@@ -415,6 +456,15 @@ class SMTPConnection extends EventEmitter {
415
456
  // sending cleartext after TLS shutdown triggers ERR_SSL_BAD_RECORD_TYPE)
416
457
  socket.on('error', TEARDOWN_NOOP);
417
458
  socket[closeMethod]();
459
+ if (closeMethod === 'end') {
460
+ // end() only closes our side, a server that never closes its own would keep
461
+ // the socket, and the process with it, around for good
462
+ const closeTimer = setTimeout(() => socket.destroy(), CLOSE_TIMEOUT);
463
+ if (typeof closeTimer.unref === 'function') {
464
+ closeTimer.unref();
465
+ }
466
+ socket.once('close', () => clearTimeout(closeTimer));
467
+ }
418
468
  }
419
469
  catch (_E) {
420
470
  // just ignore
@@ -619,14 +669,9 @@ class SMTPConnection extends EventEmitter {
619
669
  const startTime = Date.now();
620
670
  this._setEnvelope(envelope, (err, info) => {
621
671
  if (err) {
622
- // create passthrough stream to consume to prevent OOM
623
- const stream = new PassThrough();
624
- if (typeof message.pipe === 'function') {
625
- message.pipe(stream);
626
- }
627
- else {
628
- stream.write(message);
629
- stream.end();
672
+ // the message is not going to be sent, release whatever the stream reads from
673
+ if (typeof message.destroy === 'function') {
674
+ message.destroy();
630
675
  }
631
676
  return callback(err);
632
677
  }
@@ -680,7 +725,7 @@ class SMTPConnection extends EventEmitter {
680
725
  */
681
726
  _onConnect() {
682
727
  const socket = this._socket;
683
- clearTimeout(this._connectionTimeout);
728
+ this._clearPhase();
684
729
  this.logger.info({
685
730
  tnx: 'network',
686
731
  localAddress: socket.localAddress,
@@ -710,16 +755,42 @@ class SMTPConnection extends EventEmitter {
710
755
  socket.once('end', this._onSocketEnd);
711
756
  socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
712
757
  socket.on('timeout', this._onSocketTimeout);
713
- this._greetingTimeout = setTimeout(() => {
714
- // if still waiting for greeting, give up
715
- if (this._socket && !this._destroyed && this._responseActions[0] === this._actionGreeting) {
716
- this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN');
717
- }
718
- }, this.options.greetingTimeout || GREETING_TIMEOUT);
758
+ // keepalive also covers sockets handed over by a proxy or by the caller
759
+ if (typeof socket.setKeepAlive === 'function') {
760
+ socket.setKeepAlive(true, KEEPALIVE_DELAY);
761
+ }
762
+ // Commands are written in the batches they belong to (see cork() for PIPELINING), Nagle
763
+ // would only hold a write back until the server acknowledged the previous one. Against a
764
+ // server that delays its ACKs that costs 40ms on every message
765
+ if (typeof socket.setNoDelay === 'function') {
766
+ socket.setNoDelay(true);
767
+ }
768
+ // bounded by greetingTimeout and by what is left of connectionTimeout, which covers setting
769
+ // the session up from the DNS lookup to the end of a STARTTLS upgrade
770
+ this._startPhase(this._remainingSetupTime(), timeoutError('Greeting never received', 'GREETING_TIMEOUT'));
719
771
  this._responseActions.push(this._actionGreeting);
720
772
  // we have a 'data' listener set up so resume socket if it was paused
721
773
  socket.resume();
722
774
  }
775
+ /**
776
+ * Ends the session after a 421 reply. The replies queued for commands sent along with the
777
+ * answered one are not going to come, so the message in flight is failed here
778
+ *
779
+ * @param str The 421 reply
780
+ * @internal
781
+ */
782
+ _onServerClosing(str) {
783
+ if (this._destroyed) {
784
+ return;
785
+ }
786
+ const pendingSend = this._pendingSend;
787
+ const envelope = this._envelope;
788
+ this._responseActions = [];
789
+ this.close();
790
+ if (pendingSend) {
791
+ pendingSend.callback((envelope && envelope.mailError) || this._formatError('Server closed the connection', 'ECONNECTION', str, 'CONN'));
792
+ }
793
+ }
723
794
  /**
724
795
  * 'data' listener for data coming from the server
725
796
  *
@@ -794,8 +865,7 @@ class SMTPConnection extends EventEmitter {
794
865
  * @internal
795
866
  */
796
867
  _onError(err, type, data, command) {
797
- clearTimeout(this._connectionTimeout);
798
- clearTimeout(this._greetingTimeout);
868
+ this._clearPhase();
799
869
  if (this._destroyed) {
800
870
  // just ignore, already closed
801
871
  // this might happen when a socket is canceled because of reached timeout
@@ -803,12 +873,12 @@ class SMTPConnection extends EventEmitter {
803
873
  return;
804
874
  }
805
875
  err = this._formatError(err, type, data, command);
806
- const transientCodes = ['ETIMEDOUT', 'ESOCKET', 'ECONNECTION'];
807
- if (transientCodes.includes(err.code)) {
808
- this.logger.warn(data, err.message);
876
+ // the message carries the server response, it is an argument and not the format string
877
+ if (isTransientError(err)) {
878
+ this.logger.warn({ tnx: 'smtp', err }, '%s', err.message);
809
879
  }
810
880
  else {
811
- this.logger.error(data, err.message);
881
+ this.logger.error({ tnx: 'smtp', err }, '%s', err.message);
812
882
  }
813
883
  // close() forgets the send in flight, it is completed with this same error afterwards so
814
884
  // a late message stream error has nothing left to report
@@ -828,7 +898,12 @@ class SMTPConnection extends EventEmitter {
828
898
  else {
829
899
  err = new Error(message);
830
900
  }
831
- if (type && type !== 'Error') {
901
+ // a permission model denial keeps its own code, see ERR_ACCESS_DENIED
902
+ if (type && type !== 'Error' && err.code !== ERR_ACCESS_DENIED) {
903
+ // the code of a system error, such as ECONNREFUSED, still tells what happened
904
+ if (err.code && err.code !== type && !err.originalCode) {
905
+ err.originalCode = err.code;
906
+ }
832
907
  err.code = type;
833
908
  }
834
909
  if (response) {
@@ -880,6 +955,19 @@ class SMTPConnection extends EventEmitter {
880
955
  this.close();
881
956
  return;
882
957
  }
958
+ if (!failureResponse &&
959
+ this.stage === 'connected' &&
960
+ !this._responseActions.length &&
961
+ !this._pendingSend &&
962
+ !this._destroyed &&
963
+ !this._closing) {
964
+ // nothing was waiting for the server, so this is the server ending an idle session
965
+ // (usually after its idle timeout), not a failure
966
+ this.logger.info({
967
+ tnx: 'network'
968
+ }, 'Server closed the idle connection');
969
+ return this._destroy();
970
+ }
883
971
  if (failureResponse || (this._responseActions[0] !== this.close && !this._destroyed)) {
884
972
  return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', failureResponse, 'CONN');
885
973
  }
@@ -905,7 +993,7 @@ class SMTPConnection extends EventEmitter {
905
993
  * @internal
906
994
  */
907
995
  _onTimeout() {
908
- return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN');
996
+ return this._onError(timeoutError('Timeout', 'SOCKET_TIMEOUT'), 'ETIMEDOUT', false, 'CONN');
909
997
  }
910
998
  /**
911
999
  * Destroys the client, emits 'end'
@@ -920,10 +1008,7 @@ class SMTPConnection extends EventEmitter {
920
1008
  this.destroyed = true;
921
1009
  // a connection the server dropped before the greeting would otherwise keep
922
1010
  // the greeting timer, and with it the process, alive until it fires
923
- clearTimeout(this._connectionTimeout);
924
- clearTimeout(this._greetingTimeout);
925
- this._connectionTimeout = false;
926
- this._greetingTimeout = false;
1011
+ this._clearPhase();
927
1012
  this.emit('end');
928
1013
  }
929
1014
  /**
@@ -940,6 +1025,15 @@ class SMTPConnection extends EventEmitter {
940
1025
  // inject plaintext bytes after the "220" reply (e.g. a CRLF-free fragment that
941
1026
  // would otherwise be prepended to the first post-TLS response and parsed as
942
1027
  // part of the secured EHLO capabilities). STARTTLS response injection.
1028
+ const discarded = this._remainder.length + this._responseQueue.reduce((total, response) => total + response.length, 0);
1029
+ if (discarded) {
1030
+ // a server does not send anything here on its own, this is worth knowing about
1031
+ this.logger.warn({
1032
+ tnx: 'smtp',
1033
+ discarded
1034
+ }, 'Discarded %s bytes received in plaintext after the STARTTLS response', discarded);
1035
+ }
1036
+ this._plaintextEhlo = false;
943
1037
  this._remainder = '';
944
1038
  this._responseQueue = [];
945
1039
  this._responsePartial = false;
@@ -949,6 +1043,7 @@ class SMTPConnection extends EventEmitter {
949
1043
  const socketPlain = this._socket;
950
1044
  socketPlain.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards
951
1045
  socketPlain.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object
1046
+ socketPlain.setTimeout(0);
952
1047
  const opts = Object.assign({
953
1048
  socket: socketPlain,
954
1049
  host: this.host
@@ -969,9 +1064,14 @@ class SMTPConnection extends EventEmitter {
969
1064
  socketPlain.removeListener('error', this._onConnectionSocketError);
970
1065
  };
971
1066
  this.upgrading = true;
1067
+ // the socket timeout only notices a server that sends nothing at all, a handshake that
1068
+ // trickles along would otherwise hold the connection for as long as the server likes
1069
+ // STARTTLS is the last step of setting the session up
1070
+ this._startPhase(this._remainingSetupTime(), timeoutError('TLS handshake timed out', 'UPGRADE_TIMEOUT'));
972
1071
  // tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
973
1072
  try {
974
1073
  this._socket = tls.connect(opts, () => {
1074
+ this._clearPhase();
975
1075
  this.secure = true;
976
1076
  this.upgrading = false;
977
1077
  this._socket.on('data', this._onSocketData);
@@ -980,6 +1080,7 @@ class SMTPConnection extends EventEmitter {
980
1080
  });
981
1081
  }
982
1082
  catch (err) {
1083
+ this._clearPhase();
983
1084
  removePlainSocketListeners();
984
1085
  return callback(err);
985
1086
  }
@@ -1020,10 +1121,24 @@ class SMTPConnection extends EventEmitter {
1020
1121
  }, str.replace(/\r?\n$/, ''));
1021
1122
  }
1022
1123
  const action = this._responseActions.shift();
1124
+ // RFC 5321 4.2: 421 means the server is about to close the connection, whatever it answers
1125
+ const closing = /^421[ -]/.test(str);
1023
1126
  if (typeof action === 'function') {
1127
+ // the command gets its own error first, the code tells which step failed
1024
1128
  action.call(this, str);
1129
+ if (closing) {
1130
+ return this._onServerClosing(str);
1131
+ }
1025
1132
  setImmediate(() => this._processResponse());
1026
1133
  }
1134
+ else if (closing && !this._pendingSend && this.stage === 'connected') {
1135
+ // RFC 5321 4.2: a server may send 421 at any time when it is about to close the
1136
+ // connection. Nothing was waiting for a reply, so this ends an idle session
1137
+ this.logger.info({
1138
+ tnx: 'smtp'
1139
+ }, 'Server closed the idle connection: %s', str);
1140
+ this.close();
1141
+ }
1027
1142
  else {
1028
1143
  return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN');
1029
1144
  }
@@ -1118,6 +1233,9 @@ class SMTPConnection extends EventEmitter {
1118
1233
  return callback(this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM'));
1119
1234
  }
1120
1235
  }
1236
+ // RFC 2920: with PIPELINING the whole envelope and DATA go out without waiting for the
1237
+ // replies in between, which saves two round trips for every message
1238
+ this._envelope.pipelined = this._supportedExtensions.includes('PIPELINING');
1121
1239
  this._responseActions.push(str => {
1122
1240
  this._actionMAIL(str, callback);
1123
1241
  });
@@ -1155,7 +1273,38 @@ class SMTPConnection extends EventEmitter {
1155
1273
  if (this._envelope.requireTLSExtensionEnabled) {
1156
1274
  args.push('REQUIRETLS');
1157
1275
  }
1158
- this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : ''));
1276
+ const mailFrom = 'MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : '');
1277
+ this._recipientQueue = [];
1278
+ if (!this._envelope.pipelined) {
1279
+ this._sendCommand(mailFrom);
1280
+ return;
1281
+ }
1282
+ // corked, so the batch leaves in one segment instead of the first command alone
1283
+ const socket = this._socket;
1284
+ socket.cork();
1285
+ this._sendCommand(mailFrom);
1286
+ while (this._envelope.rcptQueue.length) {
1287
+ this._sendRcpt(this._envelope.rcptQueue.shift(), callback);
1288
+ }
1289
+ this._responseActions.push(str => {
1290
+ this._actionDATA(str, callback);
1291
+ });
1292
+ this._sendCommand('DATA');
1293
+ socket.uncork();
1294
+ }
1295
+ /**
1296
+ * Sends RCPT TO for a recipient and queues the handler for the reply
1297
+ *
1298
+ * @param recipient Recipient address
1299
+ * @param callback Callback to run once the envelope is processed
1300
+ * @internal
1301
+ */
1302
+ _sendRcpt(recipient, callback) {
1303
+ this._recipientQueue.push(recipient);
1304
+ this._responseActions.push(str => {
1305
+ this._actionRCPT(str, callback);
1306
+ });
1307
+ this._sendCommand('RCPT TO:<' + recipient + '>' + this._getDsnRcptToArgs());
1159
1308
  }
1160
1309
  /** @internal */
1161
1310
  _setDsnEnvelope(params) {
@@ -1271,7 +1420,7 @@ class SMTPConnection extends EventEmitter {
1271
1420
  * @internal
1272
1421
  */
1273
1422
  _actionGreeting(str) {
1274
- clearTimeout(this._greetingTimeout);
1423
+ this._clearPhase();
1275
1424
  if (str.substr(0, 3) !== '220') {
1276
1425
  this._onError(new Error('Invalid greeting. response=' + str), 'EPROTOCOL', str, 'CONN');
1277
1426
  return;
@@ -1309,7 +1458,6 @@ class SMTPConnection extends EventEmitter {
1309
1458
  * @internal
1310
1459
  */
1311
1460
  _actionEHLO(str) {
1312
- let match;
1313
1461
  if (str.substr(0, 3) === '421') {
1314
1462
  this._onError(new Error('Server terminates connection. response=' + str), 'ECONNECTION', str, 'EHLO');
1315
1463
  return;
@@ -1324,17 +1472,30 @@ class SMTPConnection extends EventEmitter {
1324
1472
  this._sendCommand('HELO ' + this.name);
1325
1473
  return;
1326
1474
  }
1327
- this._ehloLines = str
1328
- .split(/\r?\n/)
1329
- .map(line => line.replace(/^\d+[ -]/, '').trim())
1330
- .filter(line => line)
1331
- .slice(1);
1332
1475
  // Detect if the server supports STARTTLS
1333
1476
  if (!this.secure && !this.options.ignoreTLS && (/[ -]STARTTLS\b/im.test(str) || this.options.requireTLS)) {
1477
+ // kept for opportunisticTLS, a session that stays in plaintext still has these extensions
1478
+ this._plaintextEhlo = str;
1334
1479
  this._sendCommand('STARTTLS');
1335
1480
  this._responseActions.push(this._actionSTARTTLS);
1336
1481
  return;
1337
1482
  }
1483
+ this._parseEhloExtensions(str);
1484
+ this.emit('connect');
1485
+ }
1486
+ /**
1487
+ * Reads the extensions and the authentication mechanisms out of an EHLO response
1488
+ *
1489
+ * @param str EHLO response from the server
1490
+ * @internal
1491
+ */
1492
+ _parseEhloExtensions(str) {
1493
+ let match;
1494
+ this._ehloLines = str
1495
+ .split(/\r?\n/)
1496
+ .map(line => line.replace(/^\d+[ -]/, '').trim())
1497
+ .filter(line => line)
1498
+ .slice(1);
1338
1499
  // Detect if the server supports SMTPUTF8
1339
1500
  if (/[ -]SMTPUTF8\b/im.test(str)) {
1340
1501
  this._supportedExtensions.push('SMTPUTF8');
@@ -1384,7 +1545,6 @@ class SMTPConnection extends EventEmitter {
1384
1545
  this._supportedExtensions.push('SIZE');
1385
1546
  this._maxAllowedSize = Number(match[1]) || 0;
1386
1547
  }
1387
- this.emit('connect');
1388
1548
  }
1389
1549
  /**
1390
1550
  * Handles server response for HELO command. If it yielded in
@@ -1416,6 +1576,14 @@ class SMTPConnection extends EventEmitter {
1416
1576
  this.logger.info({
1417
1577
  tnx: 'smtp'
1418
1578
  }, 'Failed STARTTLS upgrade, continuing unencrypted');
1579
+ // the plaintext session goes on with what the server announced for it, except for
1580
+ // AUTH: credentials are not sent over a connection that failed to encrypt
1581
+ if (this._plaintextEhlo) {
1582
+ this._parseEhloExtensions(this._plaintextEhlo);
1583
+ this._plaintextEhlo = false;
1584
+ this.allowsAuth = false;
1585
+ this._supportedAuth = [];
1586
+ }
1419
1587
  this.emit('connect');
1420
1588
  return;
1421
1589
  }
@@ -1596,21 +1764,9 @@ class SMTPConnection extends EventEmitter {
1596
1764
  const message = this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(envelope.from)
1597
1765
  ? 'Internationalized mailbox name not allowed'
1598
1766
  : 'Mail command failed';
1599
- return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
1600
- }
1601
- if (!envelope.rcptQueue.length) {
1602
- return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API'));
1767
+ envelope.mailError = this._formatError(message, 'EENVELOPE', str, 'MAIL FROM');
1603
1768
  }
1604
- this._recipientQueue = [];
1605
- const usePipelining = this._supportedExtensions.includes('PIPELINING');
1606
- do {
1607
- const curRecipient = envelope.rcptQueue.shift();
1608
- this._recipientQueue.push(curRecipient);
1609
- this._responseActions.push(str => {
1610
- this._actionRCPT(str, callback);
1611
- });
1612
- this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
1613
- } while (usePipelining && envelope.rcptQueue.length);
1769
+ this._advanceEnvelope(str, callback);
1614
1770
  }
1615
1771
  /**
1616
1772
  * Handle response for a RCPT TO: command
@@ -1637,32 +1793,67 @@ class SMTPConnection extends EventEmitter {
1637
1793
  else {
1638
1794
  envelope.accepted.push(curRecipient);
1639
1795
  }
1640
- if (!envelope.rcptQueue.length && !this._recipientQueue.length) {
1641
- if (envelope.rejected.length < envelope.to.length) {
1642
- this._responseActions.push(str => {
1643
- this._actionDATA(str, callback);
1644
- });
1645
- this._sendCommand('DATA');
1646
- }
1647
- else {
1648
- // report a temporary rejection when there is one, taking the last reply would mark the
1649
- // whole message as permanently failed although some recipients were only deferred
1650
- const deferred = envelope.rejectedErrors.find(rejectedErr => rejectedErr.responseCode && rejectedErr.responseCode < 500);
1651
- const reply = deferred?.response ?? str;
1652
- err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', reply, 'RCPT TO');
1653
- err.rejected = envelope.rejected;
1654
- err.rejectedErrors = envelope.rejectedErrors;
1655
- return callback(err);
1656
- }
1796
+ this._advanceEnvelope(str, callback);
1797
+ }
1798
+ /**
1799
+ * Moves the envelope on after a reply to MAIL FROM or RCPT TO. A pipelined envelope sent every
1800
+ * command at once and is decided by the reply to DATA. Otherwise the commands go one at a
1801
+ * time: the next recipient, or once every reply is in, DATA or the error that ends the message
1802
+ *
1803
+ * @param str The reply that was handled
1804
+ * @param callback Callback to run once the envelope is processed
1805
+ * @internal
1806
+ */
1807
+ _advanceEnvelope(str, callback) {
1808
+ const envelope = this._envelope;
1809
+ if (envelope.pipelined) {
1810
+ return;
1657
1811
  }
1658
- else if (envelope.rcptQueue.length) {
1659
- const nextRecipient = envelope.rcptQueue.shift();
1660
- this._recipientQueue.push(nextRecipient);
1661
- this._responseActions.push(str => {
1662
- this._actionRCPT(str, callback);
1663
- });
1664
- this._sendCommand('RCPT TO:<' + nextRecipient + '>' + this._getDsnRcptToArgs());
1812
+ if (envelope.mailError) {
1813
+ return callback(envelope.mailError);
1814
+ }
1815
+ if (envelope.rcptQueue.length) {
1816
+ return this._sendRcpt(envelope.rcptQueue.shift(), callback);
1817
+ }
1818
+ if (this._recipientQueue.length) {
1819
+ // replies still to come
1820
+ return;
1821
+ }
1822
+ const err = this._envelopeError(str);
1823
+ if (err) {
1824
+ return callback(err);
1665
1825
  }
1826
+ this._responseActions.push(str => {
1827
+ this._actionDATA(str, callback);
1828
+ });
1829
+ this._sendCommand('DATA');
1830
+ }
1831
+ /**
1832
+ * Decides how the envelope went once every reply to MAIL FROM and RCPT TO is in. Called after
1833
+ * the last RCPT TO reply, or with PIPELINING on the reply to the DATA command sent along
1834
+ *
1835
+ * @param str The reply being handled
1836
+ * @returns The error to fail the message with, or null when DATA can go ahead
1837
+ * @internal
1838
+ */
1839
+ _envelopeError(str) {
1840
+ const envelope = this._envelope;
1841
+ if (envelope.mailError) {
1842
+ return envelope.mailError;
1843
+ }
1844
+ if (envelope.accepted.length) {
1845
+ return null;
1846
+ }
1847
+ // report a temporary rejection when there is one, taking the last reply would mark the
1848
+ // whole message as permanently failed although some recipients were only deferred
1849
+ const deferred = envelope.rejectedErrors.find(rejectedErr => rejectedErr.responseCode && rejectedErr.responseCode < 500);
1850
+ const lastRejected = envelope.rejectedErrors[envelope.rejectedErrors.length - 1];
1851
+ const reply = deferred?.response ?? (envelope.pipelined && lastRejected ? lastRejected.response : str);
1852
+ // every recipient was rejected
1853
+ const err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', reply, 'RCPT TO');
1854
+ err.rejected = envelope.rejected;
1855
+ err.rejectedErrors = envelope.rejectedErrors;
1856
+ return err;
1666
1857
  }
1667
1858
  /**
1668
1859
  * Handle response for a DATA command
@@ -1673,6 +1864,27 @@ class SMTPConnection extends EventEmitter {
1673
1864
  */
1674
1865
  _actionDATA(str, callback) {
1675
1866
  const envelope = this._envelope;
1867
+ if (envelope.pipelined) {
1868
+ const err = this._envelopeError(str);
1869
+ if (err) {
1870
+ if (/^3/.test(str)) {
1871
+ if (envelope.mailError) {
1872
+ // A server that refused the sender has no transaction to end, one that
1873
+ // took DATA anyway can not be trusted with an empty message either.
1874
+ // Drop the connection instead of answering it
1875
+ this.close();
1876
+ return callback(err);
1877
+ }
1878
+ // A server must refuse DATA without an accepted recipient, this one took it
1879
+ // anyway. End the empty message, it has nobody to go to, so the session
1880
+ // stays usable
1881
+ this._responseActions.push(() => callback(err));
1882
+ this._sendCommand('.');
1883
+ return;
1884
+ }
1885
+ return callback(err);
1886
+ }
1887
+ }
1676
1888
  // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
1677
1889
  // some servers might use 250 instead, so lets check for 2 or 3 as the first digit
1678
1890
  if (!/^[23]/.test(str)) {