'use strict'; const EventEmitter = require('events'); const SMTPConnection = require('../smtp-connection'); const wellKnown = require('../well-known'); const shared = require('../shared'); const XOAuth2 = require('../xoauth2'); const packageData = require('../../package.json'); /** * Creates a SMTP transport object for Nodemailer * * @constructor * @param {Object} options Connection options */ class SMTPTransport extends EventEmitter { constructor(options) { super(); options = options || {}; if (typeof options === 'string') { options = { url: options }; } let urlData; let service = options.service; if (typeof options.getSocket === 'function') { this.getSocket = options.getSocket; } if (options.url) { urlData = shared.parseConnectionUrl(options.url); service = service || urlData.service; } this.options = shared.assign( false, // create new object options, // regular options urlData, // url options service && wellKnown(service) // wellknown options ); this.logger = shared.getLogger(this.options, { component: this.options.component || 'smtp-transport' }); // temporary object let connection = new SMTPConnection(this.options); this.name = 'SMTP'; this.version = packageData.version + '[client:' + connection.version + ']'; if (this.options.auth) { this.auth = this.getAuth({}); } } /** * Placeholder function for creating proxy sockets. This method immediatelly returns * without a socket * * @param {Object} options Connection options * @param {Function} callback Callback function to run with the socket keys */ getSocket(options, callback) { // return immediatelly return setImmediate(() => callback(null, false)); } getAuth(authOpts) { if (!authOpts) { return this.auth; } let hasAuth = false; let authData = {}; if (this.options.auth && typeof this.options.auth === 'object') { Object.keys(this.options.auth).forEach(key => { hasAuth = true; authData[key] = this.options.auth[key]; }); } if (authOpts && typeof authOpts === 'object') { Object.keys(authOpts).forEach(key => { hasAuth = true; authData[key] = authOpts[key]; }); } if (!hasAuth) { return false; } switch ((authData.type || '').toString().toUpperCase()) { case 'OAUTH2': { if (!authData.service && !authData.user) { return false; } let oauth2 = new XOAuth2(authData, this.logger); oauth2.provisionCallback = (this.mailer && this.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback; oauth2.on('token', token => this.mailer.emit('token', token)); oauth2.on('error', err => this.emit('error', err)); return { type: 'OAUTH2', user: authData.user, oauth2, method: 'XOAUTH2' }; } default: return { type: (authData.type || '').toString().toUpperCase() || 'LOGIN', user: authData.user, credentials: { user: authData.user || '', pass: authData.pass, options: authData.options }, method: (authData.method || '').trim().toUpperCase() || this.options.authMethod || false }; } } /** * Sends an e-mail using the selected settings * * @param {Object} mail Mail object * @param {Function} callback Callback function */ send(mail, callback) { this.getSocket(this.options, (err, socketOptions) => { if (err) { return callback(err); } let returned = false; let options = this.options; if (socketOptions && socketOptions.connection) { this.logger.info( { tnx: 'proxy', remoteAddress: socketOptions.connection.remoteAddress, remotePort: socketOptions.connection.remotePort, destHost: options.host || '', destPort: options.port || '', action: 'connected' }, 'Using proxied socket from %s:%s to %s:%s', socketOptions.connection.remoteAddress, socketOptions.connection.remotePort, options.host || '', options.port || '' ); // only copy options if we need to modify it options = shared.assign(false, options); Object.keys(socketOptions).forEach(key => { options[key] = socketOptions[key]; }); } let connection = new SMTPConnection(options); connection.once('error', err => { if (returned) { return; } returned = true; connection.close(); return callback(err); }); connection.once('end', () => { if (returned) { return; } let timer = setTimeout(() => { if (returned) { return; } returned = true; // still have not returned, this means we have an unexpected connection close let err = new Error('Unexpected socket close'); if (connection && connection._socket && connection._socket.upgrading) { // starttls connection errors err.code = 'ETLS'; } callback(err); }, 1000); try { timer.unref(); } catch (E) { // Ignore. Happens on envs with non-node timer implementation } }); let sendMessage = () => { let envelope = mail.message.getEnvelope(); let messageId = mail.message.messageId(); let recipients = [].concat(envelope.to || []); if (recipients.length > 3) { recipients.push('...and ' + recipients.splice(2).length + ' more'); } if (mail.data.dsn) { envelope.dsn = mail.data.dsn; } this.logger.info( { tnx: 'send', messageId }, 'Sending message %s to <%s>', messageId, recipients.join(', ') ); connection.send(envelope, mail.message.createReadStream(), (err, info) => { returned = true; connection.close(); if (err) { this.logger.error( { err, tnx: 'send' }, 'Send error for %s: %s', messageId, err.message ); return callback(err); } info.envelope = { from: envelope.from, to: envelope.to }; info.messageId = messageId; try { return callback(null, info); } catch (E) { this.logger.error( { err: E, tnx: 'callback' }, 'Callback error for %s: %s', messageId, E.message ); } }); }; connection.connect(() => { if (returned) { return; } let auth = this.getAuth(mail.data.auth); if (auth && connection.allowsAuth) { connection.login(auth, err => { if (auth && auth !== this.auth && auth.oauth2) { auth.oauth2.removeAllListeners(); } if (returned) { return; } if (err) { returned = true; connection.close(); return callback(err); } sendMessage(); }); } else { sendMessage(); } }); }); } /** * Verifies SMTP configuration * * @param {Function} callback Callback function */ verify(callback) { let promise; if (!callback) { promise = new Promise((resolve, reject) => { callback = shared.callbackPromise(resolve, reject); }); } this.getSocket(this.options, (err, socketOptions) => { if (err) { return callback(err); } let options = this.options; if (socketOptions && socketOptions.connection) { this.logger.info( { tnx: 'proxy', remoteAddress: socketOptions.connection.remoteAddress, remotePort: socketOptions.connection.remotePort, destHost: options.host || '', destPort: options.port || '', action: 'connected' }, 'Using proxied socket from %s:%s to %s:%s', socketOptions.connection.remoteAddress, socketOptions.connection.remotePort, options.host || '', options.port || '' ); options = shared.assign(false, options); Object.keys(socketOptions).forEach(key => { options[key] = socketOptions[key]; }); } let connection = new SMTPConnection(options); let returned = false; connection.once('error', err => { if (returned) { return; } returned = true; connection.close(); return callback(err); }); connection.once('end', () => { if (returned) { return; } returned = true; return callback(new Error('Connection closed')); }); let finalize = () => { if (returned) { return; } returned = true; connection.quit(); return callback(null, true); }; connection.connect(() => { if (returned) { return; } let authData = this.getAuth({}); if (authData && connection.allowsAuth) { connection.login(authData, err => { if (returned) { return; } if (err) { returned = true; connection.close(); return callback(err); } finalize(); }); } else { finalize(); } }); }); return promise; } /** * Releases resources */ close() { if (this.auth && this.auth.oauth2) { this.auth.oauth2.removeAllListeners(); } this.emit('close'); } } // expose to the world module.exports = SMTPTransport;