继续操作前请注册或者登录。
index.js 12.4 KB
'use strict';

const EventEmitter = require('events');
const shared = require('../shared');
const mimeTypes = require('../mime-funcs/mime-types');
const MailComposer = require('../mail-composer');
const DKIM = require('../dkim');
const httpProxyClient = require('../smtp-connection/http-proxy-client');
const util = require('util');
const urllib = require('url');
const packageData = require('../../package.json');
const MailMessage = require('./mail-message');
const net = require('net');
const dns = require('dns');
const crypto = require('crypto');

/**
 * Creates an object for exposing the Mail API
 *
 * @constructor
 * @param {Object} transporter Transport object instance to pass the mails to
 */
class Mail extends EventEmitter {
    constructor(transporter, options, defaults) {
        super();

        this.options = options || {};
        this._defaults = defaults || {};

        this._defaultPlugins = {
            compile: [(...args) => this._convertDataImages(...args)],
            stream: []
        };

        this._userPlugins = {
            compile: [],
            stream: []
        };

        this.meta = new Map();

        this.dkim = this.options.dkim ? new DKIM(this.options.dkim) : false;

        this.transporter = transporter;
        this.transporter.mailer = this;

        this.logger = shared.getLogger(this.options, {
            component: this.options.component || 'mail'
        });

        this.logger.debug({
            tnx: 'create'
        }, 'Creating transport: %s', this.getVersionString());

        // setup emit handlers for the transporter
        if (typeof transporter.on === 'function') {

            // deprecated log interface
            this.transporter.on('log', log => {
                this.logger.debug({
                    tnx: 'transport'
                }, '%s: %s', log.type, log.message);
            });

            // transporter errors
            this.transporter.on('error', err => {
                this.logger.error({
                    err,
                    tnx: 'transport'
                }, 'Transport Error: %s', err.message);
                this.emit('error', err);
            });

            // indicates if the sender has became idle
            this.transporter.on('idle', (...args) => {
                this.emit('idle', ...args);
            });
        }

        /**
         * Optional methods passed to the underlying transport object
         */
        ['close', 'isIdle', 'verify'].forEach(method => {
            this[method] = (...args) => {
                if (typeof this.transporter[method] === 'function') {
                    return this.transporter[method](...args);
                } else {
                    this.logger.warn({
                        tnx: 'transport',
                        methodName: method
                    }, 'Non existing method %s called for transport', method);
                    return false;
                }
            };
        });

        // setup proxy handling
        if (this.options.proxy && typeof this.options.proxy === 'string') {
            this.setupProxy(this.options.proxy);
        }
    }

    use(step, plugin) {
        step = (step || '').toString();
        if (!this._userPlugins.hasOwnProperty(step)) {
            this._userPlugins[step] = [plugin];
        } else {
            this._userPlugins[step].push(plugin);
        }
    }

    /**
     * Sends an email using the preselected transport object
     *
     * @param {Object} data E-data description
     * @param {Function} callback Callback to run once the sending succeeded or failed
     */
    sendMail(data, callback) {
        let promise;

        if (!callback && typeof Promise === 'function') {
            promise = new Promise((resolve, reject) => {
                callback = shared.callbackPromise(resolve, reject);
            });
        }

        if (typeof this.getSocket === 'function') {
            this.transporter.getSocket = this.getSocket;
            this.getSocket = false;
        }

        let mail = new MailMessage(this, data);

        this.logger.debug({
            tnx: 'transport',
            name: this.transporter.name,
            version: this.transporter.version,
            action: 'send'
        }, 'Sending mail using %s/%s', this.transporter.name, this.transporter.version);

        this._processPlugins('compile', mail, err => {
            if (err) {
                this.logger.error({
                    err,
                    tnx: 'plugin',
                    action: 'compile'
                }, 'PluginCompile Error: %s', err.message);
                return callback(err);
            }

            mail.message = new MailComposer(mail.data).compile();

            mail.setMailerHeader();
            mail.setPriorityHeaders();
            mail.setListHeaders();

            this._processPlugins('stream', mail, err => {
                if (err) {
                    this.logger.error({
                        err,
                        tnx: 'plugin',
                        action: 'stream'
                    }, 'PluginStream Error: %s', err.message);
                    return callback(err);
                }

                if (mail.data.dkim || this.dkim) {
                    mail.message.processFunc(input => {
                        let dkim = mail.data.dkim ? new DKIM(mail.data.dkim) : this.dkim;
                        this.logger.debug({
                            tnx: 'DKIM',
                            messageId: mail.message.messageId(),
                            dkimDomains: dkim.keys.map(key => key.keySelector + '.' + key.domainName).join(', ')
                        }, 'Signing outgoing message with %s keys', dkim.keys.length);
                        return dkim.sign(input, mail.data._dkim);
                    });
                }

                this.transporter.send(mail, (...args) => {
                    if (args[0]) {
                        this.logger.error({
                            err: args[0],
                            tnx: 'transport',
                            action: 'send'
                        }, 'Send Error: %s', args[0].message);
                    }
                    callback(...args);
                });
            });
        });

        return promise;
    }

