source: node_modules/nodemailer/lib/smtp-connection/index.js@ 81bc7da

finki-main main
Last change on this file since 81bc7da was 81bc7da, checked in by Klimentina Efremova <klimentina08642@…>, 3 months ago

Initial commit

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