@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.
- package/.jsii +10 -10
- package/lib/rootmail.js +1 -1
- package/lib/ses-receive.js +1 -1
- package/node_modules/@aws-sdk/checksums/package.json +3 -3
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-cjs/index.js +23 -1
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-es/models/CloudWatchLogsServiceException.js +1 -0
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-es/models/errors.js +21 -0
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/models/CloudWatchLogsServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/models/errors.d.ts +21 -0
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/ts3.4/models/CloudWatchLogsServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/ts3.4/models/errors.d.ts +21 -0
- package/node_modules/@aws-sdk/client-cloudwatch-logs/package.json +4 -4
- package/node_modules/@aws-sdk/client-route-53/dist-cjs/index.js +71 -1
- package/node_modules/@aws-sdk/client-route-53/dist-es/models/Route53ServiceException.js +1 -0
- package/node_modules/@aws-sdk/client-route-53/dist-es/models/errors.js +69 -0
- package/node_modules/@aws-sdk/client-route-53/dist-types/models/Route53ServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/client-route-53/dist-types/models/errors.d.ts +69 -0
- package/node_modules/@aws-sdk/client-route-53/dist-types/ts3.4/models/Route53ServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/client-route-53/dist-types/ts3.4/models/errors.d.ts +69 -0
- package/node_modules/@aws-sdk/client-route-53/package.json +4 -4
- package/node_modules/@aws-sdk/client-s3/dist-cjs/index.js +23 -1
- package/node_modules/@aws-sdk/client-s3/dist-es/models/S3ServiceException.js +1 -0
- package/node_modules/@aws-sdk/client-s3/dist-es/models/errors.js +21 -0
- package/node_modules/@aws-sdk/client-s3/dist-types/models/S3ServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/client-s3/dist-types/models/errors.d.ts +21 -0
- package/node_modules/@aws-sdk/client-s3/dist-types/ts3.4/models/S3ServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/client-s3/dist-types/ts3.4/models/errors.d.ts +21 -0
- package/node_modules/@aws-sdk/client-s3/package.json +8 -8
- package/node_modules/@aws-sdk/client-ses/dist-cjs/index.js +36 -1
- package/node_modules/@aws-sdk/client-ses/dist-es/models/SESServiceException.js +1 -0
- package/node_modules/@aws-sdk/client-ses/dist-es/models/errors.js +34 -0
- package/node_modules/@aws-sdk/client-ses/dist-types/models/SESServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/client-ses/dist-types/models/errors.d.ts +34 -0
- package/node_modules/@aws-sdk/client-ses/dist-types/ts3.4/models/SESServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/client-ses/dist-types/ts3.4/models/errors.d.ts +34 -0
- package/node_modules/@aws-sdk/client-ses/package.json +4 -4
- package/node_modules/@aws-sdk/client-ssm/dist-cjs/index.js +142 -1
- package/node_modules/@aws-sdk/client-ssm/dist-es/models/SSMServiceException.js +1 -0
- package/node_modules/@aws-sdk/client-ssm/dist-es/models/errors.js +140 -0
- package/node_modules/@aws-sdk/client-ssm/dist-types/models/SSMServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/client-ssm/dist-types/models/errors.d.ts +140 -0
- package/node_modules/@aws-sdk/client-ssm/dist-types/ts3.4/models/SSMServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/client-ssm/dist-types/ts3.4/models/errors.d.ts +140 -0
- package/node_modules/@aws-sdk/client-ssm/package.json +4 -4
- package/node_modules/@aws-sdk/core/package.json +3 -3
- package/node_modules/@aws-sdk/credential-provider-env/package.json +3 -3
- package/node_modules/@aws-sdk/credential-provider-http/package.json +3 -3
- package/node_modules/@aws-sdk/credential-provider-ini/package.json +10 -10
- package/node_modules/@aws-sdk/credential-provider-login/package.json +4 -4
- package/node_modules/@aws-sdk/credential-provider-node/package.json +8 -8
- package/node_modules/@aws-sdk/credential-provider-process/package.json +3 -3
- package/node_modules/@aws-sdk/credential-provider-sso/package.json +5 -5
- package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +4 -4
- package/node_modules/@aws-sdk/middleware-sdk-s3/package.json +4 -4
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/cognito-identity/index.js +11 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/signin/index.js +6 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso/index.js +6 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso-oidc/index.js +13 -1
- package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sts/index.js +9 -1
- package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/cognito-identity/models/CognitoIdentityServiceException.js +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/cognito-identity/models/errors.js +9 -0
- package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/signin/models/SigninServiceException.js +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/signin/models/errors.js +4 -0
- package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/sso/models/SSOServiceException.js +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/sso/models/errors.js +4 -0
- package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/sso-oidc/models/SSOOIDCServiceException.js +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/sso-oidc/models/errors.js +11 -0
- package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/sts/models/STSServiceException.js +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-es/submodules/sts/models/errors.js +7 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/cognito-identity/models/CognitoIdentityServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/cognito-identity/models/errors.d.ts +9 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/signin/models/SigninServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/signin/models/errors.d.ts +4 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/sso/models/SSOServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/sso/models/errors.d.ts +4 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/sso-oidc/models/SSOOIDCServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/sso-oidc/models/errors.d.ts +11 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/sts/models/STSServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/submodules/sts/models/errors.d.ts +7 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/cognito-identity/models/CognitoIdentityServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/cognito-identity/models/errors.d.ts +9 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/signin/models/SigninServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/signin/models/errors.d.ts +4 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/sso/models/SSOServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/sso/models/errors.d.ts +4 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/sso-oidc/models/SSOOIDCServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/sso-oidc/models/errors.d.ts +11 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/sts/models/STSServiceException.d.ts +1 -0
- package/node_modules/@aws-sdk/nested-clients/dist-types/ts3.4/submodules/sts/models/errors.d.ts +7 -0
- package/node_modules/@aws-sdk/nested-clients/package.json +4 -4
- package/node_modules/@aws-sdk/signature-v4-multi-region/package.json +2 -2
- package/node_modules/@aws-sdk/token-providers/package.json +4 -4
- package/node_modules/@smithy/core/dist-cjs/submodules/client/index.js +33 -11
- package/node_modules/@smithy/core/dist-cjs/submodules/endpoints/index.browser.js +2 -1
- package/node_modules/@smithy/core/dist-cjs/submodules/endpoints/index.js +2 -1
- package/node_modules/@smithy/core/dist-cjs/submodules/schema/index.js +3 -0
- package/node_modules/@smithy/core/dist-es/submodules/client/smithy-client/exceptions.js +33 -11
- package/node_modules/@smithy/core/dist-es/submodules/endpoints/util-endpoints/cache/EndpointCache.js +2 -1
- package/node_modules/@smithy/core/dist-es/submodules/schema/TypeRegistry.js +3 -0
- package/node_modules/@smithy/core/dist-types/submodules/client/smithy-client/exceptions.d.ts +1 -0
- package/node_modules/@smithy/core/dist-types/ts3.4/submodules/client/smithy-client/exceptions.d.ts +1 -0
- package/node_modules/@smithy/core/package.json +1 -1
- package/node_modules/@smithy/signature-v4/dist-cjs/index.js +4 -1
- package/node_modules/@smithy/signature-v4/dist-es/getCanonicalHeaders.js +4 -1
- package/node_modules/@smithy/signature-v4/package.json +3 -3
- package/node_modules/mailparser/.release-please-manifest.json +1 -1
- package/node_modules/mailparser/CHANGELOG.md +7 -0
- package/node_modules/mailparser/package.json +2 -2
- package/node_modules/nodemailer/CHANGELOG.md +27 -0
- package/node_modules/nodemailer/dist/cjs/base64/index.d.ts +3 -0
- package/node_modules/nodemailer/dist/cjs/base64/index.js +80 -41
- package/node_modules/nodemailer/dist/cjs/dkim/index.js +43 -29
- package/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +2 -2
- package/node_modules/nodemailer/dist/cjs/errors.d.ts +17 -0
- package/node_modules/nodemailer/dist/cjs/errors.js +12 -1
- package/node_modules/nodemailer/dist/cjs/fetch/index.js +23 -3
- package/node_modules/nodemailer/dist/cjs/mailer/index.js +23 -7
- package/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +5 -0
- package/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +20 -0
- package/node_modules/nodemailer/dist/cjs/mime-node/index.d.ts +1 -1
- package/node_modules/nodemailer/dist/cjs/mime-node/index.js +165 -105
- package/node_modules/nodemailer/dist/cjs/nodemailer.js +12 -9
- package/node_modules/nodemailer/dist/cjs/package-info.d.ts +1 -1
- package/node_modules/nodemailer/dist/cjs/package-info.js +1 -1
- package/node_modules/nodemailer/dist/cjs/qp/index.d.ts +2 -0
- package/node_modules/nodemailer/dist/cjs/qp/index.js +103 -33
- package/node_modules/nodemailer/dist/cjs/sendmail-transport/index.js +54 -14
- package/node_modules/nodemailer/dist/cjs/shared/index.d.ts +32 -1
- package/node_modules/nodemailer/dist/cjs/shared/index.js +212 -115
- package/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +3 -1
- package/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +88 -68
- package/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +7 -1
- package/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +348 -136
- package/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +3 -0
- package/node_modules/nodemailer/dist/cjs/smtp-pool/index.js +7 -1
- package/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.js +106 -17
- package/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +8 -2
- package/node_modules/nodemailer/dist/esm/base64/index.d.ts +3 -0
- package/node_modules/nodemailer/dist/esm/base64/index.js +80 -41
- package/node_modules/nodemailer/dist/esm/dkim/index.js +43 -29
- package/node_modules/nodemailer/dist/esm/dkim/message-parser.js +2 -2
- package/node_modules/nodemailer/dist/esm/errors.d.ts +17 -0
- package/node_modules/nodemailer/dist/esm/errors.js +10 -0
- package/node_modules/nodemailer/dist/esm/fetch/index.js +23 -3
- package/node_modules/nodemailer/dist/esm/mailer/index.js +23 -7
- package/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +5 -0
- package/node_modules/nodemailer/dist/esm/mailer/mail-message.js +20 -0
- package/node_modules/nodemailer/dist/esm/mime-node/index.d.ts +1 -1
- package/node_modules/nodemailer/dist/esm/mime-node/index.js +166 -106
- package/node_modules/nodemailer/dist/esm/nodemailer.js +12 -9
- package/node_modules/nodemailer/dist/esm/package-info.d.ts +1 -1
- package/node_modules/nodemailer/dist/esm/package-info.js +1 -1
- package/node_modules/nodemailer/dist/esm/qp/index.d.ts +2 -0
- package/node_modules/nodemailer/dist/esm/qp/index.js +103 -33
- package/node_modules/nodemailer/dist/esm/sendmail-transport/index.js +54 -14
- package/node_modules/nodemailer/dist/esm/shared/index.d.ts +32 -1
- package/node_modules/nodemailer/dist/esm/shared/index.js +207 -115
- package/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +3 -1
- package/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.js +88 -68
- package/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +7 -1
- package/node_modules/nodemailer/dist/esm/smtp-connection/index.js +348 -136
- package/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +3 -0
- package/node_modules/nodemailer/dist/esm/smtp-pool/index.js +7 -1
- package/node_modules/nodemailer/dist/esm/smtp-pool/pool-resource.js +106 -17
- package/node_modules/nodemailer/dist/esm/smtp-transport/index.js +8 -2
- package/node_modules/nodemailer/package.json +1 -1
- package/node_modules/uuid/package.json +17 -17
- 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.
|
|
139
|
-
this.
|
|
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.
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
321
|
-
* @param
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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
|
|
394
|
-
//
|
|
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
|
-
//
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
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
|
-
|
|
807
|
-
if (
|
|
808
|
-
this.logger.warn(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
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)) {
|