Index: node_modules/nodemailer/lib/fetch/cookies.js
===================================================================
--- node_modules/nodemailer/lib/fetch/cookies.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
+++ node_modules/nodemailer/lib/fetch/cookies.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
@@ -0,0 +1,281 @@
+'use strict';
+
+// module to handle cookies
+
+const urllib = require('url');
+
+const SESSION_TIMEOUT = 1800; // 30 min
+
+/**
+ * Creates a biskviit cookie jar for managing cookie values in memory
+ *
+ * @constructor
+ * @param {Object} [options] Optional options object
+ */
+class Cookies {
+    constructor(options) {
+        this.options = options || {};
+        this.cookies = [];
+    }
+
+    /**
+     * Stores a cookie string to the cookie storage
+     *
+     * @param {String} cookieStr Value from the 'Set-Cookie:' header
+     * @param {String} url Current URL
+     */
+    set(cookieStr, url) {
+        let urlparts = urllib.parse(url || '');
+        let cookie = this.parse(cookieStr);
+        let domain;
+
+        if (cookie.domain) {
+            domain = cookie.domain.replace(/^\./, '');
+
+            // do not allow cross origin cookies
+            if (
+                // can't be valid if the requested domain is shorter than current hostname
+                urlparts.hostname.length < domain.length ||
+                // prefix domains with dot to be sure that partial matches are not used
+                ('.' + urlparts.hostname).substr(-domain.length + 1) !== '.' + domain
+            ) {
+                cookie.domain = urlparts.hostname;
+            }
+        } else {
+            cookie.domain = urlparts.hostname;
+        }
+
+        if (!cookie.path) {
+            cookie.path = this.getPath(urlparts.pathname);
+        }
+
+        // if no expire date, then use sessionTimeout value
+        if (!cookie.expires) {
+            cookie.expires = new Date(Date.now() + (Number(this.options.sessionTimeout || SESSION_TIMEOUT) || SESSION_TIMEOUT) * 1000);
+        }
+
+        return this.add(cookie);
+    }
+
+    /**
+     * Returns cookie string for the 'Cookie:' header.
+     *
+     * @param {String} url URL to check for
+     * @returns {String} Cookie header or empty string if no matches were found
+     */
+    get(url) {
+        return this.list(url)
+            .map(cookie => cookie.name + '=' + cookie.value)
+            .join('; ');
+    }
+
+    /**
+     * Lists all valied cookie objects for the specified URL
+     *
+     * @param {String} url URL to check for
+     * @returns {Array} An array of cookie objects
+     */
+    list(url) {
+        let result = [];
+        let i;
+        let cookie;
+
+        for (i = this.cookies.length - 1; i >= 0; i--) {
+            cookie = this.cookies[i];
+
+            if (this.isExpired(cookie)) {
+                this.cookies.splice(i, i);
+                continue;
+            }
+
+            if (this.match(cookie, url)) {
+                result.unshift(cookie);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Parses cookie string from the 'Set-Cookie:' header
+     *
+     * @param {String} cookieStr String from the 'Set-Cookie:' header
+     * @returns {Object} Cookie object
+     */
+    parse(cookieStr) {
+        let cookie = {};
+
+        (cookieStr || '')
+            .toString()
+            .split(';')
+            .forEach(cookiePart => {
+                let valueParts = cookiePart.split('=');
+                let key = valueParts.shift().trim().toLowerCase();
+                let value = valueParts.join('=').trim();
+                let domain;
+
+                if (!key) {
+                    // skip empty parts
+                    return;
+                }
+
+                switch (key) {
+                    case 'expires':
+                        value = new Date(value);
+                        // ignore date if can not parse it
+                        if (value.toString() !== 'Invalid Date') {
+                            cookie.expires = value;
+                        }
+                        break;
+
+                    case 'path':
+                        cookie.path = value;
+                        break;
+
+                    case 'domain':
+                        domain = value.toLowerCase();
+                        if (domain.length && domain.charAt(0) !== '.') {
+                            domain = '.' + domain; // ensure preceeding dot for user set domains
+                        }
+                        cookie.domain = domain;
+                        break;
+
+                    case 'max-age':
+                        cookie.expires = new Date(Date.now() + (Number(value) || 0) * 1000);
+                        break;
+
+                    case 'secure':
+                        cookie.secure = true;
+                        break;
+
+                    case 'httponly':
+                        cookie.httponly = true;
+                        break;
+
+                    default:
+                        if (!cookie.name) {
+                            cookie.name = key;
+                            cookie.value = value;
+                        }
+                }
+            });
+
+        return cookie;
+    }
+
+    /**
+     * Checks if a cookie object is valid for a specified URL
+     *
+     * @param {Object} cookie Cookie object
+     * @param {String} url URL to check for
+     * @returns {Boolean} true if cookie is valid for specifiec URL
+     */
+    match(cookie, url) {
+        let urlparts = urllib.parse(url || '');
+
+        // check if hostname matches
+        // .foo.com also matches subdomains, foo.com does not
+        if (
+            urlparts.hostname !== cookie.domain &&
+            (cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)
+        ) {
+            return false;
+        }
+
+        // check if path matches
+        let path = this.getPath(urlparts.pathname);
+        if (path.substr(0, cookie.path.length) !== cookie.path) {
+            return false;
+        }
+
+        // check secure argument
+        if (cookie.secure && urlparts.protocol !== 'https:') {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Adds (or updates/removes if needed) a cookie object to the cookie storage
+     *
+     * @param {Object} cookie Cookie value to be stored
+     */
+    add(cookie) {
+        let i;
+        let len;
+
+        // nothing to do here
+        if (!cookie || !cookie.name) {
+            return false;
+        }
+
+        // overwrite if has same params
+        for (i = 0, len = this.cookies.length; i < len; i++) {
+            if (this.compare(this.cookies[i], cookie)) {
+                // check if the cookie needs to be removed instead
+                if (this.isExpired(cookie)) {
+                    this.cookies.splice(i, 1); // remove expired/unset cookie
+                    return false;
+                }
+
+                this.cookies[i] = cookie;
+                return true;
+            }
+        }
+
+        // add as new if not already expired
+        if (!this.isExpired(cookie)) {
+            this.cookies.push(cookie);
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks if two cookie objects are the same
+     *
+     * @param {Object} a Cookie to check against
+     * @param {Object} b Cookie to check against
+     * @returns {Boolean} True, if the cookies are the same
+     */
+    compare(a, b) {
+        return a.name === b.name && a.path === b.path && a.domain === b.domain && a.secure === b.secure && a.httponly === a.httponly;
+    }
+
+    /**
+     * Checks if a cookie is expired
+     *
+     * @param {Object} cookie Cookie object to check against
+     * @returns {Boolean} True, if the cookie is expired
+     */
+    isExpired(cookie) {
+        return (cookie.expires && cookie.expires < new Date()) || !cookie.value;
+    }
+
+    /**
+     * Returns normalized cookie path for an URL path argument
+     *
+     * @param {String} pathname
+     * @returns {String} Normalized path
+     */
+    getPath(pathname) {
+        let path = (pathname || '/').split('/');
+        path.pop(); // remove filename part
+        path = path.join('/').trim();
+
+        // ensure path prefix /
+        if (path.charAt(0) !== '/') {
+            path = '/' + path;
+        }
+
+        // ensure path suffix /
+        if (path.substr(-1) !== '/') {
+            path += '/';
+        }
+
+        return path;
+    }
+}
+
+module.exports = Cookies;
Index: node_modules/nodemailer/lib/fetch/index.js
===================================================================
--- node_modules/nodemailer/lib/fetch/index.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
+++ node_modules/nodemailer/lib/fetch/index.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
@@ -0,0 +1,274 @@
+'use strict';
+
+const http = require('http');
+const https = require('https');
+const urllib = require('url');
+const zlib = require('zlib');
+const PassThrough = require('stream').PassThrough;
+const Cookies = require('./cookies');
+const packageData = require('../../package.json');
+const net = require('net');
+
+const MAX_REDIRECTS = 5;
+
+module.exports = function (url, options) {
+    return nmfetch(url, options);
+};
+
+module.exports.Cookies = Cookies;
+
+function nmfetch(url, options) {
+    options = options || {};
+
+    options.fetchRes = options.fetchRes || new PassThrough();
+    options.cookies = options.cookies || new Cookies();
+    options.redirects = options.redirects || 0;
+    options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
+
+    if (options.cookie) {
+        [].concat(options.cookie || []).forEach(cookie => {
+            options.cookies.set(cookie, url);
+        });
+        options.cookie = false;
+    }
+
+    let fetchRes = options.fetchRes;
+    let parsed = urllib.parse(url);
+    let method = (options.method || '').toString().trim().toUpperCase() || 'GET';
+    let finished = false;
+    let cookies;
+    let body;
+
+    let handler = parsed.protocol === 'https:' ? https : http;
+
+    let headers = {
+        'accept-encoding': 'gzip,deflate',
+        'user-agent': 'nodemailer/' + packageData.version
+    };
+
+    Object.keys(options.headers || {}).forEach(key => {
+        headers[key.toLowerCase().trim()] = options.headers[key];
+    });
+
+    if (options.userAgent) {
+        headers['user-agent'] = options.userAgent;
+    }
+
+    if (parsed.auth) {
+        headers.Authorization = 'Basic ' + Buffer.from(parsed.auth).toString('base64');
+    }
+
+    if ((cookies = options.cookies.get(url))) {
+        headers.cookie = cookies;
+    }
+
+    if (options.body) {
+        if (options.contentType !== false) {
+            headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
+        }
+
+        if (typeof options.body.pipe === 'function') {
+            // it's a stream
+            headers['Transfer-Encoding'] = 'chunked';
+            body = options.body;
+            body.on('error', err => {
+                if (finished) {
+                    return;
+                }
+                finished = true;
+                err.type = 'FETCH';
+                err.sourceUrl = url;
+                fetchRes.emit('error', err);
+            });
+        } else {
+            if (options.body instanceof Buffer) {
+                body = options.body;
+            } else if (typeof options.body === 'object') {
+                try {
+                    // encodeURIComponent can fail on invalid input (partial emoji etc.)
+                    body = Buffer.from(
+                        Object.keys(options.body)
+                            .map(key => {
+                                let value = options.body[key].toString().trim();
+                                return encodeURIComponent(key) + '=' + encodeURIComponent(value);
+                            })
+                            .join('&')
+                    );
+                } catch (E) {
+                    if (finished) {
+                        return;
+                    }
+                    finished = true;
+                    E.type = 'FETCH';
+                    E.sourceUrl = url;
+                    fetchRes.emit('error', E);
+                    return;
+                }
+            } else {
+                body = Buffer.from(options.body.toString().trim());
+            }
+
+            headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
+            headers['Content-Length'] = body.length;
+        }
+        // if method is not provided, use POST instead of GET
+        method = (options.method || '').toString().trim().toUpperCase() || 'POST';
+    }
+
+    let req;
+    let reqOptions = {
+        method,
+        host: parsed.hostname,
+        path: parsed.path,
+        port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80,
+        headers,
+        rejectUnauthorized: false,
+        agent: false
+    };
+
+    if (options.tls) {
+        Object.keys(options.tls).forEach(key => {
+            reqOptions[key] = options.tls[key];
+        });
+    }
+
+    if (parsed.protocol === 'https:' && parsed.hostname && parsed.hostname !== reqOptions.host && !net.isIP(parsed.hostname) && !reqOptions.servername) {
+        reqOptions.servername = parsed.hostname;
+    }
+
+    try {
+        req = handler.request(reqOptions);
+    } catch (E) {
+        finished = true;
+        setImmediate(() => {
+            E.type = 'FETCH';
+            E.sourceUrl = url;
+            fetchRes.emit('error', E);
+        });
+        return fetchRes;
+    }
+
+    if (options.timeout) {
+        req.setTimeout(options.timeout, () => {
+            if (finished) {
+                return;
+            }
+            finished = true;
+            req.abort();
+            let err = new Error('Request Timeout');
+            err.type = 'FETCH';
+            err.sourceUrl = url;
+            fetchRes.emit('error', err);
+        });
+    }
+
+    req.on('error', err => {
+        if (finished) {
+            return;
+        }
+        finished = true;
+        err.type = 'FETCH';
+        err.sourceUrl = url;
+        fetchRes.emit('error', err);
+    });
+
+    req.on('response', res => {
+        let inflate;
+
+        if (finished) {
+            return;
+        }
+
+        switch (res.headers['content-encoding']) {
+            case 'gzip':
+            case 'deflate':
+                inflate = zlib.createUnzip();
+                break;
+        }
+
+        if (res.headers['set-cookie']) {
+            [].concat(res.headers['set-cookie'] || []).forEach(cookie => {
+                options.cookies.set(cookie, url);
+            });
+        }
+
+        if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
+            // redirect
+            options.redirects++;
+            if (options.redirects > options.maxRedirects) {
+                finished = true;
+                let err = new Error('Maximum redirect count exceeded');
+                err.type = 'FETCH';
+                err.sourceUrl = url;
+                fetchRes.emit('error', err);
+                req.abort();
+                return;
+            }
+            // redirect does not include POST body
+            options.method = 'GET';
+            options.body = false;
+            return nmfetch(urllib.resolve(url, res.headers.location), options);
+        }
+
+        fetchRes.statusCode = res.statusCode;
+        fetchRes.headers = res.headers;
+
+        if (res.statusCode >= 300 && !options.allowErrorResponse) {
+            finished = true;
+            let err = new Error('Invalid status code ' + res.statusCode);
+            err.type = 'FETCH';
+            err.sourceUrl = url;
+            fetchRes.emit('error', err);
+            req.abort();
+            return;
+        }
+
+        res.on('error', err => {
+            if (finished) {
+                return;
+            }
+            finished = true;
+            err.type = 'FETCH';
+            err.sourceUrl = url;
+            fetchRes.emit('error', err);
+            req.abort();
+        });
+
+        if (inflate) {
+            res.pipe(inflate).pipe(fetchRes);
+            inflate.on('error', err => {
+                if (finished) {
+                    return;
+                }
+                finished = true;
+                err.type = 'FETCH';
+                err.sourceUrl = url;
+                fetchRes.emit('error', err);
+                req.abort();
+            });
+        } else {
+            res.pipe(fetchRes);
+        }
+    });
+
+    setImmediate(() => {
+        if (body) {
+            try {
+                if (typeof body.pipe === 'function') {
+                    return body.pipe(req);
+                } else {
+                    req.write(body);
+                }
+            } catch (err) {
+                finished = true;
+                err.type = 'FETCH';
+                err.sourceUrl = url;
+                fetchRes.emit('error', err);
+                return;
+            }
+        }
+        req.end();
+    });
+
+    return fetchRes;
+}
