@nocobase/plugin-workflow-mailer 2.2.17 → 2.2.18
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/dist/externalVersion.js +6 -6
- package/dist/node_modules/joi/dist/joi-browser.min.js +1 -1
- package/dist/node_modules/joi/lib/index.js +1 -1
- package/dist/node_modules/joi/lib/messages.js +35 -6
- package/dist/node_modules/joi/lib/types/keys.js +18 -1
- package/dist/node_modules/joi/lib/types/link.js +18 -2
- package/dist/node_modules/joi/lib/types/string.js +2 -2
- package/dist/node_modules/joi/package.json +1 -1
- package/dist/node_modules/nodemailer/.ncurc.js +2 -0
- package/dist/node_modules/nodemailer/.prettierignore +8 -0
- package/dist/node_modules/nodemailer/.prettierrc +12 -0
- package/dist/node_modules/nodemailer/.prettierrc.js +2 -0
- package/dist/node_modules/nodemailer/.release-please-config.json +9 -0
- package/dist/node_modules/nodemailer/eslint.config.js +88 -0
- package/dist/node_modules/nodemailer/lib/addressparser/index.js +314 -71
- package/dist/node_modules/nodemailer/lib/base64/index.js +9 -13
- package/dist/node_modules/nodemailer/lib/dkim/index.js +17 -18
- package/dist/node_modules/nodemailer/lib/dkim/message-parser.js +12 -13
- package/dist/node_modules/nodemailer/lib/dkim/relaxed-body.js +3 -3
- package/dist/node_modules/nodemailer/lib/dkim/sign.js +22 -18
- package/dist/node_modules/nodemailer/lib/errors.js +59 -0
- package/dist/node_modules/nodemailer/lib/fetch/cookies.js +14 -19
- package/dist/node_modules/nodemailer/lib/fetch/index.js +164 -27
- package/dist/node_modules/nodemailer/lib/json-transport/index.js +4 -4
- package/dist/node_modules/nodemailer/lib/mail-composer/index.js +229 -155
- package/dist/node_modules/nodemailer/lib/mailer/index.js +106 -54
- package/dist/node_modules/nodemailer/lib/mailer/mail-message.js +106 -68
- package/dist/node_modules/nodemailer/lib/mime-funcs/index.js +105 -59
- package/dist/node_modules/nodemailer/lib/mime-funcs/mime-types.js +21 -14
- package/dist/node_modules/nodemailer/lib/mime-node/index.js +389 -137
- package/dist/node_modules/nodemailer/lib/mime-node/last-newline.js +1 -1
- package/dist/node_modules/nodemailer/lib/mime-node/le-unix.js +4 -7
- package/dist/node_modules/nodemailer/lib/mime-node/le-windows.js +1 -4
- package/dist/node_modules/nodemailer/lib/nodemailer.js +1 -1
- package/dist/node_modules/nodemailer/lib/punycode/index.js +1 -1
- package/dist/node_modules/nodemailer/lib/qp/index.js +33 -22
- package/dist/node_modules/nodemailer/lib/sendmail-transport/index.js +54 -44
- package/dist/node_modules/nodemailer/lib/ses-transport/index.js +110 -193
- package/dist/node_modules/nodemailer/lib/shared/index.js +299 -240
- package/dist/node_modules/nodemailer/lib/shared/objects.js +42 -0
- package/dist/node_modules/nodemailer/lib/shared/url.js +151 -0
- package/dist/node_modules/nodemailer/lib/smtp-connection/data-stream.js +3 -6
- package/dist/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +50 -19
- package/dist/node_modules/nodemailer/lib/smtp-connection/index.js +415 -223
- package/dist/node_modules/nodemailer/lib/smtp-pool/index.js +27 -34
- package/dist/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +19 -16
- package/dist/node_modules/nodemailer/lib/smtp-transport/index.js +66 -53
- package/dist/node_modules/nodemailer/lib/stream-transport/index.js +16 -7
- package/dist/node_modules/nodemailer/lib/well-known/index.js +7 -7
- package/dist/node_modules/nodemailer/lib/well-known/services.json +328 -38
- package/dist/node_modules/nodemailer/lib/xoauth2/index.js +105 -33
- package/dist/node_modules/nodemailer/package.json +1 -1
- package/package.json +4 -4
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const packageInfo = require('../../package.json');
|
|
4
|
-
const EventEmitter = require('events')
|
|
4
|
+
const { EventEmitter } = require('events');
|
|
5
5
|
const net = require('net');
|
|
6
6
|
const tls = require('tls');
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const crypto = require('crypto');
|
|
9
9
|
const DataStream = require('./data-stream');
|
|
10
|
-
const PassThrough = require('stream')
|
|
10
|
+
const { PassThrough } = require('stream');
|
|
11
11
|
const shared = require('../shared');
|
|
12
12
|
|
|
13
13
|
// default timeout values in ms
|
|
@@ -15,6 +15,29 @@ const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection
|
|
|
15
15
|
const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
|
|
16
16
|
const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
|
|
17
17
|
const DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
|
|
18
|
+
const TEARDOWN_NOOP = () => {}; // reusable no-op handler for absorbing errors during socket teardown
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Re-interpret a server response stored in fake 8-bit byte-container form
|
|
22
|
+
* (the result of chunk.toString('binary') in _onData) as UTF-8.
|
|
23
|
+
*
|
|
24
|
+
* Server reply text has no formally defined charset (RFC 5321 §4.2.1), but
|
|
25
|
+
* modern MTAs commonly use UTF-8. The byte-container plumbing in _onData is
|
|
26
|
+
* required to reassemble multi-byte sequences split across socket chunks;
|
|
27
|
+
* this helper performs the actual decode at the line boundary, falling back
|
|
28
|
+
* to the byte-container form when the bytes are not valid UTF-8 so that
|
|
29
|
+
* legacy 8-bit replies are still recoverable byte-for-byte.
|
|
30
|
+
*/
|
|
31
|
+
function decodeServerResponse(str) {
|
|
32
|
+
if (!str) {
|
|
33
|
+
return str;
|
|
34
|
+
}
|
|
35
|
+
const utf8 = Buffer.from(str, 'binary').toString('utf8');
|
|
36
|
+
// The input is a byte container (each char is in U+0000..U+00FF) so it can never
|
|
37
|
+
// already contain U+FFFD; any \uFFFD in the result was inserted by Node's UTF-8
|
|
38
|
+
// decoder for invalid bytes, which means we should return the original bytes intact.
|
|
39
|
+
return utf8.includes('\uFFFD') ? str : utf8;
|
|
40
|
+
}
|
|
18
41
|
|
|
19
42
|
/**
|
|
20
43
|
* Generates a SMTP connection object
|
|
@@ -28,9 +51,9 @@ const DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
|
|
|
28
51
|
* * **requireTLS** - forces the client to use STARTTLS
|
|
29
52
|
* * **name** - the name of the client server
|
|
30
53
|
* * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
|
|
31
|
-
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to
|
|
32
|
-
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish
|
|
33
|
-
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to
|
|
54
|
+
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
|
55
|
+
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish (defaults to 2 minutes)
|
|
56
|
+
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 10 minutes)
|
|
34
57
|
* * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
|
|
35
58
|
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
|
36
59
|
* * **logger** - bunyan compatible logger interface
|
|
@@ -67,7 +90,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
67
90
|
this.secureConnection = true;
|
|
68
91
|
}
|
|
69
92
|
|
|
70
|
-
this.name = this.options.name || this._getHostname();
|
|
93
|
+
this.name = (this.options.name || this._getHostname()).toString().replace(/[\r\n]+/g, '');
|
|
71
94
|
|
|
72
95
|
this.logger = shared.getLogger(this.options, {
|
|
73
96
|
component: this.options.component || 'smtp-connection',
|
|
@@ -75,13 +98,12 @@ class SMTPConnection extends EventEmitter {
|
|
|
75
98
|
});
|
|
76
99
|
|
|
77
100
|
this.customAuth = new Map();
|
|
78
|
-
Object.keys(this.options.customAuth || {})
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
101
|
+
for (const key of Object.keys(this.options.customAuth || {})) {
|
|
102
|
+
const mapKey = (key || '').toString().trim().toUpperCase();
|
|
103
|
+
if (mapKey) {
|
|
104
|
+
this.customAuth.set(mapKey, this.options.customAuth[key]);
|
|
82
105
|
}
|
|
83
|
-
|
|
84
|
-
});
|
|
106
|
+
}
|
|
85
107
|
|
|
86
108
|
/**
|
|
87
109
|
* Expose version nr, just for the reference
|
|
@@ -124,7 +146,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
124
146
|
|
|
125
147
|
/**
|
|
126
148
|
* The socket connecting to the server
|
|
127
|
-
* @
|
|
149
|
+
* @public
|
|
128
150
|
*/
|
|
129
151
|
this._socket = false;
|
|
130
152
|
|
|
@@ -189,6 +211,13 @@ class SMTPConnection extends EventEmitter {
|
|
|
189
211
|
*/
|
|
190
212
|
this._closing = false;
|
|
191
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Message DATA stream currently piped to the socket, if any. Tracked so
|
|
216
|
+
* close() can unpipe it before tearing the socket down.
|
|
217
|
+
* @private
|
|
218
|
+
*/
|
|
219
|
+
this._currentDataStream = false;
|
|
220
|
+
|
|
192
221
|
/**
|
|
193
222
|
* Callbacks for socket's listeners
|
|
194
223
|
*/
|
|
@@ -197,6 +226,17 @@ class SMTPConnection extends EventEmitter {
|
|
|
197
226
|
this._onSocketClose = () => this._onClose();
|
|
198
227
|
this._onSocketEnd = () => this._onEnd();
|
|
199
228
|
this._onSocketTimeout = () => this._onTimeout();
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Connection-phase error handler (supports fallback to alternative addresses)
|
|
232
|
+
*/
|
|
233
|
+
this._onConnectionSocketError = err => this._onConnectionError(err, 'ESOCKET');
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Connection attempt counter for fallback race condition protection
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
this._connectionAttemptId = 0;
|
|
200
240
|
}
|
|
201
241
|
|
|
202
242
|
/**
|
|
@@ -232,17 +272,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
232
272
|
opts.localAddress = this.options.localAddress;
|
|
233
273
|
}
|
|
234
274
|
|
|
235
|
-
let setupConnectionHandlers = () => {
|
|
236
|
-
this._connectionTimeout = setTimeout(() => {
|
|
237
|
-
this._onError('Connection timeout', 'ETIMEDOUT', false, 'CONN');
|
|
238
|
-
}, this.options.connectionTimeout || CONNECTION_TIMEOUT);
|
|
239
|
-
|
|
240
|
-
this._socket.on('error', this._onSocketError);
|
|
241
|
-
};
|
|
242
|
-
|
|
243
275
|
if (this.options.connection) {
|
|
244
276
|
// connection is already opened
|
|
245
277
|
this._socket = this.options.connection;
|
|
278
|
+
this._setupConnectionHandlers();
|
|
279
|
+
|
|
246
280
|
if (this.secureConnection && !this.alreadySecured) {
|
|
247
281
|
setImmediate(() =>
|
|
248
282
|
this._upgradeConnection(err => {
|
|
@@ -260,115 +294,178 @@ class SMTPConnection extends EventEmitter {
|
|
|
260
294
|
} else if (this.options.socket) {
|
|
261
295
|
// socket object is set up but not yet connected
|
|
262
296
|
this._socket = this.options.socket;
|
|
263
|
-
return
|
|
264
|
-
if (err) {
|
|
265
|
-
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
|
|
266
|
-
}
|
|
267
|
-
this.logger.debug(
|
|
268
|
-
{
|
|
269
|
-
tnx: 'dns',
|
|
270
|
-
source: opts.host,
|
|
271
|
-
resolved: resolved.host,
|
|
272
|
-
cached: !!resolved.cached
|
|
273
|
-
},
|
|
274
|
-
'Resolved %s as %s [cache %s]',
|
|
275
|
-
opts.host,
|
|
276
|
-
resolved.host,
|
|
277
|
-
resolved.cached ? 'hit' : 'miss'
|
|
278
|
-
);
|
|
279
|
-
Object.keys(resolved).forEach(key => {
|
|
280
|
-
if (key.charAt(0) !== '_' && resolved[key]) {
|
|
281
|
-
opts[key] = resolved[key];
|
|
282
|
-
}
|
|
283
|
-
});
|
|
297
|
+
return this._resolveAndConnect(opts, _resolved => {
|
|
284
298
|
try {
|
|
285
299
|
this._socket.connect(this.port, this.host, () => {
|
|
286
300
|
this._socket.setKeepAlive(true);
|
|
301
|
+
|
|
302
|
+
// a `secure` connection over a caller-provided socket must still
|
|
303
|
+
// perform the TLS handshake, otherwise AUTH and the message body
|
|
304
|
+
// would be sent in cleartext despite the caller requesting TLS
|
|
305
|
+
if (this.secureConnection && !this.alreadySecured) {
|
|
306
|
+
return this._upgradeConnection(err => {
|
|
307
|
+
if (err) {
|
|
308
|
+
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
this._onConnect();
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
287
315
|
this._onConnect();
|
|
288
316
|
});
|
|
289
|
-
|
|
317
|
+
this._setupConnectionHandlers();
|
|
290
318
|
} catch (E) {
|
|
291
319
|
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
|
292
320
|
}
|
|
293
321
|
});
|
|
294
|
-
} else
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
Object.keys(this.options.tls).forEach(key => {
|
|
298
|
-
opts[key] = this.options.tls[key];
|
|
299
|
-
});
|
|
300
|
-
}
|
|
322
|
+
} else {
|
|
323
|
+
if (this.secureConnection) {
|
|
324
|
+
Object.assign(opts, this.options.tls || {});
|
|
301
325
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
326
|
+
// ensure servername for SNI
|
|
327
|
+
if (this.servername && !opts.servername) {
|
|
328
|
+
opts.servername = this.servername;
|
|
329
|
+
}
|
|
305
330
|
}
|
|
306
331
|
|
|
307
|
-
return
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
tnx: 'dns',
|
|
314
|
-
source: opts.host,
|
|
315
|
-
resolved: resolved.host,
|
|
316
|
-
cached: !!resolved.cached
|
|
317
|
-
},
|
|
318
|
-
'Resolved %s as %s [cache %s]',
|
|
319
|
-
opts.host,
|
|
320
|
-
resolved.host,
|
|
321
|
-
resolved.cached ? 'hit' : 'miss'
|
|
322
|
-
);
|
|
323
|
-
Object.keys(resolved).forEach(key => {
|
|
324
|
-
if (key.charAt(0) !== '_' && resolved[key]) {
|
|
325
|
-
opts[key] = resolved[key];
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
try {
|
|
329
|
-
this._socket = tls.connect(opts, () => {
|
|
330
|
-
this._socket.setKeepAlive(true);
|
|
331
|
-
this._onConnect();
|
|
332
|
-
});
|
|
333
|
-
setupConnectionHandlers();
|
|
334
|
-
} catch (E) {
|
|
335
|
-
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
|
336
|
-
}
|
|
332
|
+
return this._resolveAndConnect(opts, resolved => {
|
|
333
|
+
// Store fallback addresses for retry on connection failure
|
|
334
|
+
this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
|
|
335
|
+
this._connectOpts = Object.assign({}, opts);
|
|
336
|
+
|
|
337
|
+
this._connectToHost(opts, this.secureConnection);
|
|
337
338
|
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Resolves the hostname and applies resolved values to opts,
|
|
344
|
+
* then calls the provided callback with the resolved data
|
|
345
|
+
*
|
|
346
|
+
* @param {Object} opts Connection options (modified in place)
|
|
347
|
+
* @param {Function} callback Called with resolved data on success
|
|
348
|
+
*/
|
|
349
|
+
_resolveAndConnect(opts, callback) {
|
|
350
|
+
return shared.resolveHostname(opts, (err, resolved) => {
|
|
351
|
+
if (err) {
|
|
352
|
+
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
|
|
353
|
+
}
|
|
354
|
+
this.logger.debug(
|
|
355
|
+
{
|
|
356
|
+
tnx: 'dns',
|
|
357
|
+
source: opts.host,
|
|
358
|
+
resolved: resolved.host,
|
|
359
|
+
cached: !!resolved.cached
|
|
360
|
+
},
|
|
361
|
+
'Resolved %s as %s [cache %s]',
|
|
362
|
+
opts.host,
|
|
363
|
+
resolved.host,
|
|
364
|
+
resolved.cached ? 'hit' : 'miss'
|
|
365
|
+
);
|
|
366
|
+
for (const key of Object.keys(resolved)) {
|
|
367
|
+
if (key.charAt(0) !== '_' && resolved[key]) {
|
|
368
|
+
opts[key] = resolved[key];
|
|
343
369
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
370
|
+
}
|
|
371
|
+
callback(resolved);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Attempts to connect to the specified host address
|
|
377
|
+
*
|
|
378
|
+
* @param {Object} opts Connection options
|
|
379
|
+
* @param {Boolean} secure Whether to use TLS
|
|
380
|
+
*/
|
|
381
|
+
_connectToHost(opts, secure) {
|
|
382
|
+
// If the client was closed while DNS resolution was in flight, do not open
|
|
383
|
+
// a socket here: close() ran with this._socket still unset and so had
|
|
384
|
+
// nothing to tear down, and _onConnect's remedial close() is a no-op once
|
|
385
|
+
// _closing is set — the freshly connected socket would leak.
|
|
386
|
+
if (this._destroyed || this._closing) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this._connectionAttemptId++;
|
|
391
|
+
const currentAttemptId = this._connectionAttemptId;
|
|
392
|
+
|
|
393
|
+
const connectFn = secure ? tls.connect : net.connect;
|
|
394
|
+
try {
|
|
395
|
+
this._socket = connectFn(opts, () => {
|
|
396
|
+
// Ignore callback if this is a stale connection attempt
|
|
397
|
+
if (this._connectionAttemptId !== currentAttemptId) {
|
|
398
|
+
return;
|
|
369
399
|
}
|
|
400
|
+
this._socket.setKeepAlive(true);
|
|
401
|
+
this._onConnect();
|
|
370
402
|
});
|
|
403
|
+
this._setupConnectionHandlers();
|
|
404
|
+
} catch (E) {
|
|
405
|
+
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Sets up connection timeout and error handlers
|
|
411
|
+
*/
|
|
412
|
+
_setupConnectionHandlers() {
|
|
413
|
+
this._connectionTimeout = setTimeout(() => {
|
|
414
|
+
this._onConnectionError('Connection timeout', 'ETIMEDOUT');
|
|
415
|
+
}, this.options.connectionTimeout || CONNECTION_TIMEOUT);
|
|
416
|
+
|
|
417
|
+
this._socket.on('error', this._onConnectionSocketError);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Handles connection errors with fallback to alternative addresses
|
|
422
|
+
*
|
|
423
|
+
* @param {Error|String} err Error object or message
|
|
424
|
+
* @param {String} code Error code
|
|
425
|
+
*/
|
|
426
|
+
_onConnectionError(err, code) {
|
|
427
|
+
clearTimeout(this._connectionTimeout);
|
|
428
|
+
|
|
429
|
+
// Check if we have fallback addresses to try
|
|
430
|
+
const canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
|
|
431
|
+
|
|
432
|
+
if (!canFallback) {
|
|
433
|
+
// No more fallback addresses, report the error
|
|
434
|
+
this._onError(err, code, false, 'CONN');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const nextHost = this._fallbackAddresses.shift();
|
|
439
|
+
|
|
440
|
+
this.logger.info(
|
|
441
|
+
{
|
|
442
|
+
tnx: 'network',
|
|
443
|
+
failedHost: this._connectOpts.host,
|
|
444
|
+
nextHost,
|
|
445
|
+
error: err.message || err
|
|
446
|
+
},
|
|
447
|
+
'Connection to %s failed, trying %s',
|
|
448
|
+
this._connectOpts.host,
|
|
449
|
+
nextHost
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
// Clean up current socket
|
|
453
|
+
if (this._socket) {
|
|
454
|
+
try {
|
|
455
|
+
this._socket.removeListener('error', this._onConnectionSocketError);
|
|
456
|
+
// Absorb any late teardown error (e.g. a TLS fallback socket emitting
|
|
457
|
+
// after destroy), mirroring the guard used in close()
|
|
458
|
+
this._socket.on('error', TEARDOWN_NOOP);
|
|
459
|
+
this._socket.destroy();
|
|
460
|
+
} catch (_E) {
|
|
461
|
+
// ignore
|
|
462
|
+
}
|
|
463
|
+
this._socket = null;
|
|
371
464
|
}
|
|
465
|
+
|
|
466
|
+
// Update host and retry
|
|
467
|
+
this._connectOpts.host = nextHost;
|
|
468
|
+
this._connectToHost(this._connectOpts, this.secureConnection);
|
|
372
469
|
}
|
|
373
470
|
|
|
374
471
|
/**
|
|
@@ -393,12 +490,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
393
490
|
}
|
|
394
491
|
this._closing = true;
|
|
395
492
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (this.stage === 'init') {
|
|
399
|
-
// Close the socket immediately when connection timed out
|
|
400
|
-
closeMethod = 'destroy';
|
|
401
|
-
}
|
|
493
|
+
const closeMethod = this.stage === 'init' ? 'destroy' : 'end';
|
|
402
494
|
|
|
403
495
|
this.logger.debug(
|
|
404
496
|
{
|
|
@@ -408,12 +500,35 @@ class SMTPConnection extends EventEmitter {
|
|
|
408
500
|
closeMethod
|
|
409
501
|
);
|
|
410
502
|
|
|
411
|
-
|
|
503
|
+
const socket = (this._socket && this._socket.socket) || this._socket;
|
|
504
|
+
|
|
505
|
+
// Detach any in-flight DATA stream from the socket so the source stream
|
|
506
|
+
// can be garbage-collected once the socket is gone.
|
|
507
|
+
if (this._currentDataStream) {
|
|
508
|
+
try {
|
|
509
|
+
this._currentDataStream.unpipe(this._socket);
|
|
510
|
+
} catch (_E) {
|
|
511
|
+
// ignore
|
|
512
|
+
}
|
|
513
|
+
this._currentDataStream = false;
|
|
514
|
+
}
|
|
412
515
|
|
|
413
516
|
if (socket && !socket.destroyed) {
|
|
414
517
|
try {
|
|
415
|
-
|
|
416
|
-
|
|
518
|
+
// Clear socket timeout to prevent timer leaks
|
|
519
|
+
socket.setTimeout(0);
|
|
520
|
+
// Remove all listeners to allow proper garbage collection
|
|
521
|
+
socket.removeListener('data', this._onSocketData);
|
|
522
|
+
socket.removeListener('timeout', this._onSocketTimeout);
|
|
523
|
+
socket.removeListener('close', this._onSocketClose);
|
|
524
|
+
socket.removeListener('end', this._onSocketEnd);
|
|
525
|
+
socket.removeListener('error', this._onSocketError);
|
|
526
|
+
socket.removeListener('error', this._onConnectionSocketError);
|
|
527
|
+
// Absorb errors that may fire during socket teardown (e.g. server
|
|
528
|
+
// sending cleartext after TLS shutdown triggers ERR_SSL_BAD_RECORD_TYPE)
|
|
529
|
+
socket.on('error', TEARDOWN_NOOP);
|
|
530
|
+
socket[closeMethod]();
|
|
531
|
+
} catch (_E) {
|
|
417
532
|
// just ignore
|
|
418
533
|
}
|
|
419
534
|
}
|
|
@@ -454,11 +569,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
454
569
|
}
|
|
455
570
|
|
|
456
571
|
if (this.customAuth.has(this._authMethod)) {
|
|
457
|
-
|
|
572
|
+
const handler = this.customAuth.get(this._authMethod);
|
|
458
573
|
let lastResponse;
|
|
459
574
|
let returned = false;
|
|
460
575
|
|
|
461
|
-
|
|
576
|
+
const resolve = () => {
|
|
462
577
|
if (returned) {
|
|
463
578
|
return;
|
|
464
579
|
}
|
|
@@ -477,7 +592,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
477
592
|
callback(null, true);
|
|
478
593
|
};
|
|
479
594
|
|
|
480
|
-
|
|
595
|
+
const reject = err => {
|
|
481
596
|
if (returned) {
|
|
482
597
|
return;
|
|
483
598
|
}
|
|
@@ -485,7 +600,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
485
600
|
callback(this._formatError(err, 'EAUTH', lastResponse, 'AUTH ' + this._authMethod));
|
|
486
601
|
};
|
|
487
602
|
|
|
488
|
-
|
|
603
|
+
const handlerResponse = handler({
|
|
489
604
|
auth: this._auth,
|
|
490
605
|
method: this._authMethod,
|
|
491
606
|
|
|
@@ -612,7 +727,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
612
727
|
|
|
613
728
|
// ensure that callback is only called once
|
|
614
729
|
let returned = false;
|
|
615
|
-
|
|
730
|
+
const callback = function () {
|
|
616
731
|
if (returned) {
|
|
617
732
|
return;
|
|
618
733
|
}
|
|
@@ -625,13 +740,22 @@ class SMTPConnection extends EventEmitter {
|
|
|
625
740
|
message.on('error', err => callback(this._formatError(err, 'ESTREAM', false, 'API')));
|
|
626
741
|
}
|
|
627
742
|
|
|
628
|
-
|
|
743
|
+
const startTime = Date.now();
|
|
629
744
|
this._setEnvelope(envelope, (err, info) => {
|
|
630
745
|
if (err) {
|
|
746
|
+
// create passthrough stream to consume to prevent OOM
|
|
747
|
+
const stream = new PassThrough();
|
|
748
|
+
if (typeof message.pipe === 'function') {
|
|
749
|
+
message.pipe(stream);
|
|
750
|
+
} else {
|
|
751
|
+
stream.write(message);
|
|
752
|
+
stream.end();
|
|
753
|
+
}
|
|
754
|
+
|
|
631
755
|
return callback(err);
|
|
632
756
|
}
|
|
633
|
-
|
|
634
|
-
|
|
757
|
+
const envelopeTime = Date.now();
|
|
758
|
+
const stream = this._createSendStream((err, str) => {
|
|
635
759
|
if (err) {
|
|
636
760
|
return callback(err);
|
|
637
761
|
}
|
|
@@ -658,6 +782,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
658
782
|
* @param {Function} callback Callback to return once connection is reset
|
|
659
783
|
*/
|
|
660
784
|
reset(callback) {
|
|
785
|
+
const isDestroyedMessage = this._isDestroyedMessage('reset');
|
|
786
|
+
if (isDestroyedMessage) {
|
|
787
|
+
return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
|
|
788
|
+
}
|
|
789
|
+
|
|
661
790
|
this._sendCommand('RSET');
|
|
662
791
|
this._responseActions.push(str => {
|
|
663
792
|
if (str.charAt(0) !== '2') {
|
|
@@ -704,7 +833,13 @@ class SMTPConnection extends EventEmitter {
|
|
|
704
833
|
this._socket.removeListener('timeout', this._onSocketTimeout);
|
|
705
834
|
this._socket.removeListener('close', this._onSocketClose);
|
|
706
835
|
this._socket.removeListener('end', this._onSocketEnd);
|
|
836
|
+
// Switch from connection-phase error handler to normal error handler
|
|
837
|
+
this._socket.removeListener('error', this._onConnectionSocketError);
|
|
838
|
+
// _upgradeConnection (options.connection + secure) may already have attached
|
|
839
|
+
// the normal handler; remove it first so we never end up with a duplicate
|
|
840
|
+
this._socket.removeListener('error', this._onSocketError);
|
|
707
841
|
|
|
842
|
+
this._socket.on('error', this._onSocketError);
|
|
708
843
|
this._socket.on('data', this._onSocketData);
|
|
709
844
|
this._socket.once('close', this._onSocketClose);
|
|
710
845
|
this._socket.once('end', this._onSocketEnd);
|
|
@@ -736,7 +871,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
736
871
|
return;
|
|
737
872
|
}
|
|
738
873
|
|
|
739
|
-
let data =
|
|
874
|
+
let data = chunk.toString('binary');
|
|
740
875
|
let lines = (this._remainder + data).split(/\r?\n/);
|
|
741
876
|
let lastline;
|
|
742
877
|
|
|
@@ -783,7 +918,12 @@ class SMTPConnection extends EventEmitter {
|
|
|
783
918
|
|
|
784
919
|
err = this._formatError(err, type, data, command);
|
|
785
920
|
|
|
786
|
-
|
|
921
|
+
const transientCodes = ['ETIMEDOUT', 'ESOCKET', 'ECONNECTION'];
|
|
922
|
+
if (transientCodes.includes(err.code)) {
|
|
923
|
+
this.logger.warn(data, err.message);
|
|
924
|
+
} else {
|
|
925
|
+
this.logger.error(data, err.message);
|
|
926
|
+
}
|
|
787
927
|
|
|
788
928
|
this.emit('error', err);
|
|
789
929
|
this.close();
|
|
@@ -807,7 +947,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
807
947
|
err.message += ': ' + response;
|
|
808
948
|
}
|
|
809
949
|
|
|
810
|
-
|
|
950
|
+
const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
|
|
811
951
|
if (responseCode) {
|
|
812
952
|
err.responseCode = responseCode;
|
|
813
953
|
}
|
|
@@ -828,15 +968,15 @@ class SMTPConnection extends EventEmitter {
|
|
|
828
968
|
let serverResponse = false;
|
|
829
969
|
|
|
830
970
|
if (this._remainder && this._remainder.trim()) {
|
|
971
|
+
this.lastServerResponse = serverResponse = decodeServerResponse(this._remainder.trim());
|
|
831
972
|
if (this.options.debug || this.options.transactionLog) {
|
|
832
973
|
this.logger.debug(
|
|
833
974
|
{
|
|
834
975
|
tnx: 'server'
|
|
835
976
|
},
|
|
836
|
-
|
|
977
|
+
serverResponse
|
|
837
978
|
);
|
|
838
979
|
}
|
|
839
|
-
this.lastServerResponse = serverResponse = this._remainder.trim();
|
|
840
980
|
}
|
|
841
981
|
|
|
842
982
|
this.logger.info(
|
|
@@ -864,7 +1004,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
864
1004
|
*/
|
|
865
1005
|
_onEnd() {
|
|
866
1006
|
if (this._socket && !this._socket.destroyed) {
|
|
867
|
-
|
|
1007
|
+
// Peer sent FIN — finish our half of the close gracefully rather
|
|
1008
|
+
// than destroying. 'close' fires after the OS finalizes teardown.
|
|
1009
|
+
this._socket.end();
|
|
868
1010
|
}
|
|
869
1011
|
}
|
|
870
1012
|
|
|
@@ -885,6 +1027,8 @@ class SMTPConnection extends EventEmitter {
|
|
|
885
1027
|
return;
|
|
886
1028
|
}
|
|
887
1029
|
this._destroyed = true;
|
|
1030
|
+
// keep the documented public flag in sync with the private state
|
|
1031
|
+
this.destroyed = true;
|
|
888
1032
|
this.emit('end');
|
|
889
1033
|
}
|
|
890
1034
|
|
|
@@ -895,6 +1039,15 @@ class SMTPConnection extends EventEmitter {
|
|
|
895
1039
|
* has been secured
|
|
896
1040
|
*/
|
|
897
1041
|
_upgradeConnection(callback) {
|
|
1042
|
+
// RFC 3207 section 6: the client MUST discard any knowledge obtained from
|
|
1043
|
+
// the server that was not received over the TLS-protected session. Drop any
|
|
1044
|
+
// buffered input received before the handshake so a man-in-the-middle cannot
|
|
1045
|
+
// inject plaintext bytes after the "220" reply (e.g. a CRLF-free fragment that
|
|
1046
|
+
// would otherwise be prepended to the first post-TLS response and parsed as
|
|
1047
|
+
// part of the secured EHLO capabilities). STARTTLS response injection.
|
|
1048
|
+
this._remainder = '';
|
|
1049
|
+
this._responseQueue = [];
|
|
1050
|
+
|
|
898
1051
|
// do not remove all listeners or it breaks node v0.10 as there's
|
|
899
1052
|
// apparently a 'finish' event set that would be cleared as well
|
|
900
1053
|
|
|
@@ -902,21 +1055,32 @@ class SMTPConnection extends EventEmitter {
|
|
|
902
1055
|
this._socket.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards
|
|
903
1056
|
this._socket.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object
|
|
904
1057
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
});
|
|
1058
|
+
const socketPlain = this._socket;
|
|
1059
|
+
const opts = Object.assign(
|
|
1060
|
+
{
|
|
1061
|
+
socket: this._socket,
|
|
1062
|
+
host: this.host
|
|
1063
|
+
},
|
|
1064
|
+
this.options.tls || {}
|
|
1065
|
+
);
|
|
914
1066
|
|
|
915
1067
|
// ensure servername for SNI
|
|
916
1068
|
if (this.servername && !opts.servername) {
|
|
917
1069
|
opts.servername = this.servername;
|
|
918
1070
|
}
|
|
919
1071
|
|
|
1072
|
+
// Remove all listeners from the plain socket to allow proper garbage
|
|
1073
|
+
// collection. Used on both the TLS-success path and the synchronous
|
|
1074
|
+
// tls.connect() throw path; either way the plain socket is done.
|
|
1075
|
+
const removePlainSocketListeners = () => {
|
|
1076
|
+
socketPlain.removeListener('close', this._onSocketClose);
|
|
1077
|
+
socketPlain.removeListener('end', this._onSocketEnd);
|
|
1078
|
+
socketPlain.removeListener('error', this._onSocketError);
|
|
1079
|
+
// the connection-phase handler is attached when upgrading a pre-opened
|
|
1080
|
+
// options.connection socket; strip it so nothing lingers on the plain socket
|
|
1081
|
+
socketPlain.removeListener('error', this._onConnectionSocketError);
|
|
1082
|
+
};
|
|
1083
|
+
|
|
920
1084
|
this.upgrading = true;
|
|
921
1085
|
// tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
|
|
922
1086
|
try {
|
|
@@ -925,12 +1089,12 @@ class SMTPConnection extends EventEmitter {
|
|
|
925
1089
|
this.upgrading = false;
|
|
926
1090
|
this._socket.on('data', this._onSocketData);
|
|
927
1091
|
|
|
928
|
-
|
|
929
|
-
socketPlain.removeListener('end', this._onSocketEnd);
|
|
1092
|
+
removePlainSocketListeners();
|
|
930
1093
|
|
|
931
1094
|
return callback(null, true);
|
|
932
1095
|
});
|
|
933
1096
|
} catch (err) {
|
|
1097
|
+
removePlainSocketListeners();
|
|
934
1098
|
return callback(err);
|
|
935
1099
|
}
|
|
936
1100
|
|
|
@@ -947,18 +1111,27 @@ class SMTPConnection extends EventEmitter {
|
|
|
947
1111
|
|
|
948
1112
|
/**
|
|
949
1113
|
* Processes queued responses from the server
|
|
950
|
-
*
|
|
951
|
-
* @param {Boolean} force If true, ignores _processing flag
|
|
952
1114
|
*/
|
|
953
1115
|
_processResponse() {
|
|
954
1116
|
if (!this._responseQueue.length) {
|
|
955
1117
|
return false;
|
|
956
1118
|
}
|
|
957
1119
|
|
|
958
|
-
|
|
1120
|
+
const raw = (this._responseQueue.shift() || '').toString();
|
|
1121
|
+
|
|
1122
|
+
// Skip unexpected empty lines without consuming a response action or
|
|
1123
|
+
// overwriting lastServerResponse; reprocess whatever else is queued.
|
|
1124
|
+
if (!raw.trim()) {
|
|
1125
|
+
setImmediate(() => this._processResponse());
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
let str = (this.lastServerResponse = decodeServerResponse(raw));
|
|
959
1130
|
|
|
960
1131
|
if (/^\d+-/.test(str.split('\n').pop())) {
|
|
961
|
-
//
|
|
1132
|
+
// last line is still a continuation: put the partial response back on the
|
|
1133
|
+
// queue and wait for the rest rather than dropping it
|
|
1134
|
+
this._responseQueue.unshift(raw);
|
|
962
1135
|
return;
|
|
963
1136
|
}
|
|
964
1137
|
|
|
@@ -971,12 +1144,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
971
1144
|
);
|
|
972
1145
|
}
|
|
973
1146
|
|
|
974
|
-
|
|
975
|
-
// skip unexpected empty lines
|
|
976
|
-
setImmediate(() => this._processResponse());
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
let action = this._responseActions.shift();
|
|
1147
|
+
const action = this._responseActions.shift();
|
|
980
1148
|
|
|
981
1149
|
if (typeof action === 'function') {
|
|
982
1150
|
action.call(this, str);
|
|
@@ -1024,7 +1192,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1024
1192
|
* {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
|
|
1025
1193
|
*/
|
|
1026
1194
|
_setEnvelope(envelope, callback) {
|
|
1027
|
-
|
|
1195
|
+
const args = [];
|
|
1028
1196
|
let useSmtpUtf8 = false;
|
|
1029
1197
|
|
|
1030
1198
|
this._envelope = envelope || {};
|
|
@@ -1059,7 +1227,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1059
1227
|
}
|
|
1060
1228
|
|
|
1061
1229
|
// clone the recipients array for latter manipulation
|
|
1062
|
-
this._envelope.rcptQueue =
|
|
1230
|
+
this._envelope.rcptQueue = [].concat(this._envelope.to || []);
|
|
1063
1231
|
this._envelope.rejected = [];
|
|
1064
1232
|
this._envelope.rejectedErrors = [];
|
|
1065
1233
|
this._envelope.accepted = [];
|
|
@@ -1072,6 +1240,23 @@ class SMTPConnection extends EventEmitter {
|
|
|
1072
1240
|
}
|
|
1073
1241
|
}
|
|
1074
1242
|
|
|
1243
|
+
// RFC 8689: validate REQUIRETLS eligibility before queuing the MAIL FROM
|
|
1244
|
+
// response action, so a rejection here cannot leave an orphaned action in
|
|
1245
|
+
// _responseActions (which would consume the next reply and desync a reused
|
|
1246
|
+
// connection).
|
|
1247
|
+
if (this._envelope.requireTLSExtensionEnabled) {
|
|
1248
|
+
if (!this.secure) {
|
|
1249
|
+
return callback(
|
|
1250
|
+
this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
if (!this._supportedExtensions.includes('REQUIRETLS')) {
|
|
1254
|
+
return callback(
|
|
1255
|
+
this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1075
1260
|
this._responseActions.push(str => {
|
|
1076
1261
|
this._actionMAIL(str, callback);
|
|
1077
1262
|
});
|
|
@@ -1091,7 +1276,10 @@ class SMTPConnection extends EventEmitter {
|
|
|
1091
1276
|
}
|
|
1092
1277
|
|
|
1093
1278
|
if (this._envelope.size && this._supportedExtensions.includes('SIZE')) {
|
|
1094
|
-
|
|
1279
|
+
const sizeValue = Number(this._envelope.size) || 0;
|
|
1280
|
+
if (sizeValue > 0) {
|
|
1281
|
+
args.push('SIZE=' + sizeValue);
|
|
1282
|
+
}
|
|
1095
1283
|
}
|
|
1096
1284
|
|
|
1097
1285
|
// If the server supports DSN and the envelope includes an DSN prop
|
|
@@ -1105,6 +1293,13 @@ class SMTPConnection extends EventEmitter {
|
|
|
1105
1293
|
}
|
|
1106
1294
|
}
|
|
1107
1295
|
|
|
1296
|
+
// RFC 8689: append the REQUIRETLS keyword to MAIL FROM. Eligibility
|
|
1297
|
+
// (TLS connection + server support) was already validated above, before
|
|
1298
|
+
// the response action was queued.
|
|
1299
|
+
if (this._envelope.requireTLSExtensionEnabled) {
|
|
1300
|
+
args.push('REQUIRETLS');
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1108
1303
|
this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : ''));
|
|
1109
1304
|
}
|
|
1110
1305
|
|
|
@@ -1127,7 +1322,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1127
1322
|
throw new Error('ret: ' + JSON.stringify(ret));
|
|
1128
1323
|
}
|
|
1129
1324
|
|
|
1130
|
-
|
|
1325
|
+
const envid = (params.envid || params.id || '').toString() || null;
|
|
1131
1326
|
|
|
1132
1327
|
let notify = params.notify || null;
|
|
1133
1328
|
if (notify) {
|
|
@@ -1135,9 +1330,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
1135
1330
|
notify = notify.split(',');
|
|
1136
1331
|
}
|
|
1137
1332
|
notify = notify.map(n => n.trim().toUpperCase());
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
if (
|
|
1333
|
+
const validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY'];
|
|
1334
|
+
const invalidNotify = notify.filter(n => !validNotify.includes(n));
|
|
1335
|
+
if (invalidNotify.length || (notify.length > 1 && notify.includes('NEVER'))) {
|
|
1141
1336
|
throw new Error('notify: ' + JSON.stringify(notify.join(',')));
|
|
1142
1337
|
}
|
|
1143
1338
|
notify = notify.join(',');
|
|
@@ -1157,7 +1352,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1157
1352
|
}
|
|
1158
1353
|
|
|
1159
1354
|
_getDsnRcptToArgs() {
|
|
1160
|
-
|
|
1355
|
+
const args = [];
|
|
1161
1356
|
// If the server supports DSN and the envelope includes an DSN prop
|
|
1162
1357
|
// then append DSN params to the RCPT TO command
|
|
1163
1358
|
if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
|
|
@@ -1172,12 +1367,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
1172
1367
|
}
|
|
1173
1368
|
|
|
1174
1369
|
_createSendStream(callback) {
|
|
1175
|
-
|
|
1176
|
-
let logStream;
|
|
1370
|
+
const dataStream = new DataStream();
|
|
1177
1371
|
|
|
1178
1372
|
if (this.options.lmtp) {
|
|
1179
1373
|
this._envelope.accepted.forEach((recipient, i) => {
|
|
1180
|
-
|
|
1374
|
+
const final = i === this._envelope.accepted.length - 1;
|
|
1181
1375
|
this._responseActions.push(str => {
|
|
1182
1376
|
this._actionLMTPStream(recipient, final, str, callback);
|
|
1183
1377
|
});
|
|
@@ -1188,12 +1382,13 @@ class SMTPConnection extends EventEmitter {
|
|
|
1188
1382
|
});
|
|
1189
1383
|
}
|
|
1190
1384
|
|
|
1385
|
+
this._currentDataStream = dataStream;
|
|
1191
1386
|
dataStream.pipe(this._socket, {
|
|
1192
1387
|
end: false
|
|
1193
1388
|
});
|
|
1194
1389
|
|
|
1195
1390
|
if (this.options.debug) {
|
|
1196
|
-
logStream = new PassThrough();
|
|
1391
|
+
const logStream = new PassThrough();
|
|
1197
1392
|
logStream.on('readable', () => {
|
|
1198
1393
|
let chunk;
|
|
1199
1394
|
while ((chunk = logStream.read())) {
|
|
@@ -1209,6 +1404,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
1209
1404
|
}
|
|
1210
1405
|
|
|
1211
1406
|
dataStream.once('end', () => {
|
|
1407
|
+
if (this._currentDataStream === dataStream) {
|
|
1408
|
+
this._currentDataStream = false;
|
|
1409
|
+
}
|
|
1212
1410
|
this.logger.info(
|
|
1213
1411
|
{
|
|
1214
1412
|
tnx: 'message',
|
|
@@ -1283,7 +1481,12 @@ class SMTPConnection extends EventEmitter {
|
|
|
1283
1481
|
|
|
1284
1482
|
if (str.charAt(0) !== '2') {
|
|
1285
1483
|
if (this.options.requireTLS) {
|
|
1286
|
-
this._onError(
|
|
1484
|
+
this._onError(
|
|
1485
|
+
new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str),
|
|
1486
|
+
'ECONNECTION',
|
|
1487
|
+
str,
|
|
1488
|
+
'EHLO'
|
|
1489
|
+
);
|
|
1287
1490
|
return;
|
|
1288
1491
|
}
|
|
1289
1492
|
|
|
@@ -1321,6 +1524,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
1321
1524
|
this._supportedExtensions.push('8BITMIME');
|
|
1322
1525
|
}
|
|
1323
1526
|
|
|
1527
|
+
// Detect if the server supports REQUIRETLS (RFC 8689)
|
|
1528
|
+
if (/[ -]REQUIRETLS\b/im.test(str)) {
|
|
1529
|
+
this._supportedExtensions.push('REQUIRETLS');
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1324
1532
|
// Detect if the server supports PIPELINING
|
|
1325
1533
|
if (/[ -]PIPELINING\b/im.test(str)) {
|
|
1326
1534
|
this._supportedExtensions.push('PIPELINING');
|
|
@@ -1461,22 +1669,21 @@ class SMTPConnection extends EventEmitter {
|
|
|
1461
1669
|
* @param {String} str Message from the server
|
|
1462
1670
|
*/
|
|
1463
1671
|
_actionAUTH_CRAM_MD5(str, callback) {
|
|
1464
|
-
|
|
1465
|
-
let challengeString = '';
|
|
1672
|
+
const challengeMatch = str.match(/^334\s+(.+)$/);
|
|
1466
1673
|
|
|
1467
1674
|
if (!challengeMatch) {
|
|
1468
|
-
return callback(
|
|
1469
|
-
|
|
1470
|
-
|
|
1675
|
+
return callback(
|
|
1676
|
+
this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5')
|
|
1677
|
+
);
|
|
1471
1678
|
}
|
|
1472
1679
|
|
|
1473
1680
|
// Decode from base64
|
|
1474
|
-
|
|
1475
|
-
|
|
1681
|
+
const base64decoded = Buffer.from(challengeMatch[1], 'base64').toString('ascii');
|
|
1682
|
+
const hmacMD5 = crypto.createHmac('md5', this._auth.credentials.pass);
|
|
1476
1683
|
|
|
1477
1684
|
hmacMD5.update(base64decoded);
|
|
1478
1685
|
|
|
1479
|
-
|
|
1686
|
+
const prepended = this._auth.credentials.user + ' ' + hmacMD5.digest('hex');
|
|
1480
1687
|
|
|
1481
1688
|
this._responseActions.push(str => {
|
|
1482
1689
|
this._actionAUTH_CRAM_MD5_PASS(str, callback);
|
|
@@ -1597,39 +1804,29 @@ class SMTPConnection extends EventEmitter {
|
|
|
1597
1804
|
* @param {String} str Message from the server
|
|
1598
1805
|
*/
|
|
1599
1806
|
_actionMAIL(str, callback) {
|
|
1600
|
-
let message, curRecipient;
|
|
1601
1807
|
if (Number(str.charAt(0)) !== 2) {
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
}
|
|
1808
|
+
const message =
|
|
1809
|
+
this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(this._envelope.from)
|
|
1810
|
+
? 'Internationalized mailbox name not allowed'
|
|
1811
|
+
: 'Mail command failed';
|
|
1607
1812
|
return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
|
|
1608
1813
|
}
|
|
1609
1814
|
|
|
1610
1815
|
if (!this._envelope.rcptQueue.length) {
|
|
1611
|
-
return callback(this._formatError('
|
|
1612
|
-
} else {
|
|
1613
|
-
this._recipientQueue = [];
|
|
1614
|
-
|
|
1615
|
-
if (this._supportedExtensions.includes('PIPELINING')) {
|
|
1616
|
-
while (this._envelope.rcptQueue.length) {
|
|
1617
|
-
curRecipient = this._envelope.rcptQueue.shift();
|
|
1618
|
-
this._recipientQueue.push(curRecipient);
|
|
1619
|
-
this._responseActions.push(str => {
|
|
1620
|
-
this._actionRCPT(str, callback);
|
|
1621
|
-
});
|
|
1622
|
-
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
|
|
1623
|
-
}
|
|
1624
|
-
} else {
|
|
1625
|
-
curRecipient = this._envelope.rcptQueue.shift();
|
|
1626
|
-
this._recipientQueue.push(curRecipient);
|
|
1627
|
-
this._responseActions.push(str => {
|
|
1628
|
-
this._actionRCPT(str, callback);
|
|
1629
|
-
});
|
|
1630
|
-
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
|
|
1631
|
-
}
|
|
1816
|
+
return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API'));
|
|
1632
1817
|
}
|
|
1818
|
+
|
|
1819
|
+
this._recipientQueue = [];
|
|
1820
|
+
const usePipelining = this._supportedExtensions.includes('PIPELINING');
|
|
1821
|
+
|
|
1822
|
+
do {
|
|
1823
|
+
const curRecipient = this._envelope.rcptQueue.shift();
|
|
1824
|
+
this._recipientQueue.push(curRecipient);
|
|
1825
|
+
this._responseActions.push(str => {
|
|
1826
|
+
this._actionRCPT(str, callback);
|
|
1827
|
+
});
|
|
1828
|
+
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
|
|
1829
|
+
} while (usePipelining && this._envelope.rcptQueue.length);
|
|
1633
1830
|
}
|
|
1634
1831
|
|
|
1635
1832
|
/**
|
|
@@ -1638,16 +1835,14 @@ class SMTPConnection extends EventEmitter {
|
|
|
1638
1835
|
* @param {String} str Message from the server
|
|
1639
1836
|
*/
|
|
1640
1837
|
_actionRCPT(str, callback) {
|
|
1641
|
-
let
|
|
1642
|
-
|
|
1643
|
-
curRecipient = this._recipientQueue.shift();
|
|
1838
|
+
let err;
|
|
1839
|
+
const curRecipient = this._recipientQueue.shift();
|
|
1644
1840
|
if (Number(str.charAt(0)) !== 2) {
|
|
1645
1841
|
// this is a soft error
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
}
|
|
1842
|
+
const message =
|
|
1843
|
+
this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)
|
|
1844
|
+
? 'Internationalized mailbox name not allowed'
|
|
1845
|
+
: 'Recipient command failed';
|
|
1651
1846
|
this._envelope.rejected.push(curRecipient);
|
|
1652
1847
|
// store error for the failed recipient
|
|
1653
1848
|
err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO');
|
|
@@ -1664,18 +1859,18 @@ class SMTPConnection extends EventEmitter {
|
|
|
1664
1859
|
});
|
|
1665
1860
|
this._sendCommand('DATA');
|
|
1666
1861
|
} else {
|
|
1667
|
-
err = this._formatError('
|
|
1862
|
+
err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', str, 'RCPT TO');
|
|
1668
1863
|
err.rejected = this._envelope.rejected;
|
|
1669
1864
|
err.rejectedErrors = this._envelope.rejectedErrors;
|
|
1670
1865
|
return callback(err);
|
|
1671
1866
|
}
|
|
1672
1867
|
} else if (this._envelope.rcptQueue.length) {
|
|
1673
|
-
|
|
1674
|
-
this._recipientQueue.push(
|
|
1868
|
+
const nextRecipient = this._envelope.rcptQueue.shift();
|
|
1869
|
+
this._recipientQueue.push(nextRecipient);
|
|
1675
1870
|
this._responseActions.push(str => {
|
|
1676
1871
|
this._actionRCPT(str, callback);
|
|
1677
1872
|
});
|
|
1678
|
-
this._sendCommand('RCPT TO:<' +
|
|
1873
|
+
this._sendCommand('RCPT TO:<' + nextRecipient + '>' + this._getDsnRcptToArgs());
|
|
1679
1874
|
}
|
|
1680
1875
|
}
|
|
1681
1876
|
|
|
@@ -1691,7 +1886,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1691
1886
|
return callback(this._formatError('Data command failed', 'EENVELOPE', str, 'DATA'));
|
|
1692
1887
|
}
|
|
1693
1888
|
|
|
1694
|
-
|
|
1889
|
+
const response = {
|
|
1695
1890
|
accepted: this._envelope.accepted,
|
|
1696
1891
|
rejected: this._envelope.rejected
|
|
1697
1892
|
};
|
|
@@ -1715,12 +1910,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
1715
1910
|
*/
|
|
1716
1911
|
_actionSMTPStream(str, callback) {
|
|
1717
1912
|
if (Number(str.charAt(0)) !== 2) {
|
|
1718
|
-
// Message failed
|
|
1719
1913
|
return callback(this._formatError('Message failed', 'EMESSAGE', str, 'DATA'));
|
|
1720
|
-
} else {
|
|
1721
|
-
// Message sent succesfully
|
|
1722
|
-
return callback(null, str);
|
|
1723
1914
|
}
|
|
1915
|
+
return callback(null, str);
|
|
1724
1916
|
}
|
|
1725
1917
|
|
|
1726
1918
|
/**
|
|
@@ -1803,7 +1995,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1803
1995
|
let defaultHostname;
|
|
1804
1996
|
try {
|
|
1805
1997
|
defaultHostname = os.hostname() || '';
|
|
1806
|
-
} catch (
|
|
1998
|
+
} catch (_err) {
|
|
1807
1999
|
// fails on windows 7
|
|
1808
2000
|
defaultHostname = 'localhost';
|
|
1809
2001
|
}
|