aiplang 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/bin/aiplang.js +7 -7
  2. package/package.json +7 -5
  3. package/server/node_modules/.package-lock.json +9 -0
  4. package/server/node_modules/nodemailer/.gitattributes +6 -0
  5. package/server/node_modules/nodemailer/.ncurc.js +9 -0
  6. package/server/node_modules/nodemailer/.prettierignore +8 -0
  7. package/server/node_modules/nodemailer/.prettierrc +12 -0
  8. package/server/node_modules/nodemailer/.prettierrc.js +10 -0
  9. package/server/node_modules/nodemailer/.release-please-config.json +9 -0
  10. package/server/node_modules/nodemailer/CHANGELOG.md +976 -0
  11. package/server/node_modules/nodemailer/CODE_OF_CONDUCT.md +76 -0
  12. package/server/node_modules/nodemailer/LICENSE +16 -0
  13. package/server/node_modules/nodemailer/README.md +86 -0
  14. package/server/node_modules/nodemailer/SECURITY.txt +22 -0
  15. package/server/node_modules/nodemailer/eslint.config.js +88 -0
  16. package/server/node_modules/nodemailer/lib/addressparser/index.js +382 -0
  17. package/server/node_modules/nodemailer/lib/base64/index.js +140 -0
  18. package/server/node_modules/nodemailer/lib/dkim/index.js +245 -0
  19. package/server/node_modules/nodemailer/lib/dkim/message-parser.js +154 -0
  20. package/server/node_modules/nodemailer/lib/dkim/relaxed-body.js +154 -0
  21. package/server/node_modules/nodemailer/lib/dkim/sign.js +116 -0
  22. package/server/node_modules/nodemailer/lib/errors.js +58 -0
  23. package/server/node_modules/nodemailer/lib/fetch/cookies.js +276 -0
  24. package/server/node_modules/nodemailer/lib/fetch/index.js +278 -0
  25. package/server/node_modules/nodemailer/lib/json-transport/index.js +82 -0
  26. package/server/node_modules/nodemailer/lib/mail-composer/index.js +599 -0
  27. package/server/node_modules/nodemailer/lib/mailer/index.js +446 -0
  28. package/server/node_modules/nodemailer/lib/mailer/mail-message.js +312 -0
  29. package/server/node_modules/nodemailer/lib/mime-funcs/index.js +610 -0
  30. package/server/node_modules/nodemailer/lib/mime-funcs/mime-types.js +2109 -0
  31. package/server/node_modules/nodemailer/lib/mime-node/index.js +1334 -0
  32. package/server/node_modules/nodemailer/lib/mime-node/last-newline.js +33 -0
  33. package/server/node_modules/nodemailer/lib/mime-node/le-unix.js +40 -0
  34. package/server/node_modules/nodemailer/lib/mime-node/le-windows.js +49 -0
  35. package/server/node_modules/nodemailer/lib/nodemailer.js +151 -0
  36. package/server/node_modules/nodemailer/lib/punycode/index.js +460 -0
  37. package/server/node_modules/nodemailer/lib/qp/index.js +230 -0
  38. package/server/node_modules/nodemailer/lib/sendmail-transport/index.js +205 -0
  39. package/server/node_modules/nodemailer/lib/ses-transport/index.js +223 -0
  40. package/server/node_modules/nodemailer/lib/shared/index.js +698 -0
  41. package/server/node_modules/nodemailer/lib/smtp-connection/data-stream.js +105 -0
  42. package/server/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +144 -0
  43. package/server/node_modules/nodemailer/lib/smtp-connection/index.js +1903 -0
  44. package/server/node_modules/nodemailer/lib/smtp-pool/index.js +641 -0
  45. package/server/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +256 -0
  46. package/server/node_modules/nodemailer/lib/smtp-transport/index.js +402 -0
  47. package/server/node_modules/nodemailer/lib/stream-transport/index.js +135 -0
  48. package/server/node_modules/nodemailer/lib/well-known/index.js +47 -0
  49. package/server/node_modules/nodemailer/lib/well-known/services.json +619 -0
  50. package/server/node_modules/nodemailer/lib/xoauth2/index.js +436 -0
  51. package/server/node_modules/nodemailer/package.json +48 -0
  52. package/server/server.js +686 -865
  53. /package/{FLUX-PROJECT-KNOWLEDGE.md → aiplang-knowledge.md} +0 -0
