@nocobase/plugin-notification-email 2.3.0-beta.11 → 2.3.0-beta.13
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/client/index.js +1 -1
- package/dist/client-v2/locale.d.ts +1 -1
- package/dist/externalVersion.js +7 -7
- 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 +23 -19
- 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 +235 -161
- package/dist/node_modules/nodemailer/lib/mailer/index.js +106 -52
- 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 +399 -138
- 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 +460 -0
- 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 +316 -207
- 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 +423 -218
- 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 +333 -38
- package/dist/node_modules/nodemailer/lib/xoauth2/index.js +105 -33
- package/dist/node_modules/nodemailer/package.json +1 -1
- package/package.json +3 -3
|
@@ -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
|
|
@@ -58,6 +81,8 @@ class SMTPConnection extends EventEmitter {
|
|
|
58
81
|
this.port = Number(this.options.port) || (this.secureConnection ? 465 : 587);
|
|
59
82
|
this.host = this.options.host || 'localhost';
|
|
60
83
|
|
|
84
|
+
this.servername = this.options.servername ? this.options.servername : !net.isIP(this.host) ? this.host : false;
|
|
85
|
+
|
|
61
86
|
this.allowInternalNetworkInterfaces = this.options.allowInternalNetworkInterfaces || false;
|
|
62
87
|
|
|
63
88
|
if (typeof this.options.secure === 'undefined' && this.port === 465) {
|
|
@@ -65,7 +90,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
65
90
|
this.secureConnection = true;
|
|
66
91
|
}
|
|
67
92
|
|
|
68
|
-
this.name = this.options.name || this._getHostname();
|
|
93
|
+
this.name = (this.options.name || this._getHostname()).toString().replace(/[\r\n]+/g, '');
|
|
69
94
|
|
|
70
95
|
this.logger = shared.getLogger(this.options, {
|
|
71
96
|
component: this.options.component || 'smtp-connection',
|
|
@@ -73,13 +98,12 @@ class SMTPConnection extends EventEmitter {
|
|
|
73
98
|
});
|
|
74
99
|
|
|
75
100
|
this.customAuth = new Map();
|
|
76
|
-
Object.keys(this.options.customAuth || {})
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
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]);
|
|
80
105
|
}
|
|
81
|
-
|
|
82
|
-
});
|
|
106
|
+
}
|
|
83
107
|
|
|
84
108
|
/**
|
|
85
109
|
* Expose version nr, just for the reference
|
|
@@ -122,7 +146,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
122
146
|
|
|
123
147
|
/**
|
|
124
148
|
* The socket connecting to the server
|
|
125
|
-
* @
|
|
149
|
+
* @public
|
|
126
150
|
*/
|
|
127
151
|
this._socket = false;
|
|
128
152
|
|
|
@@ -187,6 +211,13 @@ class SMTPConnection extends EventEmitter {
|
|
|
187
211
|
*/
|
|
188
212
|
this._closing = false;
|
|
189
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
|
+
|
|
190
221
|
/**
|
|
191
222
|
* Callbacks for socket's listeners
|
|
192
223
|
*/
|
|
@@ -195,6 +226,17 @@ class SMTPConnection extends EventEmitter {
|
|
|
195
226
|
this._onSocketClose = () => this._onClose();
|
|
196
227
|
this._onSocketEnd = () => this._onEnd();
|
|
197
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;
|
|
198
240
|
}
|
|
199
241
|
|
|
200
242
|
/**
|
|
@@ -230,17 +272,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
230
272
|
opts.localAddress = this.options.localAddress;
|
|
231
273
|
}
|
|
232
274
|
|
|
233
|
-
let setupConnectionHandlers = () => {
|
|
234
|
-
this._connectionTimeout = setTimeout(() => {
|
|
235
|
-
this._onError('Connection timeout', 'ETIMEDOUT', false, 'CONN');
|
|
236
|
-
}, this.options.connectionTimeout || CONNECTION_TIMEOUT);
|
|
237
|
-
|
|
238
|
-
this._socket.on('error', this._onSocketError);
|
|
239
|
-
};
|
|
240
|
-
|
|
241
275
|
if (this.options.connection) {
|
|
242
276
|
// connection is already opened
|
|
243
277
|
this._socket = this.options.connection;
|
|
278
|
+
this._setupConnectionHandlers();
|
|
279
|
+
|
|
244
280
|
if (this.secureConnection && !this.alreadySecured) {
|
|
245
281
|
setImmediate(() =>
|
|
246
282
|
this._upgradeConnection(err => {
|
|
@@ -258,109 +294,178 @@ class SMTPConnection extends EventEmitter {
|
|
|
258
294
|
} else if (this.options.socket) {
|
|
259
295
|
// socket object is set up but not yet connected
|
|
260
296
|
this._socket = this.options.socket;
|
|
261
|
-
return
|
|
262
|
-
if (err) {
|
|
263
|
-
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
|
|
264
|
-
}
|
|
265
|
-
this.logger.debug(
|
|
266
|
-
{
|
|
267
|
-
tnx: 'dns',
|
|
268
|
-
source: opts.host,
|
|
269
|
-
resolved: resolved.host,
|
|
270
|
-
cached: !!resolved.cached
|
|
271
|
-
},
|
|
272
|
-
'Resolved %s as %s [cache %s]',
|
|
273
|
-
opts.host,
|
|
274
|
-
resolved.host,
|
|
275
|
-
resolved.cached ? 'hit' : 'miss'
|
|
276
|
-
);
|
|
277
|
-
Object.keys(resolved).forEach(key => {
|
|
278
|
-
if (key.charAt(0) !== '_' && resolved[key]) {
|
|
279
|
-
opts[key] = resolved[key];
|
|
280
|
-
}
|
|
281
|
-
});
|
|
297
|
+
return this._resolveAndConnect(opts, _resolved => {
|
|
282
298
|
try {
|
|
283
299
|
this._socket.connect(this.port, this.host, () => {
|
|
284
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
|
+
|
|
285
315
|
this._onConnect();
|
|
286
316
|
});
|
|
287
|
-
|
|
317
|
+
this._setupConnectionHandlers();
|
|
288
318
|
} catch (E) {
|
|
289
319
|
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
|
290
320
|
}
|
|
291
321
|
});
|
|
292
|
-
} else
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return shared.resolveHostname(opts, (err, resolved) => {
|
|
300
|
-
if (err) {
|
|
301
|
-
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
|
|
302
|
-
}
|
|
303
|
-
this.logger.debug(
|
|
304
|
-
{
|
|
305
|
-
tnx: 'dns',
|
|
306
|
-
source: opts.host,
|
|
307
|
-
resolved: resolved.host,
|
|
308
|
-
cached: !!resolved.cached
|
|
309
|
-
},
|
|
310
|
-
'Resolved %s as %s [cache %s]',
|
|
311
|
-
opts.host,
|
|
312
|
-
resolved.host,
|
|
313
|
-
resolved.cached ? 'hit' : 'miss'
|
|
314
|
-
);
|
|
315
|
-
Object.keys(resolved).forEach(key => {
|
|
316
|
-
if (key.charAt(0) !== '_' && resolved[key]) {
|
|
317
|
-
opts[key] = resolved[key];
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
try {
|
|
321
|
-
this._socket = tls.connect(opts, () => {
|
|
322
|
-
this._socket.setKeepAlive(true);
|
|
323
|
-
this._onConnect();
|
|
324
|
-
});
|
|
325
|
-
setupConnectionHandlers();
|
|
326
|
-
} catch (E) {
|
|
327
|
-
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
|
322
|
+
} else {
|
|
323
|
+
if (this.secureConnection) {
|
|
324
|
+
Object.assign(opts, this.options.tls || {});
|
|
325
|
+
|
|
326
|
+
// ensure servername for SNI
|
|
327
|
+
if (this.servername && !opts.servername) {
|
|
328
|
+
opts.servername = this.servername;
|
|
328
329
|
}
|
|
330
|
+
}
|
|
331
|
+
|
|
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);
|
|
329
338
|
});
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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];
|
|
335
369
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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;
|
|
361
399
|
}
|
|
400
|
+
this._socket.setKeepAlive(true);
|
|
401
|
+
this._onConnect();
|
|
362
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;
|
|
363
464
|
}
|
|
465
|
+
|
|
466
|
+
// Update host and retry
|
|
467
|
+
this._connectOpts.host = nextHost;
|
|
468
|
+
this._connectToHost(this._connectOpts, this.secureConnection);
|
|
364
469
|
}
|
|
365
470
|
|
|
366
471
|
/**
|
|
@@ -385,12 +490,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
385
490
|
}
|
|
386
491
|
this._closing = true;
|
|
387
492
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (this.stage === 'init') {
|
|
391
|
-
// Close the socket immediately when connection timed out
|
|
392
|
-
closeMethod = 'destroy';
|
|
393
|
-
}
|
|
493
|
+
const closeMethod = this.stage === 'init' ? 'destroy' : 'end';
|
|
394
494
|
|
|
395
495
|
this.logger.debug(
|
|
396
496
|
{
|
|
@@ -400,12 +500,35 @@ class SMTPConnection extends EventEmitter {
|
|
|
400
500
|
closeMethod
|
|
401
501
|
);
|
|
402
502
|
|
|
403
|
-
|
|
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
|
+
}
|
|
404
515
|
|
|
405
516
|
if (socket && !socket.destroyed) {
|
|
406
517
|
try {
|
|
407
|
-
|
|
408
|
-
|
|
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) {
|
|
409
532
|
// just ignore
|
|
410
533
|
}
|
|
411
534
|
}
|
|
@@ -446,11 +569,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
446
569
|
}
|
|
447
570
|
|
|
448
571
|
if (this.customAuth.has(this._authMethod)) {
|
|
449
|
-
|
|
572
|
+
const handler = this.customAuth.get(this._authMethod);
|
|
450
573
|
let lastResponse;
|
|
451
574
|
let returned = false;
|
|
452
575
|
|
|
453
|
-
|
|
576
|
+
const resolve = () => {
|
|
454
577
|
if (returned) {
|
|
455
578
|
return;
|
|
456
579
|
}
|
|
@@ -469,7 +592,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
469
592
|
callback(null, true);
|
|
470
593
|
};
|
|
471
594
|
|
|
472
|
-
|
|
595
|
+
const reject = err => {
|
|
473
596
|
if (returned) {
|
|
474
597
|
return;
|
|
475
598
|
}
|
|
@@ -477,7 +600,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
477
600
|
callback(this._formatError(err, 'EAUTH', lastResponse, 'AUTH ' + this._authMethod));
|
|
478
601
|
};
|
|
479
602
|
|
|
480
|
-
|
|
603
|
+
const handlerResponse = handler({
|
|
481
604
|
auth: this._auth,
|
|
482
605
|
method: this._authMethod,
|
|
483
606
|
|
|
@@ -604,7 +727,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
604
727
|
|
|
605
728
|
// ensure that callback is only called once
|
|
606
729
|
let returned = false;
|
|
607
|
-
|
|
730
|
+
const callback = function () {
|
|
608
731
|
if (returned) {
|
|
609
732
|
return;
|
|
610
733
|
}
|
|
@@ -617,13 +740,22 @@ class SMTPConnection extends EventEmitter {
|
|
|
617
740
|
message.on('error', err => callback(this._formatError(err, 'ESTREAM', false, 'API')));
|
|
618
741
|
}
|
|
619
742
|
|
|
620
|
-
|
|
743
|
+
const startTime = Date.now();
|
|
621
744
|
this._setEnvelope(envelope, (err, info) => {
|
|
622
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
|
+
|
|
623
755
|
return callback(err);
|
|
624
756
|
}
|
|
625
|
-
|
|
626
|
-
|
|
757
|
+
const envelopeTime = Date.now();
|
|
758
|
+
const stream = this._createSendStream((err, str) => {
|
|
627
759
|
if (err) {
|
|
628
760
|
return callback(err);
|
|
629
761
|
}
|
|
@@ -650,6 +782,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
650
782
|
* @param {Function} callback Callback to return once connection is reset
|
|
651
783
|
*/
|
|
652
784
|
reset(callback) {
|
|
785
|
+
const isDestroyedMessage = this._isDestroyedMessage('reset');
|
|
786
|
+
if (isDestroyedMessage) {
|
|
787
|
+
return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
|
|
788
|
+
}
|
|
789
|
+
|
|
653
790
|
this._sendCommand('RSET');
|
|
654
791
|
this._responseActions.push(str => {
|
|
655
792
|
if (str.charAt(0) !== '2') {
|
|
@@ -696,7 +833,13 @@ class SMTPConnection extends EventEmitter {
|
|
|
696
833
|
this._socket.removeListener('timeout', this._onSocketTimeout);
|
|
697
834
|
this._socket.removeListener('close', this._onSocketClose);
|
|
698
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);
|
|
699
841
|
|
|
842
|
+
this._socket.on('error', this._onSocketError);
|
|
700
843
|
this._socket.on('data', this._onSocketData);
|
|
701
844
|
this._socket.once('close', this._onSocketClose);
|
|
702
845
|
this._socket.once('end', this._onSocketEnd);
|
|
@@ -728,7 +871,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
728
871
|
return;
|
|
729
872
|
}
|
|
730
873
|
|
|
731
|
-
let data =
|
|
874
|
+
let data = chunk.toString('binary');
|
|
732
875
|
let lines = (this._remainder + data).split(/\r?\n/);
|
|
733
876
|
let lastline;
|
|
734
877
|
|
|
@@ -775,7 +918,12 @@ class SMTPConnection extends EventEmitter {
|
|
|
775
918
|
|
|
776
919
|
err = this._formatError(err, type, data, command);
|
|
777
920
|
|
|
778
|
-
|
|
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
|
+
}
|
|
779
927
|
|
|
780
928
|
this.emit('error', err);
|
|
781
929
|
this.close();
|
|
@@ -799,7 +947,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
799
947
|
err.message += ': ' + response;
|
|
800
948
|
}
|
|
801
949
|
|
|
802
|
-
|
|
950
|
+
const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
|
|
803
951
|
if (responseCode) {
|
|
804
952
|
err.responseCode = responseCode;
|
|
805
953
|
}
|
|
@@ -820,15 +968,15 @@ class SMTPConnection extends EventEmitter {
|
|
|
820
968
|
let serverResponse = false;
|
|
821
969
|
|
|
822
970
|
if (this._remainder && this._remainder.trim()) {
|
|
971
|
+
this.lastServerResponse = serverResponse = decodeServerResponse(this._remainder.trim());
|
|
823
972
|
if (this.options.debug || this.options.transactionLog) {
|
|
824
973
|
this.logger.debug(
|
|
825
974
|
{
|
|
826
975
|
tnx: 'server'
|
|
827
976
|
},
|
|
828
|
-
|
|
977
|
+
serverResponse
|
|
829
978
|
);
|
|
830
979
|
}
|
|
831
|
-
this.lastServerResponse = serverResponse = this._remainder.trim();
|
|
832
980
|
}
|
|
833
981
|
|
|
834
982
|
this.logger.info(
|
|
@@ -856,7 +1004,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
856
1004
|
*/
|
|
857
1005
|
_onEnd() {
|
|
858
1006
|
if (this._socket && !this._socket.destroyed) {
|
|
859
|
-
|
|
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();
|
|
860
1010
|
}
|
|
861
1011
|
}
|
|
862
1012
|
|
|
@@ -877,6 +1027,8 @@ class SMTPConnection extends EventEmitter {
|
|
|
877
1027
|
return;
|
|
878
1028
|
}
|
|
879
1029
|
this._destroyed = true;
|
|
1030
|
+
// keep the documented public flag in sync with the private state
|
|
1031
|
+
this.destroyed = true;
|
|
880
1032
|
this.emit('end');
|
|
881
1033
|
}
|
|
882
1034
|
|
|
@@ -887,6 +1039,15 @@ class SMTPConnection extends EventEmitter {
|
|
|
887
1039
|
* has been secured
|
|
888
1040
|
*/
|
|
889
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
|
+
|
|
890
1051
|
// do not remove all listeners or it breaks node v0.10 as there's
|
|
891
1052
|
// apparently a 'finish' event set that would be cleared as well
|
|
892
1053
|
|
|
@@ -894,15 +1055,31 @@ class SMTPConnection extends EventEmitter {
|
|
|
894
1055
|
this._socket.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards
|
|
895
1056
|
this._socket.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object
|
|
896
1057
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
+
);
|
|
902
1066
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1067
|
+
// ensure servername for SNI
|
|
1068
|
+
if (this.servername && !opts.servername) {
|
|
1069
|
+
opts.servername = this.servername;
|
|
1070
|
+
}
|
|
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
|
+
};
|
|
906
1083
|
|
|
907
1084
|
this.upgrading = true;
|
|
908
1085
|
// tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
|
|
@@ -912,12 +1089,12 @@ class SMTPConnection extends EventEmitter {
|
|
|
912
1089
|
this.upgrading = false;
|
|
913
1090
|
this._socket.on('data', this._onSocketData);
|
|
914
1091
|
|
|
915
|
-
|
|
916
|
-
socketPlain.removeListener('end', this._onSocketEnd);
|
|
1092
|
+
removePlainSocketListeners();
|
|
917
1093
|
|
|
918
1094
|
return callback(null, true);
|
|
919
1095
|
});
|
|
920
1096
|
} catch (err) {
|
|
1097
|
+
removePlainSocketListeners();
|
|
921
1098
|
return callback(err);
|
|
922
1099
|
}
|
|
923
1100
|
|
|
@@ -934,18 +1111,27 @@ class SMTPConnection extends EventEmitter {
|
|
|
934
1111
|
|
|
935
1112
|
/**
|
|
936
1113
|
* Processes queued responses from the server
|
|
937
|
-
*
|
|
938
|
-
* @param {Boolean} force If true, ignores _processing flag
|
|
939
1114
|
*/
|
|
940
1115
|
_processResponse() {
|
|
941
1116
|
if (!this._responseQueue.length) {
|
|
942
1117
|
return false;
|
|
943
1118
|
}
|
|
944
1119
|
|
|
945
|
-
|
|
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));
|
|
946
1130
|
|
|
947
1131
|
if (/^\d+-/.test(str.split('\n').pop())) {
|
|
948
|
-
//
|
|
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);
|
|
949
1135
|
return;
|
|
950
1136
|
}
|
|
951
1137
|
|
|
@@ -958,12 +1144,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
958
1144
|
);
|
|
959
1145
|
}
|
|
960
1146
|
|
|
961
|
-
|
|
962
|
-
// skip unexpected empty lines
|
|
963
|
-
setImmediate(() => this._processResponse());
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
let action = this._responseActions.shift();
|
|
1147
|
+
const action = this._responseActions.shift();
|
|
967
1148
|
|
|
968
1149
|
if (typeof action === 'function') {
|
|
969
1150
|
action.call(this, str);
|
|
@@ -1011,7 +1192,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1011
1192
|
* {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
|
|
1012
1193
|
*/
|
|
1013
1194
|
_setEnvelope(envelope, callback) {
|
|
1014
|
-
|
|
1195
|
+
const args = [];
|
|
1015
1196
|
let useSmtpUtf8 = false;
|
|
1016
1197
|
|
|
1017
1198
|
this._envelope = envelope || {};
|
|
@@ -1046,7 +1227,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1046
1227
|
}
|
|
1047
1228
|
|
|
1048
1229
|
// clone the recipients array for latter manipulation
|
|
1049
|
-
this._envelope.rcptQueue =
|
|
1230
|
+
this._envelope.rcptQueue = [].concat(this._envelope.to || []);
|
|
1050
1231
|
this._envelope.rejected = [];
|
|
1051
1232
|
this._envelope.rejectedErrors = [];
|
|
1052
1233
|
this._envelope.accepted = [];
|
|
@@ -1059,6 +1240,23 @@ class SMTPConnection extends EventEmitter {
|
|
|
1059
1240
|
}
|
|
1060
1241
|
}
|
|
1061
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
|
+
|
|
1062
1260
|
this._responseActions.push(str => {
|
|
1063
1261
|
this._actionMAIL(str, callback);
|
|
1064
1262
|
});
|
|
@@ -1078,7 +1276,10 @@ class SMTPConnection extends EventEmitter {
|
|
|
1078
1276
|
}
|
|
1079
1277
|
|
|
1080
1278
|
if (this._envelope.size && this._supportedExtensions.includes('SIZE')) {
|
|
1081
|
-
|
|
1279
|
+
const sizeValue = Number(this._envelope.size) || 0;
|
|
1280
|
+
if (sizeValue > 0) {
|
|
1281
|
+
args.push('SIZE=' + sizeValue);
|
|
1282
|
+
}
|
|
1082
1283
|
}
|
|
1083
1284
|
|
|
1084
1285
|
// If the server supports DSN and the envelope includes an DSN prop
|
|
@@ -1092,6 +1293,13 @@ class SMTPConnection extends EventEmitter {
|
|
|
1092
1293
|
}
|
|
1093
1294
|
}
|
|
1094
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
|
+
|
|
1095
1303
|
this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : ''));
|
|
1096
1304
|
}
|
|
1097
1305
|
|
|
@@ -1114,7 +1322,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1114
1322
|
throw new Error('ret: ' + JSON.stringify(ret));
|
|
1115
1323
|
}
|
|
1116
1324
|
|
|
1117
|
-
|
|
1325
|
+
const envid = (params.envid || params.id || '').toString() || null;
|
|
1118
1326
|
|
|
1119
1327
|
let notify = params.notify || null;
|
|
1120
1328
|
if (notify) {
|
|
@@ -1122,9 +1330,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
1122
1330
|
notify = notify.split(',');
|
|
1123
1331
|
}
|
|
1124
1332
|
notify = notify.map(n => n.trim().toUpperCase());
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
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'))) {
|
|
1128
1336
|
throw new Error('notify: ' + JSON.stringify(notify.join(',')));
|
|
1129
1337
|
}
|
|
1130
1338
|
notify = notify.join(',');
|
|
@@ -1144,7 +1352,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1144
1352
|
}
|
|
1145
1353
|
|
|
1146
1354
|
_getDsnRcptToArgs() {
|
|
1147
|
-
|
|
1355
|
+
const args = [];
|
|
1148
1356
|
// If the server supports DSN and the envelope includes an DSN prop
|
|
1149
1357
|
// then append DSN params to the RCPT TO command
|
|
1150
1358
|
if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
|
|
@@ -1159,12 +1367,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
1159
1367
|
}
|
|
1160
1368
|
|
|
1161
1369
|
_createSendStream(callback) {
|
|
1162
|
-
|
|
1163
|
-
let logStream;
|
|
1370
|
+
const dataStream = new DataStream();
|
|
1164
1371
|
|
|
1165
1372
|
if (this.options.lmtp) {
|
|
1166
1373
|
this._envelope.accepted.forEach((recipient, i) => {
|
|
1167
|
-
|
|
1374
|
+
const final = i === this._envelope.accepted.length - 1;
|
|
1168
1375
|
this._responseActions.push(str => {
|
|
1169
1376
|
this._actionLMTPStream(recipient, final, str, callback);
|
|
1170
1377
|
});
|
|
@@ -1175,12 +1382,13 @@ class SMTPConnection extends EventEmitter {
|
|
|
1175
1382
|
});
|
|
1176
1383
|
}
|
|
1177
1384
|
|
|
1385
|
+
this._currentDataStream = dataStream;
|
|
1178
1386
|
dataStream.pipe(this._socket, {
|
|
1179
1387
|
end: false
|
|
1180
1388
|
});
|
|
1181
1389
|
|
|
1182
1390
|
if (this.options.debug) {
|
|
1183
|
-
logStream = new PassThrough();
|
|
1391
|
+
const logStream = new PassThrough();
|
|
1184
1392
|
logStream.on('readable', () => {
|
|
1185
1393
|
let chunk;
|
|
1186
1394
|
while ((chunk = logStream.read())) {
|
|
@@ -1196,6 +1404,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
1196
1404
|
}
|
|
1197
1405
|
|
|
1198
1406
|
dataStream.once('end', () => {
|
|
1407
|
+
if (this._currentDataStream === dataStream) {
|
|
1408
|
+
this._currentDataStream = false;
|
|
1409
|
+
}
|
|
1199
1410
|
this.logger.info(
|
|
1200
1411
|
{
|
|
1201
1412
|
tnx: 'message',
|
|
@@ -1270,7 +1481,12 @@ class SMTPConnection extends EventEmitter {
|
|
|
1270
1481
|
|
|
1271
1482
|
if (str.charAt(0) !== '2') {
|
|
1272
1483
|
if (this.options.requireTLS) {
|
|
1273
|
-
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
|
+
);
|
|
1274
1490
|
return;
|
|
1275
1491
|
}
|
|
1276
1492
|
|
|
@@ -1308,6 +1524,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
1308
1524
|
this._supportedExtensions.push('8BITMIME');
|
|
1309
1525
|
}
|
|
1310
1526
|
|
|
1527
|
+
// Detect if the server supports REQUIRETLS (RFC 8689)
|
|
1528
|
+
if (/[ -]REQUIRETLS\b/im.test(str)) {
|
|
1529
|
+
this._supportedExtensions.push('REQUIRETLS');
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1311
1532
|
// Detect if the server supports PIPELINING
|
|
1312
1533
|
if (/[ -]PIPELINING\b/im.test(str)) {
|
|
1313
1534
|
this._supportedExtensions.push('PIPELINING');
|
|
@@ -1448,22 +1669,21 @@ class SMTPConnection extends EventEmitter {
|
|
|
1448
1669
|
* @param {String} str Message from the server
|
|
1449
1670
|
*/
|
|
1450
1671
|
_actionAUTH_CRAM_MD5(str, callback) {
|
|
1451
|
-
|
|
1452
|
-
let challengeString = '';
|
|
1672
|
+
const challengeMatch = str.match(/^334\s+(.+)$/);
|
|
1453
1673
|
|
|
1454
1674
|
if (!challengeMatch) {
|
|
1455
|
-
return callback(
|
|
1456
|
-
|
|
1457
|
-
|
|
1675
|
+
return callback(
|
|
1676
|
+
this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5')
|
|
1677
|
+
);
|
|
1458
1678
|
}
|
|
1459
1679
|
|
|
1460
1680
|
// Decode from base64
|
|
1461
|
-
|
|
1462
|
-
|
|
1681
|
+
const base64decoded = Buffer.from(challengeMatch[1], 'base64').toString('ascii');
|
|
1682
|
+
const hmacMD5 = crypto.createHmac('md5', this._auth.credentials.pass);
|
|
1463
1683
|
|
|
1464
1684
|
hmacMD5.update(base64decoded);
|
|
1465
1685
|
|
|
1466
|
-
|
|
1686
|
+
const prepended = this._auth.credentials.user + ' ' + hmacMD5.digest('hex');
|
|
1467
1687
|
|
|
1468
1688
|
this._responseActions.push(str => {
|
|
1469
1689
|
this._actionAUTH_CRAM_MD5_PASS(str, callback);
|
|
@@ -1584,39 +1804,29 @@ class SMTPConnection extends EventEmitter {
|
|
|
1584
1804
|
* @param {String} str Message from the server
|
|
1585
1805
|
*/
|
|
1586
1806
|
_actionMAIL(str, callback) {
|
|
1587
|
-
let message, curRecipient;
|
|
1588
1807
|
if (Number(str.charAt(0)) !== 2) {
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
}
|
|
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';
|
|
1594
1812
|
return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
|
|
1595
1813
|
}
|
|
1596
1814
|
|
|
1597
1815
|
if (!this._envelope.rcptQueue.length) {
|
|
1598
|
-
return callback(this._formatError('
|
|
1599
|
-
} else {
|
|
1600
|
-
this._recipientQueue = [];
|
|
1601
|
-
|
|
1602
|
-
if (this._supportedExtensions.includes('PIPELINING')) {
|
|
1603
|
-
while (this._envelope.rcptQueue.length) {
|
|
1604
|
-
curRecipient = this._envelope.rcptQueue.shift();
|
|
1605
|
-
this._recipientQueue.push(curRecipient);
|
|
1606
|
-
this._responseActions.push(str => {
|
|
1607
|
-
this._actionRCPT(str, callback);
|
|
1608
|
-
});
|
|
1609
|
-
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
|
|
1610
|
-
}
|
|
1611
|
-
} else {
|
|
1612
|
-
curRecipient = this._envelope.rcptQueue.shift();
|
|
1613
|
-
this._recipientQueue.push(curRecipient);
|
|
1614
|
-
this._responseActions.push(str => {
|
|
1615
|
-
this._actionRCPT(str, callback);
|
|
1616
|
-
});
|
|
1617
|
-
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
|
|
1618
|
-
}
|
|
1816
|
+
return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API'));
|
|
1619
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);
|
|
1620
1830
|
}
|
|
1621
1831
|
|
|
1622
1832
|
/**
|
|
@@ -1625,16 +1835,14 @@ class SMTPConnection extends EventEmitter {
|
|
|
1625
1835
|
* @param {String} str Message from the server
|
|
1626
1836
|
*/
|
|
1627
1837
|
_actionRCPT(str, callback) {
|
|
1628
|
-
let
|
|
1629
|
-
|
|
1630
|
-
curRecipient = this._recipientQueue.shift();
|
|
1838
|
+
let err;
|
|
1839
|
+
const curRecipient = this._recipientQueue.shift();
|
|
1631
1840
|
if (Number(str.charAt(0)) !== 2) {
|
|
1632
1841
|
// this is a soft error
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
}
|
|
1842
|
+
const message =
|
|
1843
|
+
this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)
|
|
1844
|
+
? 'Internationalized mailbox name not allowed'
|
|
1845
|
+
: 'Recipient command failed';
|
|
1638
1846
|
this._envelope.rejected.push(curRecipient);
|
|
1639
1847
|
// store error for the failed recipient
|
|
1640
1848
|
err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO');
|
|
@@ -1651,18 +1859,18 @@ class SMTPConnection extends EventEmitter {
|
|
|
1651
1859
|
});
|
|
1652
1860
|
this._sendCommand('DATA');
|
|
1653
1861
|
} else {
|
|
1654
|
-
err = this._formatError('
|
|
1862
|
+
err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', str, 'RCPT TO');
|
|
1655
1863
|
err.rejected = this._envelope.rejected;
|
|
1656
1864
|
err.rejectedErrors = this._envelope.rejectedErrors;
|
|
1657
1865
|
return callback(err);
|
|
1658
1866
|
}
|
|
1659
1867
|
} else if (this._envelope.rcptQueue.length) {
|
|
1660
|
-
|
|
1661
|
-
this._recipientQueue.push(
|
|
1868
|
+
const nextRecipient = this._envelope.rcptQueue.shift();
|
|
1869
|
+
this._recipientQueue.push(nextRecipient);
|
|
1662
1870
|
this._responseActions.push(str => {
|
|
1663
1871
|
this._actionRCPT(str, callback);
|
|
1664
1872
|
});
|
|
1665
|
-
this._sendCommand('RCPT TO:<' +
|
|
1873
|
+
this._sendCommand('RCPT TO:<' + nextRecipient + '>' + this._getDsnRcptToArgs());
|
|
1666
1874
|
}
|
|
1667
1875
|
}
|
|
1668
1876
|
|
|
@@ -1678,7 +1886,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1678
1886
|
return callback(this._formatError('Data command failed', 'EENVELOPE', str, 'DATA'));
|
|
1679
1887
|
}
|
|
1680
1888
|
|
|
1681
|
-
|
|
1889
|
+
const response = {
|
|
1682
1890
|
accepted: this._envelope.accepted,
|
|
1683
1891
|
rejected: this._envelope.rejected
|
|
1684
1892
|
};
|
|
@@ -1702,12 +1910,9 @@ class SMTPConnection extends EventEmitter {
|
|
|
1702
1910
|
*/
|
|
1703
1911
|
_actionSMTPStream(str, callback) {
|
|
1704
1912
|
if (Number(str.charAt(0)) !== 2) {
|
|
1705
|
-
// Message failed
|
|
1706
1913
|
return callback(this._formatError('Message failed', 'EMESSAGE', str, 'DATA'));
|
|
1707
|
-
} else {
|
|
1708
|
-
// Message sent succesfully
|
|
1709
|
-
return callback(null, str);
|
|
1710
1914
|
}
|
|
1915
|
+
return callback(null, str);
|
|
1711
1916
|
}
|
|
1712
1917
|
|
|
1713
1918
|
/**
|
|
@@ -1790,7 +1995,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1790
1995
|
let defaultHostname;
|
|
1791
1996
|
try {
|
|
1792
1997
|
defaultHostname = os.hostname() || '';
|
|
1793
|
-
} catch (
|
|
1998
|
+
} catch (_err) {
|
|
1794
1999
|
// fails on windows 7
|
|
1795
2000
|
defaultHostname = 'localhost';
|
|
1796
2001
|
}
|