@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
@@ -19,6 +19,8 @@ export interface SMTPPoolOptions extends SMTPTransportOptions {
19
19
  rateDelta?: number | undefined;
20
20
  /** How many times a message is requeued when its connection closes while sending, defaults to 5, a negative value means unlimited */
21
21
  maxRequeues?: number | undefined;
22
+ /** Time in milliseconds a connection may stay idle before it is closed, defaults to 4 minutes, 0 keeps it open until the server closes it */
23
+ idleTimeout?: number | undefined;
22
24
  }
23
25
  /**
24
26
  * The pool options once the constructor has applied the defaults
@@ -27,6 +29,7 @@ export type SMTPPoolResolvedOptions = SMTPPoolOptions & {
27
29
  maxConnections: number;
28
30
  maxMessages: number;
29
31
  maxRequeues: number;
32
+ idleTimeout: number;
30
33
  };
31
34
  /**
32
35
  * Result of a message sent through the pool, same as for the SMTP transport
@@ -80,6 +80,9 @@ class SMTPPool extends node_events_1.EventEmitter {
80
80
  this.options.maxMessages = this.options.maxMessages || 100;
81
81
  // a default bound, a server that closes every connection before the greeting would otherwise be retried forever
82
82
  this.options.maxRequeues = typeof this.options.maxRequeues === 'number' ? this.options.maxRequeues : 5;
83
+ // below the 5 minutes RFC 5321 asks servers to wait at least, the connection is closed by us
84
+ // and not by the server in the middle of handing it a message
85
+ this.options.idleTimeout = typeof this.options.idleTimeout === 'number' ? this.options.idleTimeout : 4 * 60 * 1000;
83
86
  this.logger = shared.getLogger(this.options, {
84
87
  component: this.options.component || 'smtp-pool'
85
88
  });
@@ -486,7 +489,9 @@ class SMTPPool extends node_events_1.EventEmitter {
486
489
  // the error paths hand over the error alone
487
490
  const done = callback;
488
491
  const auth = new pool_resource_js_1.default(this).auth;
489
- this.getSocket(this.options, (err, socketOptions) => {
492
+ // the proxy handshake, if any, counts against connectionTimeout as well
493
+ const connectStartedAt = Date.now();
494
+ this.getSocket(Object.assign({}, this.options, { connectStartedAt }), (err, socketOptions) => {
490
495
  if (err) {
491
496
  return done(err);
492
497
  }
@@ -503,6 +508,7 @@ class SMTPPool extends node_events_1.EventEmitter {
503
508
  options = Object.assign(shared.assign(false, options), socketOptions);
504
509
  }
505
510
  const connection = new index_js_1.default(options);
511
+ connection._connectStartedAt = connectStartedAt;
506
512
  let returned = false;
507
513
  connection.once('error', err => {
508
514
  if (returned) {
@@ -90,6 +90,8 @@ class PoolResource extends node_events_1.EventEmitter {
90
90
  this.messages = 0;
91
91
  this.available = true;
92
92
  this._failed = false;
93
+ this._sending = false;
94
+ this._idleTimer = false;
93
95
  }
94
96
  /**
95
97
  * Emits 'error' for the first failure only. A dead resource can report the same failure more
@@ -109,7 +111,9 @@ class PoolResource extends node_events_1.EventEmitter {
109
111
  * @param callback Callback function to run once the connection is established or failed
110
112
  */