@@ -0,0 +1,1903 @@
1
+ 'use strict';
2
+
3
+ const packageInfo = require('../../package.json');
4
+ const { EventEmitter } = require('events');
5
+ const net = require('net');
6
+ const tls = require('tls');
7
+ const os = require('os');
8
+ const crypto = require('crypto');
9
+ const DataStream = require('./data-stream');
10
+ const { PassThrough } = require('stream');
11
+ const shared = require('../shared');
12
+
13
+ // default timeout values in ms
14
+ const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
15
+ const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
16
+ const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
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
+ * Generates a SMTP connection object
22
+ *
23
+ * Optional options object takes the following possible properties:
24
+ *
25
+ * * **port** - is the port to connect to (defaults to 587 or 465)
26
+ * * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
27
+ * * **secure** - use SSL
28
+ * * **ignoreTLS** - ignore server support for STARTTLS
29
+ * * **requireTLS** - forces the client to use STARTTLS
30
+ * * **name** - the name of the client server
31
+ * * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
32
+ * * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 10000)
33
+ * * **connectionTimeout** - how many milliseconds to wait for the connection to establish
34
+ * * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 1 hour)
35
+ * * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
36
+ * * **lmtp** - if true, uses LMTP instead of SMTP protocol
37
+ * * **logger** - bunyan compatible logger interface
38
+ * * **debug** - if true pass SMTP traffic to the logger
39
+ * * **tls** - options for createCredentials
40
+ * * **socket** - existing socket to use instead of creating a new one (see: http://nodejs.org/api/net.html#net_class_net_socket)
41
+ * * **secured** - boolean indicates that the provided socket has already been upgraded to tls
42
+ *
43
+ * @constructor
44
+ * @namespace SMTP Client module
45
+ * @param {Object} [options] Option properties
46
+ */
47
+ class SMTPConnection extends EventEmitter {
48
+ constructor(options) {
49
+ super(options);
50
+
51
+ this.id = crypto.randomBytes(8).toString('base64').replace(/\W/g, '');
52
+ this.stage = 'init';
53
+
54
+ this.options = options || {};
55
+
56
+ this.secureConnection = !!this.options.secure;
57
+ this.alreadySecured = !!this.options.secured;
58
+
59
+ this.port = Number(this.options.port) || (this.secureConnection ? 465 : 587);
60
+ this.host = this.options.host || 'localhost';
61
+
62
+ this.servername = this.options.servername ? this.options.servername : !net.isIP(this.host) ? this.host : false;
63
+
64
+ this.allowInternalNetworkInterfaces = this.options.allowInternalNetworkInterfaces || false;
65
+
66
+ if (typeof this.options.secure === 'undefined' && this.port === 465) {
67
+ // if secure option is not set but port is 465, then default to secure
68
+ this.secureConnection = true;
69
+ }
70
+
71
+ this.name = this.options.name || this._getHostname();
72
+
73
+ this.logger = shared.getLogger(this.options, {
74
+ component: this.options.component || 'smtp-connection',
75
+ sid: this.id
76
+ });
77
+
78
+ this.customAuth = new Map();
79
+ for (const key of Object.keys(this.options.customAuth || {})) {
80
+ const mapKey = (key || '').toString().trim().toUpperCase();
81
+ if (mapKey) {
82
+ this.customAuth.set(mapKey, this.options.customAuth[key]);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Expose version nr, just for the reference
88
+ * @type {String}
89
+ */
90
+ this.version = packageInfo.version;
91
+
92
+ /**
93
+ * If true, then the user is authenticated
94
+ * @type {Boolean}
95
+ */
96
+ this.authenticated = false;
97
+
98
+ /**
99
+ * If set to true, this instance is no longer active
100
+ * @private
101
+ */
102
+ this.destroyed = false;
103
+
104
+ /**
105
+ * Defines if the current connection is secure or not. If not,
106
+ * STARTTLS can be used if available
107
+ * @private
108
+ */
109
+ this.secure = !!this.secureConnection;
110
+
111
+ /**
112
+ * Store incomplete messages coming from the server
113
+ * @private
114
+ */
115
+ this._remainder = '';
116
+
117
+ /**
118
+ * Unprocessed responses from the server
119
+ * @type {Array}
120
+ */
121
+ this._responseQueue = [];
122
+
123
+ this.lastServerResponse = false;
124
+
125
+ /**
126
+ * The socket connecting to the server
127
+ * @public
128
+ */
129
+ this._socket = false;
130
+
131
+ /**
132
+ * Lists supported auth mechanisms
133
+ * @private
134
+ */
135
+ this._supportedAuth = [];
136
+
137
+ /**
138
+ * Set to true, if EHLO response includes "AUTH".
139
+ * If false then authentication is not tried
140
+ */
141
+ this.allowsAuth = false;
142
+
143
+ /**
144
+ * Includes current envelope (from, to)
145
+ * @private
146
+ */
147
+ this._envelope = false;
148
+
149
+ /**
150
+ * Lists supported extensions
151
+ * @private
152
+ */
153
+ this._supportedExtensions = [];
154
+
155
+ /**
156
+ * Defines the maximum allowed size for a single message
157
+ * @private
158
+ */
159
+ this._maxAllowedSize = 0;
160
+
161
+ /**
162
+ * Function queue to run if a data chunk comes from the server
163
+ * @private
164
+ */
165
+ this._responseActions = [];
166
+ this._recipientQueue = [];
167
+
168
+ /**
169
+ * Timeout variable for waiting the greeting
170
+ * @private
171
+ */
172
+ this._greetingTimeout = false;
173
+
174
+ /**
175
+ * Timeout variable for waiting the connection to start
176
+ * @private
177
+ */
178
+ this._connectionTimeout = false;
179
+
180
+ /**
181
+ * If the socket is deemed already closed
182
+ * @private
183
+ */
184
+ this._destroyed = false;
185
+
186
+ /**
187
+ * If the socket is already being closed
188
+ * @private
189
+ */
190
+ this._closing = false;
191
+
192
+ /**
193
+ * Callbacks for socket's listeners
194
+ */
195
+ this._onSocketData = chunk => this._onData(chunk);
196
+ this._onSocketError = error => this._onError(error, 'ESOCKET', false, 'CONN');
197
+ this._onSocketClose = () => this._onClose();
198
+ this._onSocketEnd = () => this._onEnd();
199
+ this._onSocketTimeout = () => this._onTimeout();
200
+
201
+ /**
202
+ * Connection-phase error handler (supports fallback to alternative addresses)
203
+ */
204
+ this._onConnectionSocketError = err => this._onConnectionError(err, 'ESOCKET');
205
+
206
+ /**
207
+ * Connection attempt counter for fallback race condition protection
208
+ * @private
209
+ */
210
+ this._connectionAttemptId = 0;
211
+ }
212
+
213
+ /**
214
+ * Creates a connection to a SMTP server and sets up connection
215
+ * listener
216
+ */
217
+ connect(connectCallback) {
218
+ if (typeof connectCallback === 'function') {
219
+ this.once('connect', () => {
220
+ this.logger.debug(
221
+ {
222
+ tnx: 'smtp'
223
+ },
224
+ 'SMTP handshake finished'
225
+ );
226
+ connectCallback();
227
+ });
228
+
229
+ const isDestroyedMessage = this._isDestroyedMessage('connect');
230
+ if (isDestroyedMessage) {
231
+ return connectCallback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'CONN'));
232
+ }
233
+ }
234
+
235
+ let opts = {
236
+ port: this.port,
237
+ host: this.host,
238
+ allowInternalNetworkInterfaces: this.allowInternalNetworkInterfaces,
239
+ timeout: this.options.dnsTimeout || DNS_TIMEOUT
240
+ };
241
+
242
+ if (this.options.localAddress) {
243
+ opts.localAddress = this.options.localAddress;
244
+ }
245
+
246
+ if (this.options.connection) {
247
+ // connection is already opened
248
+ this._socket = this.options.connection;
249
+ this._setupConnectionHandlers();
250
+
251
+ if (this.secureConnection && !this.alreadySecured) {
252
+ setImmediate(() =>
253
+ this._upgradeConnection(err => {
254
+ if (err) {
255
+ this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN');
256
+ return;
257
+ }
258
+ this._onConnect();
259
+ })
260
+ );
261
+ } else {
262
+ setImmediate(() => this._onConnect());
263
+ }
264
+ return;
265
+ } else if (this.options.socket) {
266
+ // socket object is set up but not yet connected
267
+ this._socket = this.options.socket;
268
+ return this._resolveAndConnect(opts, _resolved => {
269
+ try {
270
+ this._socket.connect(this.port, this.host, () => {
271
+ this._socket.setKeepAlive(true);
272
+ this._onConnect();
273
+ });
274
+ this._setupConnectionHandlers();
275
+ } catch (E) {
276
+ return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
277
+ }
278
+ });
279
+ } else {
280
+ if (this.secureConnection) {
281
+ Object.assign(opts, this.options.tls || {});
282
+
283
+ // ensure servername for SNI
284
+ if (this.servername && !opts.servername) {
285
+ opts.servername = this.servername;
286
+ }
287
+ }
288
+
289
+ return this._resolveAndConnect(opts, resolved => {
290
+ // Store fallback addresses for retry on connection failure
291
+ this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
292
+ this._connectOpts = Object.assign({}, opts);
293
+
294
+ this._connectToHost(opts, this.secureConnection);
295
+ });
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Resolves the hostname and applies resolved values to opts,
301
+ * then calls the provided callback with the resolved data
302
+ *
303
+ * @param {Object} opts Connection options (modified in place)
304
+ * @param {Function} callback Called with resolved data on success
305
+ */
306
+ _resolveAndConnect(opts, callback) {
307
+ return shared.resolveHostname(opts, (err, resolved) => {
308
+ if (err) {
309
+ return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
310
+ }
311
+ this.logger.debug(
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
+ for (const key of Object.keys(resolved)) {
324
+ if (key.charAt(0) !== '_' && resolved[key]) {
325
+ opts[key] = resolved[key];
326
+ }
327
+ }
328
+ callback(resolved);
329
+ });
330
+ }
331
+
332
+ /**
333
+ * Attempts to connect to the specified host address
334
+ *
335
+ * @param {Object} opts Connection options
336
+ * @param {Boolean} secure Whether to use TLS
337
+ */
338
+ _connectToHost(opts, secure) {
339
+ this._connectionAttemptId++;
340
+ const currentAttemptId = this._connectionAttemptId;
341
+
342
+ const connectFn = secure ? tls.connect : net.connect;
343
+ try {
344
+ this._socket = connectFn(opts, () => {
345
+ // Ignore callback if this is a stale connection attempt
346
+ if (this._connectionAttemptId !== currentAttemptId) {
347
+ return;
348
+ }
349
+ this._socket.setKeepAlive(true);
350
+ this._onConnect();
351
+ });
352
+ this._setupConnectionHandlers();
353
+ } catch (E) {
354
+ return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Sets up connection timeout and error handlers
360
+ */
361
+ _setupConnectionHandlers() {
362
+ this._connectionTimeout = setTimeout(() => {
363
+ this._onConnectionError('Connection timeout', 'ETIMEDOUT');
364
+ }, this.options.connectionTimeout || CONNECTION_TIMEOUT);
365
+
366
+ this._socket.on('error', this._onConnectionSocketError);
367
+ }
368
+
369
+ /**
370
+ * Handles connection errors with fallback to alternative addresses
371
+ *
372
+ * @param {Error|String} err Error object or message
373
+ * @param {String} code Error code
374
+ */
375
+ _onConnectionError(err, code) {
376
+ clearTimeout(this._connectionTimeout);
377
+
378
+ // Check if we have fallback addresses to try
379
+ const canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
380
+
381
+ if (!canFallback) {
382
+ // No more fallback addresses, report the error
383
+ this._onError(err, code, false, 'CONN');
384
+ return;
385
+ }
386
+
387
+ const nextHost = this._fallbackAddresses.shift();
388
+
389
+ this.logger.info(
390
+ {
391
+ tnx: 'network',
392
+ failedHost: this._connectOpts.host,
393
+ nextHost,
394
+ error: err.message || err
395
+ },
396
+ 'Connection to %s failed, trying %s',
397
+ this._connectOpts.host,
398
+ nextHost
399
+ );
400
+
401
+ // Clean up current socket
402
+ if (this._socket) {
403
+ try {
404
+ this._socket.removeListener('error', this._onConnectionSocketError);
405
+ this._socket.destroy();
406
+ } catch (_E) {
407
+ // ignore
408
+ }
409
+ this._socket = null;
410
+ }
411
+
412
+ // Update host and retry
413
+ this._connectOpts.host = nextHost;
414
+ this._connectToHost(this._connectOpts, this.secureConnection);
415
+ }
416
+
417
+ /**
418
+ * Sends QUIT
419
+ */
420
+ quit() {
421
+ this._sendCommand('QUIT');
422
+ this._responseActions.push(this.close);
423
+ }
424
+
425
+ /**
426
+ * Closes the connection to the server
427
+ */
428
+ close() {
429
+ clearTimeout(this._connectionTimeout);
430
+ clearTimeout(this._greetingTimeout);
431
+ this._responseActions = [];
432
+
433
+ // allow to run this function only once
434
+ if (this._closing) {
435
+ return;
436
+ }
437
+ this._closing = true;
438
+
439
+ const closeMethod = this.stage === 'init' ? 'destroy' : 'end';
440
+
441
+ this.logger.debug(
442
+ {
443
+ tnx: 'smtp'
444
+ },
445
+ 'Closing connection to the server using "%s"',
446
+ closeMethod
447
+ );
448
+
449
+ const socket = (this._socket && this._socket.socket) || this._socket;
450
+
451
+ if (socket && !socket.destroyed) {
452
+ try {
453
+ // Clear socket timeout to prevent timer leaks
454
+ socket.setTimeout(0);
455
+ // Remove all listeners to allow proper garbage collection
456
+ socket.removeListener('data', this._onSocketData);
457
+ socket.removeListener('timeout', this._onSocketTimeout);
458
+ socket.removeListener('close', this._onSocketClose);
459
+ socket.removeListener('end', this._onSocketEnd);
460
+ socket.removeListener('error', this._onSocketError);
461
+ socket.removeListener('error', this._onConnectionSocketError);
462
+ // Absorb errors that may fire during socket teardown (e.g. server
463
+ // sending cleartext after TLS shutdown triggers ERR_SSL_BAD_RECORD_TYPE)
464
+ socket.on('error', TEARDOWN_NOOP);
465
+ socket[closeMethod]();
466
+ } catch (_E) {
467
+ // just ignore
468
+ }
469
+ }
470
+
471
+ this._destroy();
472
+ }
473
+
474
+ /**
475
+ * Authenticate user
476
+ */
477
+ login(authData, callback) {
478
+ const isDestroyedMessage = this._isDestroyedMessage('login');
479
+ if (isDestroyedMessage) {
480
+ return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
481
+ }
482
+
483
+ this._auth = authData || {};
484
+ // Select SASL authentication method
485
+ this._authMethod = (this._auth.method || '').toString().trim().toUpperCase() || false;
486
+
487
+ if (!this._authMethod && this._auth.oauth2 && !this._auth.credentials) {
488
+ this._authMethod = 'XOAUTH2';
489
+ } else if (!this._authMethod || (this._authMethod === 'XOAUTH2' && !this._auth.oauth2)) {
490
+ // use first supported
491
+ this._authMethod = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim();
492
+ }
493
+
494
+ if (this._authMethod !== 'XOAUTH2' && (!this._auth.credentials || !this._auth.credentials.user || !this._auth.credentials.pass)) {
495
+ if ((this._auth.user && this._auth.pass) || this.customAuth.has(this._authMethod)) {
496
+ this._auth.credentials = {
497
+ user: this._auth.user,
498
+ pass: this._auth.pass,
499
+ options: this._auth.options
500
+ };
501
+ } else {
502
+ return callback(this._formatError('Missing credentials for "' + this._authMethod + '"', 'EAUTH', false, 'API'));
503
+ }
504
+ }
505
+
506
+ if (this.customAuth.has(this._authMethod)) {
507
+ const handler = this.customAuth.get(this._authMethod);
508
+ let lastResponse;
509
+ let returned = false;
510
+
511
+ const resolve = () => {
512
+ if (returned) {
513
+ return;
514
+ }
515
+ returned = true;
516
+ this.logger.info(
517
+ {
518
+ tnx: 'smtp',
519
+ username: this._auth.user,
520
+ action: 'authenticated',
521
+ method: this._authMethod
522
+ },
523
+ 'User %s authenticated',
524
+ JSON.stringify(this._auth.user)
525
+ );
526
+ this.authenticated = true;
527
+ callback(null, true);
528
+ };
529
+
530
+ const reject = err => {
531
+ if (returned) {
532
+ return;
533
+ }
534
+ returned = true;
535
+ callback(this._formatError(err, 'EAUTH', lastResponse, 'AUTH ' + this._authMethod));
536
+ };
537
+
538
+ const handlerResponse = handler({
539
+ auth: this._auth,
540
+ method: this._authMethod,
541
+
542
+ extensions: [].concat(this._supportedExtensions),
543
+ authMethods: [].concat(this._supportedAuth),
544
+ maxAllowedSize: this._maxAllowedSize || false,
545
+
546
+ sendCommand: (cmd, done) => {
547
+ let promise;
548
+
549
+ if (!done) {
550
+ promise = new Promise((resolve, reject) => {
551
+ done = shared.callbackPromise(resolve, reject);
552
+ });
553
+ }
554
+
555
+ this._responseActions.push(str => {
556
+ lastResponse = str;
557
+
558
+ let codes = str.match(/^(\d+)(?:\s(\d+\.\d+\.\d+))?\s/);
559
+ let data = {
560
+ command: cmd,
561
+ response: str
562
+ };
563
+ if (codes) {
564
+ data.status = Number(codes[1]) || 0;
565
+ if (codes[2]) {
566
+ data.code = codes[2];
567
+ }
568
+ data.text = str.substr(codes[0].length);
569
+ } else {
570
+ data.text = str;
571
+ data.status = 0; // just in case we need to perform numeric comparisons
572
+ }
573
+ done(null, data);
574
+ });
575
+ setImmediate(() => this._sendCommand(cmd));
576
+
577
+ return promise;
578
+ },
579
+
580
+ resolve,
581
+ reject
582
+ });
583
+
584
+ if (handlerResponse && typeof handlerResponse.catch === 'function') {
585
+ // a promise was returned
586
+ handlerResponse.then(resolve).catch(reject);
587
+ }
588
+
589
+ return;
590
+ }
591
+
592
+ switch (this._authMethod) {
593
+ case 'XOAUTH2':
594
+ this._handleXOauth2Token(false, callback);
595
+ return;
596
+ case 'LOGIN':
597
+ this._responseActions.push(str => {
598
+ this._actionAUTH_LOGIN_USER(str, callback);
599
+ });
600
+ this._sendCommand('AUTH LOGIN');
601
+ return;
602
+ case 'PLAIN':
603
+ this._responseActions.push(str => {
604
+ this._actionAUTHComplete(str, callback);
605
+ });
606
+ this._sendCommand(
607
+ 'AUTH PLAIN ' +
608
+ Buffer.from(
609
+ //this._auth.user+'\u0000'+
610
+ '\u0000' + // skip authorization identity as it causes problems with some servers
611
+ this._auth.credentials.user +
612
+ '\u0000' +
613
+ this._auth.credentials.pass,
614
+ 'utf-8'
615
+ ).toString('base64'),
616
+ // log entry without passwords
617
+ 'AUTH PLAIN ' +
618
+ Buffer.from(
619
+ //this._auth.user+'\u0000'+
620
+ '\u0000' + // skip authorization identity as it causes problems with some servers
621
+ this._auth.credentials.user +
622
+ '\u0000' +
623
+ '/* secret */',
624
+ 'utf-8'
625
+ ).toString('base64')
626
+ );
627
+ return;
628
+ case 'CRAM-MD5':
629
+ this._responseActions.push(str => {
630
+ this._actionAUTH_CRAM_MD5(str, callback);
631
+ });
632
+ this._sendCommand('AUTH CRAM-MD5');
633
+ return;
634
+ }
635
+
636
+ return callback(this._formatError('Unknown authentication method "' + this._authMethod + '"', 'EAUTH', false, 'API'));
637
+ }
638
+
639
+ /**
640
+ * Sends a message
641
+ *
642
+ * @param {Object} envelope Envelope object, {from: addr, to: [addr]}
643
+ * @param {Object} message String, Buffer or a Stream
644
+ * @param {Function} callback Callback to return once sending is completed
645
+ */
646
+ send(envelope, message, done) {
647
+ if (!message) {
648
+ return done(this._formatError('Empty message', 'EMESSAGE', false, 'API'));
649
+ }
650
+
651
+ const isDestroyedMessage = this._isDestroyedMessage('send message');
652
+ if (isDestroyedMessage) {
653
+ return done(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
654
+ }
655
+
656
+ // reject larger messages than allowed
657
+ if (this._maxAllowedSize && envelope.size > this._maxAllowedSize) {
658
+ return setImmediate(() => {
659
+ done(this._formatError('Message size larger than allowed ' + this._maxAllowedSize, 'EMESSAGE', false, 'MAIL FROM'));
660
+ });
661
+ }
662
+
663
+ // ensure that callback is only called once
664
+ let returned = false;
665
+ const callback = function () {
666
+ if (returned) {
667
+ return;
668
+ }
669
+ returned = true;
670
+
671
+ done(...arguments);
672
+ };
673
+
674
+ if (typeof message.on === 'function') {
675
+ message.on('error', err => callback(this._formatError(err, 'ESTREAM', false, 'API')));
676
+ }
677
+
678
+ const startTime = Date.now();
679
+ this._setEnvelope(envelope, (err, info) => {
680
+ if (err) {
681
+ // create passthrough stream to consume to prevent OOM
682
+ const stream = new PassThrough();
683
+ if (typeof message.pipe === 'function') {
684
+ message.pipe(stream);
685
+ } else {
686
+ stream.write(message);
687
+ stream.end();
688
+ }
689
+
690
+ return callback(err);
691
+ }
692
+ const envelopeTime = Date.now();
693
+ const stream = this._createSendStream((err, str) => {
694
+ if (err) {
695
+ return callback(err);
696
+ }
697
+
698
+ info.envelopeTime = envelopeTime - startTime;
699
+ info.messageTime = Date.now() - envelopeTime;
700
+ info.messageSize = stream.outByteCount;
701
+ info.response = str;
702
+
703
+ return callback(null, info);
704
+ });
705
+ if (typeof message.pipe === 'function') {
706
+ message.pipe(stream);
707
+ } else {
708
+ stream.write(message);
709
+ stream.end();
710
+ }
711
+ });
712
+ }
713
+
714
+ /**
715
+ * Resets connection state
716
+ *
717
+ * @param {Function} callback Callback to return once connection is reset
718
+ */
719
+ reset(callback) {
720
+ this._sendCommand('RSET');
721
+ this._responseActions.push(str => {
722
+ if (str.charAt(0) !== '2') {
723
+ return callback(this._formatError('Could not reset session state. response=' + str, 'EPROTOCOL', str, 'RSET'));
724
+ }
725
+ this._envelope = false;
726
+ return callback(null, true);
727
+ });
728
+ }
729
+
730
+ /**
731
+ * Connection listener that is run when the connection to
732
+ * the server is opened
733
+ *
734
+ * @event
735
+ */
736
+ _onConnect() {
737
+ clearTimeout(this._connectionTimeout);
738
+
739
+ this.logger.info(
740
+ {
741
+ tnx: 'network',
742
+ localAddress: this._socket.localAddress,
743
+ localPort: this._socket.localPort,
744
+ remoteAddress: this._socket.remoteAddress,
745
+ remotePort: this._socket.remotePort
746
+ },
747
+ '%s established to %s:%s',
748
+ this.secure ? 'Secure connection' : 'Connection',
749
+ this._socket.remoteAddress,
750
+ this._socket.remotePort
751
+ );
752
+
753
+ if (this._destroyed) {
754
+ // Connection was established after we already had canceled it
755
+ this.close();
756
+ return;
757
+ }
758
+
759
+ this.stage = 'connected';
760
+
761
+ // clear existing listeners for the socket
762
+ this._socket.removeListener('data', this._onSocketData);
763
+ this._socket.removeListener('timeout', this._onSocketTimeout);
764
+ this._socket.removeListener('close', this._onSocketClose);
765
+ this._socket.removeListener('end', this._onSocketEnd);
766
+ // Switch from connection-phase error handler to normal error handler
767
+ this._socket.removeListener('error', this._onConnectionSocketError);
768
+
769
+ this._socket.on('error', this._onSocketError);
770
+ this._socket.on('data', this._onSocketData);
771
+ this._socket.once('close', this._onSocketClose);
772
+ this._socket.once('end', this._onSocketEnd);
773
+
774
+ this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
775
+ this._socket.on('timeout', this._onSocketTimeout);
776
+
777
+ this._greetingTimeout = setTimeout(() => {
778
+ // if still waiting for greeting, give up
779
+ if (this._socket && !this._destroyed && this._responseActions[0] === this._actionGreeting) {
780
+ this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN');
781
+ }
782
+ }, this.options.greetingTimeout || GREETING_TIMEOUT);
783
+
784
+ this._responseActions.push(this._actionGreeting);
785
+
786
+ // we have a 'data' listener set up so resume socket if it was paused
787
+ this._socket.resume();
788
+ }
789
+
790
+ /**
791
+ * 'data' listener for data coming from the server
792
+ *
793
+ * @event
794
+ * @param {Buffer} chunk Data chunk coming from the server
795
+ */
796
+ _onData(chunk) {
797
+ if (this._destroyed || !chunk || !chunk.length) {
798
+ return;
799
+ }
800
+
801
+ let data = (chunk || '').toString('binary');
802
+ let lines = (this._remainder + data).split(/\r?\n/);
803
+ let lastline;
804
+
805
+ this._remainder = lines.pop();
806
+
807
+ for (let i = 0, len = lines.length; i < len; i++) {
808
+ if (this._responseQueue.length) {
809
+ lastline = this._responseQueue[this._responseQueue.length - 1];
810
+ if (/^\d+-/.test(lastline.split('\n').pop())) {
811
+ this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
812
+ continue;
813
+ }
814
+ }
815
+ this._responseQueue.push(lines[i]);
816
+ }
817
+
818
+ if (this._responseQueue.length) {
819
+ lastline = this._responseQueue[this._responseQueue.length - 1];
820
+ if (/^\d+-/.test(lastline.split('\n').pop())) {
821
+ return;
822
+ }
823
+ }
824
+
825
+ this._processResponse();
826
+ }
827
+
828
+ /**
829
+ * 'error' listener for the socket
830
+ *
831
+ * @event
832
+ * @param {Error} err Error object
833
+ * @param {String} type Error name
834
+ */
835
+ _onError(err, type, data, command) {
836
+ clearTimeout(this._connectionTimeout);
837
+ clearTimeout(this._greetingTimeout);
838
+
839
+ if (this._destroyed) {
840
+ // just ignore, already closed
841
+ // this might happen when a socket is canceled because of reached timeout
842
+ // but the socket timeout error itself receives only after
843
+ return;
844
+ }
845
+
846
+ err = this._formatError(err, type, data, command);
847
+
848
+ const transientCodes = ['ETIMEDOUT', 'ESOCKET', 'ECONNECTION'];
849
+ if (transientCodes.includes(err.code)) {
850
+ this.logger.warn(data, err.message);
851
+ } else {
852
+ this.logger.error(data, err.message);
853
+ }
854
+
855
+ this.emit('error', err);
856
+ this.close();
857
+ }
858
+
859
+ _formatError(message, type, response, command) {
860
+ let err;
861
+
862
+ if (/Error\]$/i.test(Object.prototype.toString.call(message))) {
863
+ err = message;
864
+ } else {
865
+ err = new Error(message);
866
+ }
867
+
868
+ if (type && type !== 'Error') {
869
+ err.code = type;
870
+ }
871
+
872
+ if (response) {
873
+ err.response = response;
874
+ err.message += ': ' + response;
875
+ }
876
+
877
+ const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
878
+ if (responseCode) {
879
+ err.responseCode = responseCode;
880
+ }
881
+
882
+ if (command) {
883
+ err.command = command;
884
+ }
885
+
886
+ return err;
887
+ }
888
+
889
+ /**
890
+ * 'close' listener for the socket
891
+ *
892
+ * @event
893
+ */
894
+ _onClose() {
895
+ let serverResponse = false;
896
+
897
+ if (this._remainder && this._remainder.trim()) {
898
+ if (this.options.debug || this.options.transactionLog) {
899
+ this.logger.debug(
900
+ {
901
+ tnx: 'server'
902
+ },
903
+ this._remainder.replace(/\r?\n$/, '')
904
+ );
905
+ }
906
+ this.lastServerResponse = serverResponse = this._remainder.trim();
907
+ }
908
+
909
+ this.logger.info(
910
+ {
911
+ tnx: 'network'
912
+ },
913
+ 'Connection closed'
914
+ );
915
+
916
+ if (this.upgrading && !this._destroyed) {
917
+ return this._onError(new Error('Connection closed unexpectedly'), 'ETLS', serverResponse, 'CONN');
918
+ } else if (![this._actionGreeting, this.close].includes(this._responseActions[0]) && !this._destroyed) {
919
+ return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', serverResponse, 'CONN');
920
+ } else if (/^[45]\d{2}\b/.test(serverResponse)) {
921
+ return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', serverResponse, 'CONN');
922
+ }
923
+
924
+ this._destroy();
925
+ }
926
+
927
+ /**
928
+ * 'end' listener for the socket
929
+ *
930
+ * @event
931
+ */
932
+ _onEnd() {
933
+ if (this._socket && !this._socket.destroyed) {
934
+ this._socket.destroy();
935
+ }
936
+ }
937
+
938
+ /**
939
+ * 'timeout' listener for the socket
940
+ *
941
+ * @event
942
+ */
943
+ _onTimeout() {
944
+ return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN');
945
+ }
946
+
947
+ /**
948
+ * Destroys the client, emits 'end'
949
+ */
950
+ _destroy() {
951
+ if (this._destroyed) {
952
+ return;
953
+ }
954
+ this._destroyed = true;
955
+ this.emit('end');
956
+ }
957
+
958
+ /**
959
+ * Upgrades the connection to TLS
960
+ *
961
+ * @param {Function} callback Callback function to run when the connection
962
+ * has been secured
963
+ */
964
+ _upgradeConnection(callback) {
965
+ // do not remove all listeners or it breaks node v0.10 as there's
966
+ // apparently a 'finish' event set that would be cleared as well
967
+
968
+ // we can safely keep 'error', 'end', 'close' etc. events
969
+ this._socket.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards
970
+ this._socket.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object
971
+
972
+ const socketPlain = this._socket;
973
+ const opts = Object.assign(
974
+ {
975
+ socket: this._socket,
976
+ host: this.host
977
+ },
978
+ this.options.tls || {}
979
+ );
980
+
981
+ // ensure servername for SNI
982
+ if (this.servername && !opts.servername) {
983
+ opts.servername = this.servername;
984
+ }
985
+
986
+ this.upgrading = true;
987
+ // tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
988
+ try {
989
+ this._socket = tls.connect(opts, () => {
990
+ this.secure = true;
991
+ this.upgrading = false;
992
+ this._socket.on('data', this._onSocketData);
993
+
994
+ // Remove all listeners from the plain socket to allow proper garbage collection
995
+ socketPlain.removeListener('close', this._onSocketClose);
996
+ socketPlain.removeListener('end', this._onSocketEnd);
997
+ socketPlain.removeListener('error', this._onSocketError);
998
+
999
+ return callback(null, true);
1000
+ });
1001
+ } catch (err) {
1002
+ return callback(err);
1003
+ }
1004
+
1005
+ this._socket.on('error', this._onSocketError);
1006
+ this._socket.once('close', this._onSocketClose);
1007
+ this._socket.once('end', this._onSocketEnd);
1008
+
1009
+ this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); // 10 min.
1010
+ this._socket.on('timeout', this._onSocketTimeout);
1011
+
1012
+ // resume in case the socket was paused
1013
+ socketPlain.resume();
1014
+ }
1015
+
1016
+ /**
1017
+ * Processes queued responses from the server
1018
+ *
1019
+ * @param {Boolean} force If true, ignores _processing flag
1020
+ */
1021
+ _processResponse() {
1022
+ if (!this._responseQueue.length) {
1023
+ return false;
1024
+ }
1025
+
1026
+ let str = (this.lastServerResponse = (this._responseQueue.shift() || '').toString());
1027
+
1028
+ if (/^\d+-/.test(str.split('\n').pop())) {
1029
+ // keep waiting for the final part of multiline response
1030
+ return;
1031
+ }
1032
+
1033
+ if (this.options.debug || this.options.transactionLog) {
1034
+ this.logger.debug(
1035
+ {
1036
+ tnx: 'server'
1037
+ },
1038
+ str.replace(/\r?\n$/, '')
1039
+ );
1040
+ }
1041
+
1042
+ if (!str.trim()) {
1043
+ // skip unexpected empty lines
1044
+ setImmediate(() => this._processResponse());
1045
+ }
1046
+
1047
+ const action = this._responseActions.shift();
1048
+
1049
+ if (typeof action === 'function') {
1050
+ action.call(this, str);
1051
+ setImmediate(() => this._processResponse());
1052
+ } else {
1053
+ return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN');
1054
+ }
1055
+ }
1056
+
1057
+ /**
1058
+ * Send a command to the server, append \r\n
1059
+ *
1060
+ * @param {String} str String to be sent to the server
1061
+ * @param {String} logStr Optional string to be used for logging instead of the actual string
1062
+ */
1063
+ _sendCommand(str, logStr) {
1064
+ if (this._destroyed) {
1065
+ // Connection already closed, can't send any more data
1066
+ return;
1067
+ }
1068
+
1069
+ if (this._socket.destroyed) {
1070
+ return this.close();
1071
+ }
1072
+
1073
+ if (this.options.debug || this.options.transactionLog) {
1074
+ this.logger.debug(
1075
+ {
1076
+ tnx: 'client'
1077
+ },
1078
+ (logStr || str || '').toString().replace(/\r?\n$/, '')
1079
+ );
1080
+ }
1081
+
1082
+ this._socket.write(Buffer.from(str + '\r\n', 'utf-8'));
1083
+ }
1084
+
1085
+ /**
1086
+ * Initiates a new message by submitting envelope data, starting with
1087
+ * MAIL FROM: command
1088
+ *
1089
+ * @param {Object} envelope Envelope object in the form of
1090
+ * {from:'...', to:['...']}
1091
+ * or
1092
+ * {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
1093
+ */
1094
+ _setEnvelope(envelope, callback) {
1095
+ const args = [];
1096
+ let useSmtpUtf8 = false;
1097
+
1098
+ this._envelope = envelope || {};
1099
+ this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '').toString().trim();
1100
+
1101
+ this._envelope.to = [].concat(this._envelope.to || []).map(to => ((to && to.address) || to || '').toString().trim());
1102
+
1103
+ if (!this._envelope.to.length) {
1104
+ return callback(this._formatError('No recipients defined', 'EENVELOPE', false, 'API'));
1105
+ }
1106
+
1107
+ if (this._envelope.from && /[\r\n<>]/.test(this._envelope.from)) {
1108
+ return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE', false, 'API'));
1109
+ }
1110
+
1111
+ // check if the sender address uses only ASCII characters,
1112
+ // otherwise require usage of SMTPUTF8 extension
1113
+ if (/[\x80-\uFFFF]/.test(this._envelope.from)) {
1114
+ useSmtpUtf8 = true;
1115
+ }
1116
+
1117
+ for (let i = 0, len = this._envelope.to.length; i < len; i++) {
1118
+ if (!this._envelope.to[i] || /[\r\n<>]/.test(this._envelope.to[i])) {
1119
+ return callback(this._formatError('Invalid recipient ' + JSON.stringify(this._envelope.to[i]), 'EENVELOPE', false, 'API'));
1120
+ }
1121
+
1122
+ // check if the recipients addresses use only ASCII characters,
1123
+ // otherwise require usage of SMTPUTF8 extension
1124
+ if (/[\x80-\uFFFF]/.test(this._envelope.to[i])) {
1125
+ useSmtpUtf8 = true;
1126
+ }
1127
+ }
1128
+
1129
+ // clone the recipients array for latter manipulation
1130
+ this._envelope.rcptQueue = [].concat(this._envelope.to || []);
1131
+ this._envelope.rejected = [];
1132
+ this._envelope.rejectedErrors = [];
1133
+ this._envelope.accepted = [];
1134
+
1135
+ if (this._envelope.dsn) {
1136
+ try {
1137
+ this._envelope.dsn = this._setDsnEnvelope(this._envelope.dsn);
1138
+ } catch (err) {
1139
+ return callback(this._formatError('Invalid DSN ' + err.message, 'EENVELOPE', false, 'API'));
1140
+ }
1141
+ }
1142
+
1143
+ this._responseActions.push(str => {
1144
+ this._actionMAIL(str, callback);
1145
+ });
1146
+
1147
+ // If the server supports SMTPUTF8 and the envelope includes an internationalized
1148
+ // email address then append SMTPUTF8 keyword to the MAIL FROM command
1149
+ if (useSmtpUtf8 && this._supportedExtensions.includes('SMTPUTF8')) {
1150
+ args.push('SMTPUTF8');
1151
+ this._usingSmtpUtf8 = true;
1152
+ }
1153
+
1154
+ // If the server supports 8BITMIME and the message might contain non-ascii bytes
1155
+ // then append the 8BITMIME keyword to the MAIL FROM command
1156
+ if (this._envelope.use8BitMime && this._supportedExtensions.includes('8BITMIME')) {
1157
+ args.push('BODY=8BITMIME');
1158
+ this._using8BitMime = true;
1159
+ }
1160
+
1161
+ if (this._envelope.size && this._supportedExtensions.includes('SIZE')) {
1162
+ args.push('SIZE=' + this._envelope.size);
1163
+ }
1164
+
1165
+ // If the server supports DSN and the envelope includes an DSN prop
1166
+ // then append DSN params to the MAIL FROM command
1167
+ if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
1168
+ if (this._envelope.dsn.ret) {
1169
+ args.push('RET=' + shared.encodeXText(this._envelope.dsn.ret));
1170
+ }
1171
+ if (this._envelope.dsn.envid) {
1172
+ args.push('ENVID=' + shared.encodeXText(this._envelope.dsn.envid));
1173
+ }
1174
+ }
1175
+
1176
+ // RFC 8689: If the envelope requests REQUIRETLS extension
1177
+ // then append REQUIRETLS keyword to the MAIL FROM command
1178
+ // Note: REQUIRETLS can only be used over TLS connections and requires server support
1179
+ if (this._envelope.requireTLSExtensionEnabled) {
1180
+ if (!this.secure) {
1181
+ return callback(
1182
+ this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
1183
+ );
1184
+ }
1185
+ if (!this._supportedExtensions.includes('REQUIRETLS')) {
1186
+ return callback(
1187
+ this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
1188
+ );
1189
+ }
1190
+ args.push('REQUIRETLS');
1191
+ }
1192
+
1193
+ this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : ''));
1194
+ }
1195
+
1196
+ _setDsnEnvelope(params) {
1197
+ let ret = (params.ret || params.return || '').toString().toUpperCase() || null;
1198
+ if (ret) {
1199
+ switch (ret) {
1200
+ case 'HDRS':
1201
+ case 'HEADERS':
1202
+ ret = 'HDRS';
1203
+ break;
1204
+ case 'FULL':
1205
+ case 'BODY':
1206
+ ret = 'FULL';
1207
+ break;
1208
+ }
1209
+ }
1210
+
1211
+ if (ret && !['FULL', 'HDRS'].includes(ret)) {
1212
+ throw new Error('ret: ' + JSON.stringify(ret));
1213
+ }
1214
+
1215
+ const envid = (params.envid || params.id || '').toString() || null;
1216
+
1217
+ let notify = params.notify || null;
1218
+ if (notify) {
1219
+ if (typeof notify === 'string') {
1220
+ notify = notify.split(',');
1221
+ }
1222
+ notify = notify.map(n => n.trim().toUpperCase());
1223
+ const validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY'];
1224
+ const invalidNotify = notify.filter(n => !validNotify.includes(n));
1225
+ if (invalidNotify.length || (notify.length > 1 && notify.includes('NEVER'))) {
1226
+ throw new Error('notify: ' + JSON.stringify(notify.join(',')));
1227
+ }
1228
+ notify = notify.join(',');
1229
+ }
1230
+
1231
+ let orcpt = (params.recipient || params.orcpt || '').toString() || null;
1232
+ if (orcpt && orcpt.indexOf(';') < 0) {
1233
+ orcpt = 'rfc822;' + orcpt;
1234
+ }
1235
+
1236
+ return {
1237
+ ret,
1238
+ envid,
1239
+ notify,
1240
+ orcpt
1241
+ };
1242
+ }
1243
+
1244
+ _getDsnRcptToArgs() {
1245
+ const args = [];
1246
+ // If the server supports DSN and the envelope includes an DSN prop
1247
+ // then append DSN params to the RCPT TO command
1248
+ if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
1249
+ if (this._envelope.dsn.notify) {
1250
+ args.push('NOTIFY=' + shared.encodeXText(this._envelope.dsn.notify));
1251
+ }
1252
+ if (this._envelope.dsn.orcpt) {
1253
+ args.push('ORCPT=' + shared.encodeXText(this._envelope.dsn.orcpt));
1254
+ }
1255
+ }
1256
+ return args.length ? ' ' + args.join(' ') : '';
1257
+ }
1258
+
1259
+ _createSendStream(callback) {
1260
+ const dataStream = new DataStream();
1261
+
1262
+ if (this.options.lmtp) {
1263
+ this._envelope.accepted.forEach((recipient, i) => {
1264
+ const final = i === this._envelope.accepted.length - 1;
1265
+ this._responseActions.push(str => {
1266
+ this._actionLMTPStream(recipient, final, str, callback);
1267
+ });
1268
+ });
1269
+ } else {
1270
+ this._responseActions.push(str => {
1271
+ this._actionSMTPStream(str, callback);
1272
+ });
1273
+ }
1274
+
1275
+ dataStream.pipe(this._socket, {
1276
+ end: false
1277
+ });
1278
+
1279
+ if (this.options.debug) {
1280
+ const logStream = new PassThrough();
1281
+ logStream.on('readable', () => {
1282
+ let chunk;
1283
+ while ((chunk = logStream.read())) {
1284
+ this.logger.debug(
1285
+ {
1286
+ tnx: 'message'
1287
+ },
1288
+ chunk.toString('binary').replace(/\r?\n$/, '')
1289
+ );
1290
+ }
1291
+ });
1292
+ dataStream.pipe(logStream);
1293
+ }
1294
+
1295
+ dataStream.once('end', () => {
1296
+ this.logger.info(
1297
+ {
1298
+ tnx: 'message',
1299
+ inByteCount: dataStream.inByteCount,
1300
+ outByteCount: dataStream.outByteCount
1301
+ },
1302
+ '<%s bytes encoded mime message (source size %s bytes)>',
1303
+ dataStream.outByteCount,
1304
+ dataStream.inByteCount
1305
+ );
1306
+ });
1307
+
1308
+ return dataStream;
1309
+ }
1310
+
1311
+ /** ACTIONS **/
1312
+
1313
+ /**
1314
+ * Will be run after the connection is created and the server sends
1315
+ * a greeting. If the incoming message starts with 220 initiate
1316
+ * SMTP session by sending EHLO command
1317
+ *
1318
+ * @param {String} str Message from the server
1319
+ */
1320
+ _actionGreeting(str) {
1321
+ clearTimeout(this._greetingTimeout);
1322
+
1323
+ if (str.substr(0, 3) !== '220') {
1324
+ this._onError(new Error('Invalid greeting. response=' + str), 'EPROTOCOL', str, 'CONN');
1325
+ return;
1326
+ }
1327
+
1328
+ if (this.options.lmtp) {
1329
+ this._responseActions.push(this._actionLHLO);
1330
+ this._sendCommand('LHLO ' + this.name);
1331
+ } else {
1332
+ this._responseActions.push(this._actionEHLO);
1333
+ this._sendCommand('EHLO ' + this.name);
1334
+ }
1335
+ }
1336
+
1337
+ /**
1338
+ * Handles server response for LHLO command. If it yielded in
1339
+ * error, emit 'error', otherwise treat this as an EHLO response
1340
+ *
1341
+ * @param {String} str Message from the server
1342
+ */
1343
+ _actionLHLO(str) {
1344
+ if (str.charAt(0) !== '2') {
1345
+ this._onError(new Error('Invalid LHLO. response=' + str), 'EPROTOCOL', str, 'LHLO');
1346
+ return;
1347
+ }
1348
+
1349
+ this._actionEHLO(str);
1350
+ }
1351
+
1352
+ /**
1353
+ * Handles server response for EHLO command. If it yielded in
1354
+ * error, try HELO instead, otherwise initiate TLS negotiation
1355
+ * if STARTTLS is supported by the server or move into the
1356
+ * authentication phase.
1357
+ *
1358
+ * @param {String} str Message from the server
1359
+ */
1360
+ _actionEHLO(str) {
1361
+ let match;
1362
+
1363
+ if (str.substr(0, 3) === '421') {
1364
+ this._onError(new Error('Server terminates connection. response=' + str), 'ECONNECTION', str, 'EHLO');
1365
+ return;
1366
+ }
1367
+
1368
+ if (str.charAt(0) !== '2') {
1369
+ if (this.options.requireTLS) {
1370
+ this._onError(
1371
+ new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str),
1372
+ 'ECONNECTION',
1373
+ str,
1374
+ 'EHLO'
1375
+ );
1376
+ return;
1377
+ }
1378
+
1379
+ // Try HELO instead
1380
+ this._responseActions.push(this._actionHELO);
1381
+ this._sendCommand('HELO ' + this.name);
1382
+ return;
1383
+ }
1384
+
1385
+ this._ehloLines = str
1386
+ .split(/\r?\n/)
1387
+ .map(line => line.replace(/^\d+[ -]/, '').trim())
1388
+ .filter(line => line)
1389
+ .slice(1);
1390
+
1391
+ // Detect if the server supports STARTTLS
1392
+ if (!this.secure && !this.options.ignoreTLS && (/[ -]STARTTLS\b/im.test(str) || this.options.requireTLS)) {
1393
+ this._sendCommand('STARTTLS');
1394
+ this._responseActions.push(this._actionSTARTTLS);
1395
+ return;
1396
+ }
1397
+
1398
+ // Detect if the server supports SMTPUTF8
1399
+ if (/[ -]SMTPUTF8\b/im.test(str)) {
1400
+ this._supportedExtensions.push('SMTPUTF8');
1401
+ }
1402
+
1403
+ // Detect if the server supports DSN
1404
+ if (/[ -]DSN\b/im.test(str)) {
1405
+ this._supportedExtensions.push('DSN');
1406
+ }
1407
+
1408
+ // Detect if the server supports 8BITMIME
1409
+ if (/[ -]8BITMIME\b/im.test(str)) {
1410
+ this._supportedExtensions.push('8BITMIME');
1411
+ }
1412
+
1413
+ // Detect if the server supports REQUIRETLS (RFC 8689)
1414
+ if (/[ -]REQUIRETLS\b/im.test(str)) {
1415
+ this._supportedExtensions.push('REQUIRETLS');
1416
+ }
1417
+
1418
+ // Detect if the server supports PIPELINING
1419
+ if (/[ -]PIPELINING\b/im.test(str)) {
1420
+ this._supportedExtensions.push('PIPELINING');
1421
+ }
1422
+
1423
+ // Detect if the server supports AUTH
1424
+ if (/[ -]AUTH\b/i.test(str)) {
1425
+ this.allowsAuth = true;
1426
+ }
1427
+
1428
+ // Detect if the server supports PLAIN auth
1429
+ if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i.test(str)) {
1430
+ this._supportedAuth.push('PLAIN');
1431
+ }
1432
+
1433
+ // Detect if the server supports LOGIN auth
1434
+ if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i.test(str)) {
1435
+ this._supportedAuth.push('LOGIN');
1436
+ }
1437
+
1438
+ // Detect if the server supports CRAM-MD5 auth
1439
+ if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i.test(str)) {
1440
+ this._supportedAuth.push('CRAM-MD5');
1441
+ }
1442
+
1443
+ // Detect if the server supports XOAUTH2 auth
1444
+ if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i.test(str)) {
1445
+ this._supportedAuth.push('XOAUTH2');
1446
+ }
1447
+
1448
+ // Detect if the server supports SIZE extensions (and the max allowed size)
1449
+ if ((match = str.match(/[ -]SIZE(?:[ \t]+(\d+))?/im))) {
1450
+ this._supportedExtensions.push('SIZE');
1451
+ this._maxAllowedSize = Number(match[1]) || 0;
1452
+ }
1453
+
1454
+ this.emit('connect');
1455
+ }
1456
+
1457
+ /**
1458
+ * Handles server response for HELO command. If it yielded in
1459
+ * error, emit 'error', otherwise move into the authentication phase.
1460
+ *
1461
+ * @param {String} str Message from the server
1462
+ */
1463
+ _actionHELO(str) {
1464
+ if (str.charAt(0) !== '2') {
1465
+ this._onError(new Error('Invalid HELO. response=' + str), 'EPROTOCOL', str, 'HELO');
1466
+ return;
1467
+ }
1468
+
1469
+ // assume that authentication is enabled (most probably is not though)
1470
+ this.allowsAuth = true;
1471
+
1472
+ this.emit('connect');
1473
+ }
1474
+
1475
+ /**
1476
+ * Handles server response for STARTTLS command. If there's an error
1477
+ * try HELO instead, otherwise initiate TLS upgrade. If the upgrade
1478
+ * succeedes restart the EHLO
1479
+ *
1480
+ * @param {String} str Message from the server
1481
+ */
1482
+ _actionSTARTTLS(str) {
1483
+ if (str.charAt(0) !== '2') {
1484
+ if (this.options.opportunisticTLS) {
1485
+ this.logger.info(
1486
+ {
1487
+ tnx: 'smtp'
1488
+ },
1489
+ 'Failed STARTTLS upgrade, continuing unencrypted'
1490
+ );
1491
+ return this.emit('connect');
1492
+ }
1493
+ this._onError(new Error('Error upgrading connection with STARTTLS'), 'ETLS', str, 'STARTTLS');
1494
+ return;
1495
+ }
1496
+
1497
+ this._upgradeConnection((err, secured) => {
1498
+ if (err) {
1499
+ this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'STARTTLS');
1500
+ return;
1501
+ }
1502
+
1503
+ this.logger.info(
1504
+ {
1505
+ tnx: 'smtp'
1506
+ },
1507
+ 'Connection upgraded with STARTTLS'
1508
+ );
1509
+
1510
+ if (secured) {
1511
+ // restart session
1512
+ if (this.options.lmtp) {
1513
+ this._responseActions.push(this._actionLHLO);
1514
+ this._sendCommand('LHLO ' + this.name);
1515
+ } else {
1516
+ this._responseActions.push(this._actionEHLO);
1517
+ this._sendCommand('EHLO ' + this.name);
1518
+ }
1519
+ } else {
1520
+ this.emit('connect');
1521
+ }
1522
+ });
1523
+ }
1524
+
1525
+ /**
1526
+ * Handle the response for AUTH LOGIN command. We are expecting
1527
+ * '334 VXNlcm5hbWU6' (base64 for 'Username:'). Data to be sent as
1528
+ * response needs to be base64 encoded username. We do not need
1529
+ * exact match but settle with 334 response in general as some
1530
+ * hosts invalidly use a longer message than VXNlcm5hbWU6
1531
+ *
1532
+ * @param {String} str Message from the server
1533
+ */
1534
+ _actionAUTH_LOGIN_USER(str, callback) {
1535
+ if (!/^334[ -]/.test(str)) {
1536
+ // expecting '334 VXNlcm5hbWU6'
1537
+ callback(this._formatError('Invalid login sequence while waiting for "334 VXNlcm5hbWU6"', 'EAUTH', str, 'AUTH LOGIN'));
1538
+ return;
1539
+ }
1540
+
1541
+ this._responseActions.push(str => {
1542
+ this._actionAUTH_LOGIN_PASS(str, callback);
1543
+ });
1544
+
1545
+ this._sendCommand(Buffer.from(this._auth.credentials.user + '', 'utf-8').toString('base64'));
1546
+ }
1547
+
1548
+ /**
1549
+ * Handle the response for AUTH CRAM-MD5 command. We are expecting
1550
+ * '334 <challenge string>'. Data to be sent as response needs to be
1551
+ * base64 decoded challenge string, MD5 hashed using the password as
1552
+ * a HMAC key, prefixed by the username and a space, and finally all
1553
+ * base64 encoded again.
1554
+ *
1555
+ * @param {String} str Message from the server
1556
+ */
1557
+ _actionAUTH_CRAM_MD5(str, callback) {
1558
+ const challengeMatch = str.match(/^334\s+(.+)$/);
1559
+
1560
+ if (!challengeMatch) {
1561
+ return callback(
1562
+ this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5')
1563
+ );
1564
+ }
1565
+
1566
+ // Decode from base64
1567
+ const base64decoded = Buffer.from(challengeMatch[1], 'base64').toString('ascii');
1568
+ const hmacMD5 = crypto.createHmac('md5', this._auth.credentials.pass);
1569
+
1570
+ hmacMD5.update(base64decoded);
1571
+
1572
+ const prepended = this._auth.credentials.user + ' ' + hmacMD5.digest('hex');
1573
+
1574
+ this._responseActions.push(str => {
1575
+ this._actionAUTH_CRAM_MD5_PASS(str, callback);
1576
+ });
1577
+
1578
+ this._sendCommand(
1579
+ Buffer.from(prepended).toString('base64'),
1580
+ // hidden hash for logs
1581
+ Buffer.from(this._auth.credentials.user + ' /* secret */').toString('base64')
1582
+ );
1583
+ }
1584
+
1585
+ /**
1586
+ * Handles the response to CRAM-MD5 authentication, if there's no error,
1587
+ * the user can be considered logged in. Start waiting for a message to send
1588
+ *
1589
+ * @param {String} str Message from the server
1590
+ */
1591
+ _actionAUTH_CRAM_MD5_PASS(str, callback) {
1592
+ if (!str.match(/^235\s+/)) {
1593
+ return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH CRAM-MD5'));
1594
+ }
1595
+
1596
+ this.logger.info(
1597
+ {
1598
+ tnx: 'smtp',
1599
+ username: this._auth.user,
1600
+ action: 'authenticated',
1601
+ method: this._authMethod
1602
+ },
1603
+ 'User %s authenticated',
1604
+ JSON.stringify(this._auth.user)
1605
+ );
1606
+ this.authenticated = true;
1607
+ callback(null, true);
1608
+ }
1609
+
1610
+ /**
1611
+ * Handle the response for AUTH LOGIN command. We are expecting
1612
+ * '334 UGFzc3dvcmQ6' (base64 for 'Password:'). Data to be sent as
1613
+ * response needs to be base64 encoded password.
1614
+ *
1615
+ * @param {String} str Message from the server
1616
+ */
1617
+ _actionAUTH_LOGIN_PASS(str, callback) {
1618
+ if (!/^334[ -]/.test(str)) {
1619
+ // expecting '334 UGFzc3dvcmQ6'
1620
+ return callback(this._formatError('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6"', 'EAUTH', str, 'AUTH LOGIN'));
1621
+ }
1622
+
1623
+ this._responseActions.push(str => {
1624
+ this._actionAUTHComplete(str, callback);
1625
+ });
1626
+
1627
+ this._sendCommand(
1628
+ Buffer.from((this._auth.credentials.pass || '').toString(), 'utf-8').toString('base64'),
1629
+ // Hidden pass for logs
1630
+ Buffer.from('/* secret */', 'utf-8').toString('base64')
1631
+ );
1632
+ }
1633
+
1634
+ /**
1635
+ * Handles the response for authentication, if there's no error,
1636
+ * the user can be considered logged in. Start waiting for a message to send
1637
+ *
1638
+ * @param {String} str Message from the server
1639
+ */
1640
+ _actionAUTHComplete(str, isRetry, callback) {
1641
+ if (!callback && typeof isRetry === 'function') {
1642
+ callback = isRetry;
1643
+ isRetry = false;
1644
+ }
1645
+
1646
+ if (str.substr(0, 3) === '334') {
1647
+ this._responseActions.push(str => {
1648
+ if (isRetry || this._authMethod !== 'XOAUTH2') {
1649
+ this._actionAUTHComplete(str, true, callback);
1650
+ } else {
1651
+ // fetch a new OAuth2 access token
1652
+ setImmediate(() => this._handleXOauth2Token(true, callback));
1653
+ }
1654
+ });
1655
+ this._sendCommand('');
1656
+ return;
1657
+ }
1658
+
1659
+ if (str.charAt(0) !== '2') {
1660
+ this.logger.info(
1661
+ {
1662
+ tnx: 'smtp',
1663
+ username: this._auth.user,
1664
+ action: 'authfail',
1665
+ method: this._authMethod
1666
+ },
1667
+ 'User %s failed to authenticate',
1668
+ JSON.stringify(this._auth.user)
1669
+ );
1670
+ return callback(this._formatError('Invalid login', 'EAUTH', str, 'AUTH ' + this._authMethod));
1671
+ }
1672
+
1673
+ this.logger.info(
1674
+ {
1675
+ tnx: 'smtp',
1676
+ username: this._auth.user,
1677
+ action: 'authenticated',
1678
+ method: this._authMethod
1679
+ },
1680
+ 'User %s authenticated',
1681
+ JSON.stringify(this._auth.user)
1682
+ );
1683
+ this.authenticated = true;
1684
+ callback(null, true);
1685
+ }
1686
+
1687
+ /**
1688
+ * Handle response for a MAIL FROM: command
1689
+ *
1690
+ * @param {String} str Message from the server
1691
+ */
1692
+ _actionMAIL(str, callback) {
1693
+ if (Number(str.charAt(0)) !== 2) {
1694
+ const message =
1695
+ this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(this._envelope.from)
1696
+ ? 'Internationalized mailbox name not allowed'
1697
+ : 'Mail command failed';
1698
+ return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
1699
+ }
1700
+
1701
+ if (!this._envelope.rcptQueue.length) {
1702
+ return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API'));
1703
+ }
1704
+
1705
+ this._recipientQueue = [];
1706
+ const usePipelining = this._supportedExtensions.includes('PIPELINING');
1707
+
1708
+ do {
1709
+ const curRecipient = this._envelope.rcptQueue.shift();
1710
+ this._recipientQueue.push(curRecipient);
1711
+ this._responseActions.push(str => {
1712
+ this._actionRCPT(str, callback);
1713
+ });
1714
+ this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
1715
+ } while (usePipelining && this._envelope.rcptQueue.length);
1716
+ }
1717
+
1718
+ /**
1719
+ * Handle response for a RCPT TO: command
1720
+ *
1721
+ * @param {String} str Message from the server
1722
+ */
1723
+ _actionRCPT(str, callback) {
1724
+ let err;
1725
+ const curRecipient = this._recipientQueue.shift();
1726
+ if (Number(str.charAt(0)) !== 2) {
1727
+ // this is a soft error
1728
+ const message =
1729
+ this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)
1730
+ ? 'Internationalized mailbox name not allowed'
1731
+ : 'Recipient command failed';
1732
+ this._envelope.rejected.push(curRecipient);
1733
+ // store error for the failed recipient
1734
+ err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO');
1735
+ err.recipient = curRecipient;
1736
+ this._envelope.rejectedErrors.push(err);
1737
+ } else {
1738
+ this._envelope.accepted.push(curRecipient);
1739
+ }
1740
+
1741
+ if (!this._envelope.rcptQueue.length && !this._recipientQueue.length) {
1742
+ if (this._envelope.rejected.length < this._envelope.to.length) {
1743
+ this._responseActions.push(str => {
1744
+ this._actionDATA(str, callback);
1745
+ });
1746
+ this._sendCommand('DATA');
1747
+ } else {
1748
+ err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', str, 'RCPT TO');
1749
+ err.rejected = this._envelope.rejected;
1750
+ err.rejectedErrors = this._envelope.rejectedErrors;
1751
+ return callback(err);
1752
+ }
1753
+ } else if (this._envelope.rcptQueue.length) {
1754
+ const nextRecipient = this._envelope.rcptQueue.shift();
1755
+ this._recipientQueue.push(nextRecipient);
1756
+ this._responseActions.push(str => {
1757
+ this._actionRCPT(str, callback);
1758
+ });
1759
+ this._sendCommand('RCPT TO:<' + nextRecipient + '>' + this._getDsnRcptToArgs());
1760
+ }
1761
+ }
1762
+
1763
+ /**
1764
+ * Handle response for a DATA command
1765
+ *
1766
+ * @param {String} str Message from the server
1767
+ */
1768
+ _actionDATA(str, callback) {
1769
+ // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
1770
+ // some servers might use 250 instead, so lets check for 2 or 3 as the first digit
1771
+ if (!/^[23]/.test(str)) {
1772
+ return callback(this._formatError('Data command failed', 'EENVELOPE', str, 'DATA'));
1773
+ }
1774
+
1775
+ const response = {
1776
+ accepted: this._envelope.accepted,
1777
+ rejected: this._envelope.rejected
1778
+ };
1779
+
1780
+ if (this._ehloLines && this._ehloLines.length) {
1781
+ response.ehlo = this._ehloLines;
1782
+ }
1783
+
1784
+ if (this._envelope.rejectedErrors.length) {
1785
+ response.rejectedErrors = this._envelope.rejectedErrors;
1786
+ }
1787
+
1788
+ callback(null, response);
1789
+ }
1790
+
1791
+ /**
1792
+ * Handle response for a DATA stream when using SMTP
1793
+ * We expect a single response that defines if the sending succeeded or failed
1794
+ *
1795
+ * @param {String} str Message from the server
1796
+ */
1797
+ _actionSMTPStream(str, callback) {
1798
+ if (Number(str.charAt(0)) !== 2) {
1799
+ return callback(this._formatError('Message failed', 'EMESSAGE', str, 'DATA'));
1800
+ }
1801
+ return callback(null, str);
1802
+ }
1803
+
1804
+ /**
1805
+ * Handle response for a DATA stream
1806
+ * We expect a separate response for every recipient. All recipients can either
1807
+ * succeed or fail separately
1808
+ *
1809
+ * @param {String} recipient The recipient this response applies to
1810
+ * @param {Boolean} final Is this the final recipient?
1811
+ * @param {String} str Message from the server
1812
+ */
1813
+ _actionLMTPStream(recipient, final, str, callback) {
1814
+ let err;
1815
+ if (Number(str.charAt(0)) !== 2) {
1816
+ // Message failed
1817
+ err = this._formatError('Message failed for recipient ' + recipient, 'EMESSAGE', str, 'DATA');
1818
+ err.recipient = recipient;
1819
+ this._envelope.rejected.push(recipient);
1820
+ this._envelope.rejectedErrors.push(err);
1821
+ for (let i = 0, len = this._envelope.accepted.length; i < len; i++) {
1822
+ if (this._envelope.accepted[i] === recipient) {
1823
+ this._envelope.accepted.splice(i, 1);
1824
+ }
1825
+ }
1826
+ }
1827
+ if (final) {
1828
+ return callback(null, str);
1829
+ }
1830
+ }
1831
+
1832
+ _handleXOauth2Token(isRetry, callback) {
1833
+ this._auth.oauth2.getToken(isRetry, (err, accessToken) => {
1834
+ if (err) {
1835
+ this.logger.info(
1836
+ {
1837
+ tnx: 'smtp',
1838
+ username: this._auth.user,
1839
+ action: 'authfail',
1840
+ method: this._authMethod
1841
+ },
1842
+ 'User %s failed to authenticate',
1843
+ JSON.stringify(this._auth.user)
1844
+ );
1845
+ return callback(this._formatError(err, 'EAUTH', false, 'AUTH XOAUTH2'));
1846
+ }
1847
+ this._responseActions.push(str => {
1848
+ this._actionAUTHComplete(str, isRetry, callback);
1849
+ });
1850
+ this._sendCommand(
1851
+ 'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token(accessToken),
1852
+ // Hidden for logs
1853
+ 'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token('/* secret */')
1854
+ );
1855
+ });
1856
+ }
1857
+
1858
+ /**
1859
+ *
1860
+ * @param {string} command
1861
+ * @private
1862
+ */
1863
+ _isDestroyedMessage(command) {
1864
+ if (this._destroyed) {
1865
+ return 'Cannot ' + command + ' - smtp connection is already destroyed.';
1866
+ }
1867
+
1868
+ if (this._socket) {
1869
+ if (this._socket.destroyed) {
1870
+ return 'Cannot ' + command + ' - smtp connection socket is already destroyed.';
1871
+ }
1872
+
1873
+ if (!this._socket.writable) {
1874
+ return 'Cannot ' + command + ' - smtp connection socket is already half-closed.';
1875
+ }
1876
+ }
1877
+ }
1878
+
1879
+ _getHostname() {
1880
+ // defaul hostname is machine hostname or [IP]
1881
+ let defaultHostname;
1882
+ try {
1883
+ defaultHostname = os.hostname() || '';
1884
+ } catch (_err) {
1885
+ // fails on windows 7
1886
+ defaultHostname = 'localhost';
1887
+ }
1888
+
1889
+ // ignore if not FQDN
1890
+ if (!defaultHostname || defaultHostname.indexOf('.') < 0) {
1891
+ defaultHostname = '[127.0.0.1]';
1892
+ }
1893
+
1894
+ // IP should be enclosed in []
1895
+ if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
1896
+ defaultHostname = '[' + defaultHostname + ']';
1897
+ }
1898
+
1899
+ return defaultHostname;
1900
+ }
1901
+ }
1902
+
1903
+ module.exports = SMTPConnection;