Index: node_modules/nodemailer/lib/mailer/index.js
===================================================================
--- node_modules/nodemailer/lib/mailer/index.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
+++ node_modules/nodemailer/lib/mailer/index.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
@@ -0,0 +1,429 @@
+'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 this.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') {
+                    if (method === 'verify' && typeof this.getSocket === 'function') {
+                        this.transporter.getSocket = this.getSocket;
+                        this.getSocket = false;
+                    }
+                    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);
+        }
+
+        return this;
+    }
+
+    /**
+     * 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 = null) {
+        let promise;
+
+        if (!callback) {
+            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 => {
+                        let proxyV2 = !!this.meta.get('proxy_socks_module').SocksClient;
+                        let socksClient = proxyV2 ? this.meta.get('proxy_socks_module').SocksClient : this.meta.get('proxy_socks_module');
+                        let proxyType = Number(proxy.protocol.replace(/\D/g, '')) || 5;
+                        let connectionOpts = {
+                            proxy: {
+                                ipaddress,
+                                port: Number(proxy.port),
+                                type: proxyType
+                            },
+                            [proxyV2 ? 'destination' : 'target']: {
+                                host: options.host,
+                                port: options.port
+                            },
+                            command: 'connect'
+                        };
+
+                        if (proxy.auth) {
+                            let username = decodeURIComponent(proxy.auth.split(':').shift());
+                            let password = decodeURIComponent(proxy.auth.split(':').pop());
+                            if (proxyV2) {
+                                connectionOpts.proxy.userId = username;
+                                connectionOpts.proxy.password = password;
+                            } else if (proxyType === 4) {
+                                connectionOpts.userid = username;
+                            } else {
+                                connectionOpts.authentication = {
+                                    username,
+                                    password
+                                };
+                            }
+                        }
+
+                        socksClient.createConnection(connectionOpts, (err, info) => {
+                            if (err) {
+                                return callback(err);
+                            }
+                            return callback(null, {
+                                connection: info.socket || info
+                            });
+                        });
+                    };
+
+                    if (net.isIP(proxy.hostname)) {
+                        return connect(proxy.hostname);
+                    }
+
+                    return dns.resolve(proxy.hostname, (err, address) => {
+                        if (err) {
+                            return callback(err);
+                        }
+                        connect(Array.isArray(address) ? address[0] : 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[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(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;
Index: node_modules/nodemailer/lib/mailer/mail-message.js
===================================================================
--- node_modules/nodemailer/lib/mailer/mail-message.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
+++ node_modules/nodemailer/lib/mailer/mail-message.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
@@ -0,0 +1,315 @@
+'use strict';
+
+const shared = require('../shared');
+const MimeNode = require('../mime-node');
+const mimeFuncs = require('../mime-funcs');
+
+class MailMessage {
+    constructor(mailer, data) {
+        this.mailer = mailer;
+        this.data = {};
+        this.message = null;
+
+        data = data || {};
+        let options = mailer.options || {};
+        let defaults = mailer._defaults || {};
+
+        Object.keys(data).forEach(key => {
+            this.data[key] = data[key];
+        });
+
+        this.data.headers = this.data.headers || {};
+
+        // apply defaults
+        Object.keys(defaults).forEach(key => {
+            if (!(key in this.data)) {
+                this.data[key] = defaults[key];
+            } else if (key === 'headers') {
+                // headers is a special case. Allow setting individual default headers
+                Object.keys(defaults.headers).forEach(key => {
+                    if (!(key in this.data.headers)) {
+                        this.data.headers[key] = defaults.headers[key];
+                    }
+                });
+            }
+        });
+
+        // force specific keys from transporter options
+        ['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey'].forEach(key => {
+            if (key in options) {
+                this.data[key] = options[key];
+            }
+        });
+    }
+
+    resolveContent(...args) {
+        return shared.resolveContent(...args);
+    }
+
+    resolveAll(callback) {
+        let keys = [
+            [this.data, 'html'],
+            [this.data, 'text'],
+            [this.data, 'watchHtml'],
+            [this.data, 'amp'],
+            [this.data, 'icalEvent']
+        ];
+
+        if (this.data.alternatives && this.data.alternatives.length) {
+            this.data.alternatives.forEach((alternative, i) => {
+                keys.push([this.data.alternatives, i]);
+            });
+        }
+
+        if (this.data.attachments && this.data.attachments.length) {
+            this.data.attachments.forEach((attachment, i) => {
+                if (!attachment.filename) {
+                    attachment.filename = (attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
+                    if (attachment.filename.indexOf('.') < 0) {
+                        attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
+                    }
+                }
+
+                if (!attachment.contentType) {
+                    attachment.contentType = mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
+                }
+
+                keys.push([this.data.attachments, i]);
+            });
+        }
+
+        let mimeNode = new MimeNode();
+
+        let addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo'];
+
+        addressKeys.forEach(address => {
+            let value;
+            if (this.message) {
+                value = [].concat(mimeNode._parseAddresses(this.message.getHeader(address === 'replyTo' ? 'reply-to' : address)) || []);
+            } else if (this.data[address]) {
+                value = [].concat(mimeNode._parseAddresses(this.data[address]) || []);
+            }
+            if (value && value.length) {
+                this.data[address] = value;
+            } else if (address in this.data) {
+                this.data[address] = null;
+            }
+        });
+
+        let singleKeys = ['from', 'sender'];
+        singleKeys.forEach(address => {
+            if (this.data[address]) {
+                this.data[address] = this.data[address].shift();
+            }
+        });
+
+        let pos = 0;
+        let resolveNext = () => {
+            if (pos >= keys.length) {
+                return callback(null, this.data);
+            }
+            let args = keys[pos++];
+            if (!args[0] || !args[0][args[1]]) {
+                return resolveNext();
+            }
+            shared.resolveContent(...args, (err, value) => {
+                if (err) {
+                    return callback(err);
+                }
+
+                let node = {
+                    content: value
+                };
+                if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
+                    Object.keys(args[0][args[1]]).forEach(key => {
+                        if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) {
+                            node[key] = args[0][args[1]][key];
+                        }
+                    });
+                }
+
+                args[0][args[1]] = node;
+                resolveNext();
+            });
+        };
+
+        setImmediate(() => resolveNext());
+    }
+
+    normalize(callback) {
+        let envelope = this.data.envelope || this.message.getEnvelope();
+        let messageId = this.message.messageId();
+
+        this.resolveAll((err, data) => {
+            if (err) {
+                return callback(err);
+            }
+
+            data.envelope = envelope;
+            data.messageId = messageId;
+
+            ['html', 'text', 'watchHtml', 'amp'].forEach(key => {
+                if (data[key] && data[key].content) {
+                    if (typeof data[key].content === 'string') {
+                        data[key] = data[key].content;
+                    } else if (Buffer.isBuffer(data[key].content)) {
+                        data[key] = data[key].content.toString();
+                    }
+                }
+            });
+
+            if (data.icalEvent && Buffer.isBuffer(data.icalEvent.content)) {
+                data.icalEvent.content = data.icalEvent.content.toString('base64');
+                data.icalEvent.encoding = 'base64';
+            }
+
+            if (data.alternatives && data.alternatives.length) {
+                data.alternatives.forEach(alternative => {
+                    if (alternative && alternative.content && Buffer.isBuffer(alternative.content)) {
+                        alternative.content = alternative.content.toString('base64');
+                        alternative.encoding = 'base64';
+                    }
+                });
+            }
+
+            if (data.attachments && data.attachments.length) {
+                data.attachments.forEach(attachment => {
+                    if (attachment && attachment.content && Buffer.isBuffer(attachment.content)) {
+                        attachment.content = attachment.content.toString('base64');
+                        attachment.encoding = 'base64';
+                    }
+                });
+            }
+
+            data.normalizedHeaders = {};
+            Object.keys(data.headers || {}).forEach(key => {
+                let value = [].concat(data.headers[key] || []).shift();
+                value = (value && value.value) || value;
+                if (value) {
+                    if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
+                        value = this.message._encodeHeaderValue(key, value);
+                    }
+                    data.normalizedHeaders[key] = value;
+                }
+            });
+
+            if (data.list && typeof data.list === 'object') {
+                let listHeaders = this._getListHeaders(data.list);
+                listHeaders.forEach(entry => {
+                    data.normalizedHeaders[entry.key] = entry.value.map(val => (val && val.value) || val).join(', ');
+                });
+            }
+
+            if (data.references) {
+                data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references);
+            }
+
+            if (data.inReplyTo) {
+                data.normalizedHeaders['in-reply-to'] = this.message._encodeHeaderValue('in-reply-to', data.inReplyTo);
+            }
+
+            return callback(null, data);
+        });
+    }
+
+    setMailerHeader() {
+        if (!this.message || !this.data.xMailer) {
+            return;
+        }
+        this.message.setHeader('X-Mailer', this.data.xMailer);
+    }
+
+    setPriorityHeaders() {
+        if (!this.message || !this.data.priority) {
+            return;
+        }
+        switch ((this.data.priority || '').toString().toLowerCase()) {
+            case 'high':
+                this.message.setHeader('X-Priority', '1 (Highest)');
+                this.message.setHeader('X-MSMail-Priority', 'High');
+                this.message.setHeader('Importance', 'High');
+                break;
+            case 'low':
+                this.message.setHeader('X-Priority', '5 (Lowest)');
+                this.message.setHeader('X-MSMail-Priority', 'Low');
+                this.message.setHeader('Importance', 'Low');
+                break;
+            default:
+            // do not add anything, since all messages are 'Normal' by default
+        }
+    }
+
+    setListHeaders() {
+        if (!this.message || !this.data.list || typeof this.data.list !== 'object') {
+            return;
+        }
+        // add optional List-* headers
+        if (this.data.list && typeof this.data.list === 'object') {
+            this._getListHeaders(this.data.list).forEach(listHeader => {
+                listHeader.value.forEach(value => {
+                    this.message.addHeader(listHeader.key, value);
+                });
+            });
+        }
+    }
+
+    _getListHeaders(listData) {
+        // make sure an url looks like <protocol:url>
+        return Object.keys(listData).map(key => ({
+            key: 'list-' + key.toLowerCase().trim(),
+            value: [].concat(listData[key] || []).map(value => ({
+                prepared: true,
+                foldLines: true,
+                value: []
+                    .concat(value || [])
+                    .map(value => {
+                        if (typeof value === 'string') {
+                            value = {
+                                url: value
+                            };
+                        }
+
+                        if (value && value.url) {
+                            if (key.toLowerCase().trim() === 'id') {
+                                // List-ID: "comment" <domain>
+                                let comment = value.comment || '';
+                                if (mimeFuncs.isPlainText(comment)) {
+                                    comment = '"' + comment + '"';
+                                } else {
+                                    comment = mimeFuncs.encodeWord(comment);
+                                }
+
+                                return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+\/{,2}/, '');
+                            }
+
+                            // List-*: <http://domain> (comment)
+                            let comment = value.comment || '';
+                            if (!mimeFuncs.isPlainText(comment)) {
+                                comment = mimeFuncs.encodeWord(comment);
+                            }
+
+                            return this._formatListUrl(value.url) + (value.comment ? ' (' + comment + ')' : '');
+                        }
+
+                        return '';
+                    })
+                    .filter(value => value)
+                    .join(', ')
+            }))
+        }));
+    }
+
+    _formatListUrl(url) {
+        url = url.replace(/[\s<]+|[\s>]+/g, '');
+        if (/^(https?|mailto|ftp):/.test(url)) {
+            return '<' + url + '>';
+        }
+        if (/^[^@]+@[^@]+$/.test(url)) {
+            return '<mailto:' + url + '>';
+        }
+
+        return '<http://' + url + '>';
+    }
+}
+
+module.exports = MailMessage;