111
113
  connect(callback) {
112
- this.pool.getSocket(this.options, (err, socketOptions) => {
114
+ // the proxy handshake, if any, counts against connectionTimeout as well
115
+ const connectStartedAt = Date.now();
116
+ this.pool.getSocket(Object.assign({}, this.options, { connectStartedAt }), (err, socketOptions) => {
113
117
  if (err) {
114
118
  // nothing was connected, so no 'close' event is coming that would free the
115
119
  // slot this resource holds in the pool, report the failure the way a failed
@@ -131,7 +135,21 @@ class PoolResource extends node_events_1.EventEmitter {
131
135
  options = Object.assign((0, index_js_2.assign)(false, options), socketOptions);
132
136
  }
133
137
  this.connection = new index_js_1.default(options);
134
- this.connection.on('error', err => {
138
+ this.connection._connectStartedAt = connectStartedAt;
139
+ this.connection.on('error', (err) => {
140
+ if (this._sending) {
141
+ // the send callback gets the same error and decides what it means for the message
142
+ return;
143
+ }
144
+ if (this._connected && errors.isTransientError(err)) {
145
+ // the server ended a connection that had nothing in flight, usually after it
146
+ // was idle for a while. The 'end' that follows closes this resource
147
+ this.logger.info({
148
+ tnx: 'pool',
149
+ cid: this.id
150
+ }, 'Connection #%s was closed by the server: %s', this.id, err.message);
151
+ return;
152
+ }
135
153
  this._fail(err);
136
154
  if (returned) {
137
155
  return;
@@ -209,9 +227,39 @@ class PoolResource extends node_events_1.EventEmitter {
209
227
  if (mail.data.requireTLSExtensionEnabled) {
210
228
  envelope.requireTLSExtensionEnabled = mail.data.requireTLSExtensionEnabled;
211
229
  }
212
- this.connection.send(envelope, mail.message.createReadStream(), (err, info) => {
230
+ this._sending = true;
231
+ // a connection that sent messages before may have been dropped by the server in between
232
+ const reused = this.messages > 0;
233
+ const messageStream = mail.message.createReadStream();
234
+ this.connection.send(envelope, messageStream, (err, info) => {
235
+ this._sending = false;
213
236
  this.messages++;
214
237
  if (err) {
238
+ if (reused && messageStream.readableDidRead === false && errors.isTransientError(err)) {
239
+ // Not a byte of the message was sent, so it can go out over another
240
+ // connection. The pool requeues the message when this resource closes
241
+ this.logger.info({
242
+ tnx: 'pool',
243
+ cid: this.id,
244
+ messageId
245
+ }, 'Connection #%s was closed by the server before message %s was sent: %s', this.id, messageId, err.message);
246
+ this.connection.close();
247
+ return;
248
+ }
249
+ if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) &&
250
+ err.responseCode !== 421 &&
251
+ !this.connection._destroyed) {
252
+ // The server refused this message, the connection itself is fine. Reset the
253
+ // session and keep using it instead of opening a new one
254
+ this.connection.reset(resetErr => {
255
+ if (resetErr) {
256
+ this.connection.close();
257
+ return;
258
+ }
259
+ this._release();
260
+ });
261
+ return callback(err);
262
+ }
215
263
  this.connection.close();
216
264
  this._fail(err);
217
265
  return callback(err);
@@ -221,28 +269,69 @@ class PoolResource extends node_events_1.EventEmitter {
221
269
  to: envelope.to
222
270
  };
223
271
  info.messageId = messageId;
224
- setImmediate(() => {
225
- if (this.messages >= this.options.maxMessages) {
226
- const err = new Error('Resource exhausted');
227
- err.code = errors.EMAXLIMIT;
228
- this.connection.close();
229
- this._fail(err);
230
- }
231
- else {
232
- this.pool._checkRateLimit(() => {
233
- this.available = true;
234
- this.emit('available');
235
- });
236
- }
237
- });
272
+ setImmediate(() => this._release());
238
273
  callback(null, info);
239
274
  });
240
275
  }
276
+ /**
277
+ * Makes the connection available for the next message, or closes it once it has sent
278
+ * maxMessages messages
279
+ *
280
+ * @internal
281
+ */
282
+ _release() {
283
+ if (this.messages >= this.options.maxMessages) {
284
+ const err = new Error('Resource exhausted');
285
+ err.code = errors.EMAXLIMIT;
286
+ this.connection.close();
287
+ this._fail(err);
288
+ return;
289
+ }
290
+ this.pool._checkRateLimit(() => {
291
+ this.available = true;
292
+ this._startIdleTimer();
293
+ this.emit('available');
294
+ });
295
+ }
296
+ /** @internal */
297
+ _startIdleTimer() {
298
+ if (!this.options.idleTimeout || this.options.idleTimeout < 0) {
299
+ return;
300
+ }
301
+ // one timer per connection, restarted every time the connection becomes available. A
302
+ // connection that is busy when it fires is simply not closed
303
+ if (this._idleTimer && typeof this._idleTimer.refresh === 'function') {
304
+ this._idleTimer.refresh();
305
+ return;
306
+ }
307
+ clearTimeout(this._idleTimer);
308
+ this._idleTimer = setTimeout(() => {
309
+ if (!this.available) {
310
+ return;
311
+ }
312
+ this.logger.debug({
313
+ tnx: 'pool',
314
+ cid: this.id
315
+ }, 'Closing connection #%s after it was idle for %sms', this.id, this.options.idleTimeout);
316
+ // not handed another message while it says goodbye
317
+ this.available = false;
318
+ this.connection.quit();
319
+ }, this.options.idleTimeout);
320
+ if (typeof this._idleTimer.unref === 'function') {
321
+ this._idleTimer.unref();
322
+ }
323
+ }
324
+ /** @internal */
325
+ _stopIdleTimer() {
326
+ clearTimeout(this._idleTimer);
327
+ this._idleTimer = false;
328
+ }
241
329
  /**
242
330
  * Closes the connection
243
331
  */
244
332
  close() {
245
333
  this._connected = false;
334
+ this._stopIdleTimer();
246
335
  if (this.auth && this.auth.oauth2) {
247
336
  this.auth.oauth2.removeAllListeners();
248
337
  }
@@ -142,7 +142,9 @@ class SMTPTransport extends node_events_1.EventEmitter {
142
142
  * @param callback Callback function
143
143
  */
144
144
  send(mail, callback) {
145
- this.getSocket(this.options, (err, socketOptions) => {
145
+ // the proxy handshake, if any, counts against connectionTimeout as well
146
+ const connectStartedAt = Date.now();
147
+ this.getSocket(Object.assign({}, this.options, { connectStartedAt }), (err, socketOptions) => {
146
148
  if (err) {
147
149
  return callback(err);
148
150
  }
@@ -161,6 +163,7 @@ class SMTPTransport extends node_events_1.EventEmitter {
161
163
  options = Object.assign(shared.assign(false, options), socketOptions);
162
164
  }
163
165
  const connection = new index_js_1.default(options);
166
+ connection._connectStartedAt = connectStartedAt;
164
167
  let perCallAuth;
165
168
  const cleanupPerCallAuth = () => {
166
169
  if (perCallAuth && perCallAuth !== this.auth && perCallAuth.oauth2) {
@@ -266,7 +269,9 @@ class SMTPTransport extends node_events_1.EventEmitter {
266
269
  }
267
270
  // the error paths hand over the error alone
268
271
  const done = callback;
269
- this.getSocket(this.options, (err, socketOptions) => {
272
+ // the proxy handshake, if any, counts against connectionTimeout as well
273
+ const connectStartedAt = Date.now();
274
+ this.getSocket(Object.assign({}, this.options, { connectStartedAt }), (err, socketOptions) => {
270
275
  if (err) {
271
276
  return done(err);
272
277
  }
@@ -283,6 +288,7 @@ class SMTPTransport extends node_events_1.EventEmitter {
283
288
  options = Object.assign(shared.assign(false, options), socketOptions);
284
289
  }
285
290
  const connection = new index_js_1.default(options);
291
+ connection._connectStartedAt = connectStartedAt;
286
292
  let returned = false;
287
293
  let perCallAuth;
288
294
  const cleanupPerCallAuth = () => {
@@ -24,6 +24,9 @@ export interface EncoderOptions {
24
24
  /**
25
25
  * Creates a transform stream for encoding data to base64 encoding
26
26
  *
27
+ * The output is the same as `wrap(encode(input), lineLength)` no matter how the input is split
28
+ * into chunks: every line but the last one ends with a line break, the last one does not
29
+ *
27
30
  * @constructor
28
31
  * @param options Stream options
29
32
  * @param [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
@@ -11,6 +11,47 @@ export function encode(buffer) {
11
11
  }
12
12
  return buffer.toString('base64');
13
13
  }
14
+ /**
15
+ * Turns a line length option into a whole number of characters, the default for anything unusable
16
+ */
17
+ function normalizeLineLength(lineLength) {
18
+ const length = Math.floor(Number(lineLength));
19
+ return Number.isFinite(length) && length >= 1 ? length : 76;
20
+ }
21
+ /**
22
+ * Splits the bytes of `src` into lines of `lineLength` bytes, each followed by a line break. With
23
+ * `final` set the last line, which may be shorter, gets no line break; otherwise only complete
24
+ * lines are taken and the rest is left for the caller
25
+ *
26
+ * @param src Bytes to wrap
27
+ * @param lineLength Line length
28
+ * @param final Whether `src` ends the output
29
+ * @returns The wrapped bytes and the number of trailing bytes not taken
30
+ */
31
+ function wrapBuffer(src, lineLength, final) {
32
+ const lines = Math.ceil(src.length / lineLength);
33
+ // the last line waits for more data unless this is the end: whether it gets a line break
34
+ // depends on whether anything follows it
35
+ const complete = Math.max(lines - 1, 0);
36
+ let rest = src.length - complete * lineLength;
37
+ const output = Buffer.allocUnsafe(complete * (lineLength + 2) + (final ? rest : 0));
38
+ let to = 0;
39
+ for (let from = 0; from < complete * lineLength; from += lineLength) {
40
+ src.copy(output, to, from, from + lineLength);
41
+ to += lineLength;
42
+ output[to++] = 0x0d;
43
+ output[to++] = 0x0a;
44
+ }
45
+ if (final) {
46
+ to += src.copy(output, to, complete * lineLength);
47
+ rest = 0;
48
+ }
49
+ if (to !== output.length) {
50
+ // never hand out bytes of the unfilled allocation
51
+ throw new Error('Unexpected wrapped length');
52
+ }
53
+ return { output, rest };
54
+ }
14
55
  /**
15
56
  * Adds soft line breaks to a base64 string
16
57
  *
@@ -39,6 +80,9 @@ export function wrap(str, lineLength) {
39
80
  /**
40
81
  * Creates a transform stream for encoding data to base64 encoding
41
82
  *
83
+ * The output is the same as `wrap(encode(input), lineLength)` no matter how the input is split
84
+ * into chunks: every line but the last one ends with a line break, the last one does not
85
+ *
42
86
  * @constructor
43
87
  * @param options Stream options
44
88
  * @param [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
@@ -48,66 +92,61 @@ export class Encoder extends Transform {
48
92
  super();
49
93
  this.options = options || {};
50
94
  if (this.options.lineLength !== false) {
51
- this.options.lineLength = this.options.lineLength || 76;
95
+ this.options.lineLength = normalizeLineLength(this.options.lineLength);
52
96
  }
53
97
  this._curLine = '';
54
98
  this._remainingBytes = false;
55
99
  this.inputBytes = 0;
56
100
  this.outputBytes = 0;
57
101
  }
102
+ /**
103
+ * Emits the encoded characters `b64` that follow the current line, keeping what can not be
104
+ * emitted yet as the new current line
105
+ *
106
+ * @internal
107
+ */
108
+ _emit(b64, final) {
109
+ const src = Buffer.from(this._curLine + b64, 'latin1');
110
+ if (!src.length) {
111
+ return;
112
+ }
113
+ let output = src;
114
+ if (this.options.lineLength) {
115
+ const wrapped = wrapBuffer(src, this.options.lineLength, final);
116
+ output = wrapped.output;
117
+ this._curLine = wrapped.rest ? src.toString('latin1', src.length - wrapped.rest) : '';
118
+ }
119
+ else {
120
+ this._curLine = '';
121
+ }
122
+ if (output.length) {
123
+ this.outputBytes += output.length;
124
+ this.push(output);
125
+ }
126
+ }
58
127
  /** @internal */
59
128
  _transform(chunk, encoding, done) {
60
129
  let buf = encoding !== 'buffer' ? Buffer.from(chunk, encoding) : chunk;
61
130
  if (!buf || !buf.length) {
62
- setImmediate(done);
63
- return;
131
+ return done();
64
132
  }
65
133
  this.inputBytes += buf.length;
66
- if (this._remainingBytes && this._remainingBytes.length) {
134
+ if (this._remainingBytes) {
67
135
  buf = Buffer.concat([this._remainingBytes, buf], this._remainingBytes.length + buf.length);
68
136
  this._remainingBytes = false;
69
137
  }
70
- if (buf.length % 3) {
71
- this._remainingBytes = buf.slice(buf.length - (buf.length % 3));
72
- buf = buf.slice(0, buf.length - (buf.length % 3));
73
- }
74
- else {
75
- this._remainingBytes = false;
76
- }
77
- let b64 = this._curLine + encode(buf);
78
- if (this.options.lineLength) {
79
- b64 = wrap(b64, this.options.lineLength);
80
- // remove last line as it is still most probably incomplete
81
- const lastLF = b64.lastIndexOf('\n');
82
- if (lastLF < 0) {
83
- this._curLine = b64;
84
- b64 = '';
85
- }
86
- else if (lastLF === b64.length - 1) {
87
- this._curLine = '';
88
- }
89
- else {
90
- this._curLine = b64.substring(lastLF + 1);
91
- b64 = b64.substring(0, lastLF + 1);
92
- }
93
- }
94
- if (b64) {
95
- this.outputBytes += b64.length;
96
- this.push(Buffer.from(b64, 'ascii'));
138
+ const extra = buf.length % 3;
139
+ if (extra) {
140
+ this._remainingBytes = buf.subarray(buf.length - extra);
141
+ buf = buf.subarray(0, buf.length - extra);
97
142
  }
98
- setImmediate(done);
143
+ this._emit(encode(buf), false);
144
+ done();
99
145
  }
100
146
  /** @internal */
101
147
  _flush(done) {
102
- if (this._remainingBytes && this._remainingBytes.length) {
103
- this._curLine += encode(this._remainingBytes);
104
- }
105
- if (this._curLine) {
106
- this._curLine = wrap(this._curLine, this.options.lineLength);
107
- this.outputBytes += this._curLine.length;
108
- this.push(Buffer.from(this._curLine, 'ascii'));
109
- this._curLine = '';
110
- }
148
+ this._emit(this._remainingBytes ? encode(this._remainingBytes) : '', true);
149
+ this._remainingBytes = false;
111
150
  done();
112
151
  }
113
152
  }
@@ -32,51 +32,67 @@ class DKIMSigner {
32
32
  this.output = output;
33
33
  this.output.usingCache = false;
34
34
  this.hasErrored = false;
35
- this.input.on('error', err => {
35
+ this.input.on('error', err => this.fail(err));
36
+ // A consumer that goes away before the signed message was read in full destroys the
37
+ // output. Stop reading the input and drop the cache file instead of leaving them open
38
+ this.output.once('close', () => {
39
+ if (this.output.writableFinished) {
40
+ return;
41
+ }
36
42
  this.hasErrored = true;
43
+ this.input.unpipe();
44
+ this.input.destroy();
37
45
  this.cleanup();
38
- output.emit('error', err);
39
46
  });
40
47
  }
48
+ /**
49
+ * Ends the output with an error and releases the cache file
50
+ */
51
+ fail(err) {
52
+ this.hasErrored = true;
53
+ this.cleanup();
54
+ this.output.destroy(err);
55
+ }
41
56
  cleanup() {
42
57
  if (!this.cache || !this.cachePath) {
43
58
  return;
44
59
  }
60
+ const cache = this.cache;
61
+ this.cache = false;
62
+ cache.destroy();
45
63
  fs.unlink(this.cachePath, () => false);
46
64
  }
47
65
  createReadCache() {
48
66
  // pipe remainings to cache file
49
67
  this.cache = fs.createReadStream(this.cachePath);
50
- this.cache.once('error', err => {
51
- this.cleanup();
52
- this.output.emit('error', err);
53
- });
68
+ this.cache.once('error', err => this.fail(err));
54
69
  this.cache.once('close', () => {
55
70
  this.cleanup();
56
71
  });
57
72
  this.cache.pipe(this.output);
58
73
  }
59
74
  sendNextChunk() {
60
- if (this.hasErrored) {
61
- return;
62
- }
63
- if (this.readPos >= this.chunks.length) {
64
- if (!this.cache) {
65
- this.output.end();
75
+ while (!this.hasErrored) {
76
+ if (this.readPos >= this.chunks.length) {
77
+ if (!this.cache) {
78
+ this.output.end();
79
+ return;
80
+ }
81
+ return this.createReadCache();
82
+ }
83
+ const chunk = this.chunks[this.readPos++];
84
+ if (this.output.write(chunk) === false) {
85
+ this.output.once('drain', () => {
86
+ this.sendNextChunk();
87
+ });
66
88
  return;
67
89
  }
68
- return this.createReadCache();
69
- }
70
- const chunk = this.chunks[this.readPos++];
71
- if (this.output.write(chunk) === false) {
72
- this.output.once('drain', () => {
73
- this.sendNextChunk();
74
- });
75
- return;
76
90
  }
77
- setImmediate(() => this.sendNextChunk());
78
91
  }
79
92
  sendSignedOutput() {
93
+ if (this.hasErrored) {
94
+ return;
95
+ }
80
96
  let keyPos = 0;
81
97
  const signNextKey = () => {
82
98
  if (keyPos >= this.keys.length) {
@@ -96,9 +112,7 @@ class DKIMSigner {
96
112
  });
97
113
  }
98
114
  catch (err) {
99
- this.hasErrored = true;
100
- this.cleanup();
101
- this.output.emit('error', err);
115
+ this.fail(err);
102
116
  return;
103
117
  }
104
118
  if (dkimField) {
@@ -117,7 +131,6 @@ class DKIMSigner {
117
131
  // pipe remainings to cache file
118
132
  this.cache = fs.createWriteStream(this.cachePath);
119
133
  this.cache.once('error', err => {
120
- this.cleanup();
121
134
  // drain input
122
135
  this.relaxedBody.unpipe(this.cache);
123
136
  this.relaxedBody.on('readable', () => {
@@ -125,11 +138,12 @@ class DKIMSigner {
125
138
  // do nothing
126
139
  }
127
140
  });
128
- this.hasErrored = true;
129
- // emit error
130
- this.output.emit('error', err);
141
+ this.fail(err);
131
142
  });
132
143
  this.cache.once('close', () => {
144
+ if (this.hasErrored) {
145
+ return;
146
+ }
133
147
  this.sendSignedOutput();
134
148
  });
135
149
  this.relaxedBody.removeAllListeners('readable');
@@ -206,7 +220,7 @@ class DKIM {
206
220
  catch (_E) {
207
221
  // the body hash is created here, an unknown hashAlgo throws inside this timer
208
222
  // where nothing else could catch it
209
- output.emit('error', sign.unsupportedHashAlgoError(signer.hashAlgo));
223
+ signer.fail(sign.unsupportedHashAlgoError(signer.hashAlgo));
210
224
  return;
211
225
  }
212
226
  if (writeValue) {
@@ -105,7 +105,7 @@ class MessageParser extends Transform {
105
105
  const chunk = data.slice(headerPos);
106
106
  this.bodySize += chunk.length;
107
107
  // this would be the first chunk of data sent downstream
108
- setImmediate(() => this.push(chunk));
108
+ this.push(chunk);
109
109
  }
110
110
  return false;
111
111
  }
@@ -134,7 +134,7 @@ class MessageParser extends Transform {
134
134
  this.bodySize += chunk.length;
135
135
  this.push(chunk);
136
136
  }
137
- setImmediate(callback);
137
+ callback();
138
138
  }
139
139
  /** @internal */
140
140
  _flush(callback) {
@@ -61,6 +61,19 @@ export declare const EPROXY = "EPROXY";
61
61
  export declare const EFILEACCESS = "EFILEACCESS";
62
62
  export declare const EURLACCESS = "EURLACCESS";
63
63
  export declare const EFETCH = "EFETCH";
64
+ /**
65
+ * Tells whether the error ended the connection rather than rejected what was sent over it
66
+ */
67
+ export declare const isTransientError: (err: {
68
+ code?: string | undefined;
69
+ responseCode?: number | undefined;
70
+ }) => boolean;
71
+ /**
72
+ * Code Node.js sets on an error when its permission model (`--permission`) denies an
73
+ * operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
74
+ * `EDNS`, `EFETCH`, ...), so a missing grant such as `--allow-net` stays recognizable.
75
+ */
76
+ export declare const ERR_ACCESS_DENIED = "ERR_ACCESS_DENIED";
64
77
  /**
65
78
  * An Error together with the properties Nodemailer attaches to the errors it
66
79
  * hands to callers. Every property is optional, the set that is present
@@ -84,6 +97,10 @@ export interface NodemailerError extends NodeJS.ErrnoException {
84
97
  rejected?: string[] | undefined;
85
98
  /** Per-recipient errors for the rejected addresses */
86
99
  rejectedErrors?: NodemailerError[] | undefined;
100
+ /** The code the error had before Nodemailer set its own, such as ECONNREFUSED for an ESOCKET error */
101
+ originalCode?: string | undefined;
102
+ /** Which wait an ETIMEDOUT error ended: connecting, the greeting, a TLS upgrade or an idle socket */
103
+ timeoutType?: 'CONNECT_TIMEOUT' | 'GREETING_TIMEOUT' | 'UPGRADE_TIMEOUT' | 'SOCKET_TIMEOUT' | undefined;
87
104
  }
88
105
  /**
89
106
  * Node style callback: called with an error, or with null and the result
@@ -65,3 +65,13 @@ export const EPROXY = 'EPROXY';
65
65
  export const EFILEACCESS = 'EFILEACCESS';
66
66
  export const EURLACCESS = 'EURLACCESS';
67
67
  export const EFETCH = 'EFETCH';
68
+ /**
69
+ * Tells whether the error ended the connection rather than rejected what was sent over it
70
+ */
71
+ export const isTransientError = (err) => err.responseCode === 421 || err.code === ECONNECTION || err.code === ESOCKET || err.code === ETIMEDOUT;
72
+ /**
73
+ * Code Node.js sets on an error when its permission model (`--permission`) denies an
74
+ * operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
75
+ * `EDNS`, `EFETCH`, ...), so a missing grant such as `--allow-net` stays recognizable.
76
+ */
77
+ export const ERR_ACCESS_DENIED = 'ERR_ACCESS_DENIED';
@@ -43,6 +43,17 @@ const TLS_OPTION_KEYS = [
43
43
  'sessionIdContext',
44
44
  'sigalgs'
45
45
  ];
46
+ /**
47
+ * Marks an error of the request as a fetch error, unless it is a permission model
48
+ * denial, which keeps its own code (see ERR_ACCESS_DENIED)
49
+ *
50
+ * @param err Error to mark
51
+ */
52
+ function setFetchCode(err) {
53
+ if (err.code !== errors.ERR_ACCESS_DENIED) {
54
+ err.code = errors.EFETCH;
55
+ }
56
+ }
46
57
  /**
47
58
  * Resolves a URL only if it is one this module is willing to request.
48
59
  *
@@ -139,7 +150,7 @@ function nmfetch(url, options) {
139
150
  return;
140
151
  }
141
152
  finished = true;
142
- err.code = errors.EFETCH;
153
+ setFetchCode(err);
143
154
  err.sourceUrl = url;
144
155
  fetchRes.emit('error', err);
145
156
  });
@@ -212,7 +223,7 @@ function nmfetch(url, options) {
212
223
  catch (E) {
213
224
  finished = true;
214
225
  setImmediate(() => {
215
- E.code = errors.EFETCH;
226
+ setFetchCode(E);
216
227
  E.sourceUrl = url;
217
228
  fetchRes.emit('error', E);
218
229
  });
@@ -224,7 +235,7 @@ function nmfetch(url, options) {
224
235
  return;
225
236
  }
226
237
  finished = true;
227
- err.code = errors.EFETCH;
238
+ setFetchCode(err);
228
239
  err.sourceUrl = sourceUrl;
229
240
  fetchRes.emit('error', err);
230
241
  req.abort();
@@ -234,6 +245,15 @@ function nmfetch(url, options) {
234
245
  req.setTimeout(timeout, () => fail(new Error('Request Timeout')));
235
246
  }
236
247
  req.on('error', (err) => fail(err));
248
+ // a consumer that destroys the response stream before it ended does not want the rest of
249
+ // the body, release the request now instead of when the timeout fires
250
+ fetchRes.once('close', () => {
251
+ if (finished || fetchRes.readableEnded) {
252
+ return;
253
+ }
254
+ finished = true;
255
+ req.destroy();
256
+ });
237
257
  req.on('response', res => {
238
258
  let inflate;
239
259
  if (finished) {