    getVersionString() {
        return util.format(
            '%s (%s; +%s; %s/%s)',
            packageData.name,
            packageData.version,
            packageData.homepage,
            this.transporter.name,
            this.transporter.version
        );
    }

    _processPlugins(step, mail, callback) {
        step = (step || '').toString();

        if (!this._userPlugins.hasOwnProperty(step)) {
            return callback();
        }

        let userPlugins = this._userPlugins[step] || [];
        let defaultPlugins = this._defaultPlugins[step] || [];

        if (userPlugins.length) {
            this.logger.debug({
                tnx: 'transaction',
                pluginCount: userPlugins.length,
                step
            }, 'Using %s plugins for %s', userPlugins.length, step);
        }

        if (userPlugins.length + defaultPlugins.length === 0) {
            return callback();
        }

        let pos = 0;
        let block = 'default';
        let processPlugins = () => {
            let curplugins = block === 'default' ? defaultPlugins : userPlugins;
            if (pos >= curplugins.length) {
                if (block === 'default' && userPlugins.length) {
                    block = 'user';
                    pos = 0;
                    curplugins = userPlugins;
                } else {
                    return callback();
                }
            }
            let plugin = curplugins[pos++];
            plugin(mail, err => {
                if (err) {
                    return callback(err);
                }
                processPlugins();
            });
        };

        processPlugins();
    }

    /**
     * Sets up proxy handler for a Nodemailer object
     *
     * @param {String} proxyUrl Proxy configuration url
     */
    setupProxy(proxyUrl) {
        let proxy = urllib.parse(proxyUrl);

        // setup socket handler for the mailer object
        this.getSocket = (options, callback) => {
            let protocol = proxy.protocol.replace(/:$/, '').toLowerCase();

            if (this.meta.has('proxy_handler_' + protocol)) {
                return this.meta.get('proxy_handler_' + protocol)(proxy, options, callback);
            }

            switch (protocol) {
                // Connect using a HTTP CONNECT method
                case 'http':
                case 'https':
                    httpProxyClient(proxy.href, options.port, options.host, (err, socket) => {
                        if (err) {
                            return callback(err);
                        }
                        return callback(null, {
                            connection: socket
                        });
                    });
                    return;
                case 'socks':
                case 'socks5':
                case 'socks4':
                case 'socks4a':
                    {
                        if (!this.meta.has('proxy_socks_module')) {
                            return callback(new Error('Socks module not loaded'));
                        }

                        let connect = ipaddress => {
                            this.meta.get('proxy_socks_module').createConnection({
                                proxy: {
                                    ipaddress,
                                    port: proxy.port,
                                    type: Number(proxy.protocol.replace(/\D/g, '')) || 5
                                },
                                target: {
                                    host: options.host,
                                    port: options.port
                                },
                                command: 'connect',
                                authentication: !proxy.auth ? false : {
                                    username: decodeURIComponent(proxy.auth.split(':').shift()),
                                    password: decodeURIComponent(proxy.auth.split(':').pop())
                                }
                            }, (err, socket) => {
                                if (err) {
                                    return callback(err);
                                }
                                return callback(null, {
                                    connection: socket
                                });
                            });
                        };

                        if (net.isIP(proxy.hostname)) {
                            return connect(proxy.hostname);
                        }

                        return dns.resolve(proxy.hostname, (err, address) => {
                            if (err) {
                                return callback(err);
                            }
                            connect(address);
                        });
                    }
            }
            callback(new Error('Unknown proxy configuration'));
        };
    }

    _convertDataImages(mail, callback) {
        if (!this.options.attachDataUrls && !mail.data.attachDataUrls || !mail.data.html) {
            return callback();
        }
        mail.resolveContent(mail.data, 'html', (err, html) => {
            if (err) {
                return callback(err);
            }
            let cidCounter = 0;
            html = (html || '').toString().replace(/(<img\b[^>]* src\s*=[\s"']*)(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
                let cid = crypto.randomBytes(10).toString('hex') + '@localhost';
                if (!mail.data.attachments) {
                    mail.data.attachments = [];
                }
                if (!Array.isArray(mail.data.attachments)) {
                    mail.data.attachments = [].concat(mail.data.attachments || []);
                }
                mail.data.attachments.push({
                    path: dataUri,
                    cid,
                    filename: 'image-' + (++cidCounter) + '.' + mimeTypes.detectExtension(mimeType)
                });
                return prefix + 'cid:' + cid;
            });
            mail.data.html = html;
            callback();
        });
    }

    set(key, value) {
        return this.meta.set(key, value);
    }

    get(key) {
        return this.meta.get(key);
    }
}

module.exports = Mail;