123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- '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.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.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;
|