Index: node_modules/nodemailer/lib/mime-node/index.js
===================================================================
--- node_modules/nodemailer/lib/mime-node/index.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
+++ node_modules/nodemailer/lib/mime-node/index.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
@@ -0,0 +1,1314 @@
+/* eslint no-undefined: 0, prefer-spread: 0, no-control-regex: 0 */
+
+'use strict';
+
+const crypto = require('crypto');
+const fs = require('fs');
+const punycode = require('../punycode');
+const PassThrough = require('stream').PassThrough;
+const shared = require('../shared');
+
+const mimeFuncs = require('../mime-funcs');
+const qp = require('../qp');
+const base64 = require('../base64');
+const addressparser = require('../addressparser');
+const nmfetch = require('../fetch');
+const LastNewline = require('./last-newline');
+
+const LeWindows = require('./le-windows');
+const LeUnix = require('./le-unix');
+
+/**
+ * Creates a new mime tree node. Assumes 'multipart/*' as the content type
+ * if it is a branch, anything else counts as leaf. If rootNode is missing from
+ * the options, assumes this is the root.
+ *
+ * @param {String} contentType Define the content type for the node. Can be left blank for attachments (derived from filename)
+ * @param {Object} [options] optional options
+ * @param {Object} [options.rootNode] root node for this tree
+ * @param {Object} [options.parentNode] immediate parent for this node
+ * @param {Object} [options.filename] filename for an attachment node
+ * @param {String} [options.baseBoundary] shared part of the unique multipart boundary
+ * @param {Boolean} [options.keepBcc] If true, do not exclude Bcc from the generated headers
+ * @param {Function} [options.normalizeHeaderKey] method to normalize header keys for custom caseing
+ * @param {String} [options.textEncoding] either 'Q' (the default) or 'B'
+ */
+class MimeNode {
+    constructor(contentType, options) {
+        this.nodeCounter = 0;
+
+        options = options || {};
+
+        /**
+         * shared part of the unique multipart boundary
+         */
+        this.baseBoundary = options.baseBoundary || crypto.randomBytes(8).toString('hex');
+        this.boundaryPrefix = options.boundaryPrefix || '--_NmP';
+
+        this.disableFileAccess = !!options.disableFileAccess;
+        this.disableUrlAccess = !!options.disableUrlAccess;
+
+        this.normalizeHeaderKey = options.normalizeHeaderKey;
+
+        /**
+         * If date headers is missing and current node is the root, this value is used instead
+         */
+        this.date = new Date();
+
+        /**
+         * Root node for current mime tree
+         */
+        this.rootNode = options.rootNode || this;
+
+        /**
+         * If true include Bcc in generated headers (if available)
+         */
+        this.keepBcc = !!options.keepBcc;
+
+        /**
+         * If filename is specified but contentType is not (probably an attachment)
+         * detect the content type from filename extension
+         */
+        if (options.filename) {
+            /**
+             * Filename for this node. Useful with attachments
+             */
+            this.filename = options.filename;
+            if (!contentType) {
+                contentType = mimeFuncs.detectMimeType(this.filename.split('.').pop());
+            }
+        }
+
+        /**
+         * Indicates which encoding should be used for header strings: "Q" or "B"
+         */
+        this.textEncoding = (options.textEncoding || '').toString().trim().charAt(0).toUpperCase();
+
+        /**
+         * Immediate parent for this node (or undefined if not set)
+         */
+        this.parentNode = options.parentNode;
+
+        /**
+         * Hostname for default message-id values
+         */
+        this.hostname = options.hostname;
+
+        /**
+         * If set to 'win' then uses \r\n, if 'linux' then \n. If not set (or `raw` is used) then newlines are kept as is.
+         */
+        this.newline = options.newline;
+
+        /**
+         * An array for possible child nodes
+         */
+        this.childNodes = [];
+
+        /**
+         * Used for generating unique boundaries (prepended to the shared base)
+         */
+        this._nodeId = ++this.rootNode.nodeCounter;
+
+        /**
+         * A list of header values for this node in the form of [{key:'', value:''}]
+         */
+        this._headers = [];
+
+        /**
+         * True if the content only uses ASCII printable characters
+         * @type {Boolean}
+         */
+        this._isPlainText = false;
+
+        /**
+         * True if the content is plain text but has longer lines than allowed
+         * @type {Boolean}
+         */
+        this._hasLongLines = false;
+
+        /**
+         * If set, use instead this value for envelopes instead of generating one
+         * @type {Boolean}
+         */
+        this._envelope = false;
+
+        /**
+         * If set then use this value as the stream content instead of building it
+         * @type {String|Buffer|Stream}
+         */
+        this._raw = false;
+
+        /**
+         * Additional transform streams that the message will be piped before
+         * exposing by createReadStream
+         * @type {Array}
+         */
+        this._transforms = [];
+
+        /**
+         * Additional process functions that the message will be piped through before
+         * exposing by createReadStream. These functions are run after transforms
+         * @type {Array}
+         */
+        this._processFuncs = [];
+
+        /**
+         * If content type is set (or derived from the filename) add it to headers
+         */
+        if (contentType) {
+            this.setHeader('Content-Type', contentType);
+        }
+    }
+
+    /////// PUBLIC METHODS
+
+    /**
+     * Creates and appends a child node.Arguments provided are passed to MimeNode constructor
+     *
+     * @param {String} [contentType] Optional content type
+     * @param {Object} [options] Optional options object
+     * @return {Object} Created node object
+     */
+    createChild(contentType, options) {
+        if (!options && typeof contentType === 'object') {
+            options = contentType;
+            contentType = undefined;
+        }
+        let node = new MimeNode(contentType, options);
+        this.appendChild(node);
+        return node;
+    }
+
+    /**
+     * Appends an existing node to the mime tree. Removes the node from an existing
+     * tree if needed
+     *
+     * @param {Object} childNode node to be appended
+     * @return {Object} Appended node object
+     */
+    appendChild(childNode) {
+        if (childNode.rootNode !== this.rootNode) {
+            childNode.rootNode = this.rootNode;
+            childNode._nodeId = ++this.rootNode.nodeCounter;
+        }
+
+        childNode.parentNode = this;
+
+        this.childNodes.push(childNode);
+        return childNode;
+    }
+
+    /**
+     * Replaces current node with another node
+     *
+     * @param {Object} node Replacement node
+     * @return {Object} Replacement node
+     */
+    replace(node) {
+        if (node === this) {
+            return this;
+        }
+
+        this.parentNode.childNodes.forEach((childNode, i) => {
+            if (childNode === this) {
+                node.rootNode = this.rootNode;
+                node.parentNode = this.parentNode;
+                node._nodeId = this._nodeId;
+
+                this.rootNode = this;
+                this.parentNode = undefined;
+
+                node.parentNode.childNodes[i] = node;
+            }
+        });
+
+        return node;
+    }
+
+    /**
+     * Removes current node from the mime tree
+     *
+     * @return {Object} removed node
+     */
+    remove() {
+        if (!this.parentNode) {
+            return this;
+        }
+
+        for (let i = this.parentNode.childNodes.length - 1; i >= 0; i--) {
+            if (this.parentNode.childNodes[i] === this) {
+                this.parentNode.childNodes.splice(i, 1);
+                this.parentNode = undefined;
+                this.rootNode = this;
+                return this;
+            }
+        }
+    }
+
+    /**
+     * Sets a header value. If the value for selected key exists, it is overwritten.
+     * You can set multiple values as well by using [{key:'', value:''}] or
+     * {key: 'value'} as the first argument.
+     *
+     * @param {String|Array|Object} key Header key or a list of key value pairs
+     * @param {String} value Header value
+     * @return {Object} current node
+     */
+    setHeader(key, value) {
+        let added = false,
+            headerValue;
+
+        // Allow setting multiple headers at once
+        if (!value && key && typeof key === 'object') {
+            // allow {key:'content-type', value: 'text/plain'}
+            if (key.key && 'value' in key) {
+                this.setHeader(key.key, key.value);
+            } else if (Array.isArray(key)) {
+                // allow [{key:'content-type', value: 'text/plain'}]
+                key.forEach(i => {
+                    this.setHeader(i.key, i.value);
+                });
+            } else {
+                // allow {'content-type': 'text/plain'}
+                Object.keys(key).forEach(i => {
+                    this.setHeader(i, key[i]);
+                });
+            }
+            return this;
+        }
+
+        key = this._normalizeHeaderKey(key);
+
+        headerValue = {
+            key,
+            value
+        };
+
+        // Check if the value exists and overwrite
+        for (let i = 0, len = this._headers.length; i < len; i++) {
+            if (this._headers[i].key === key) {
+                if (!added) {
+                    // replace the first match
+                    this._headers[i] = headerValue;
+                    added = true;
+                } else {
+                    // remove following matches
+                    this._headers.splice(i, 1);
+                    i--;
+                    len--;
+                }
+            }
+        }
+
+        // match not found, append the value
+        if (!added) {
+            this._headers.push(headerValue);
+        }
+
+        return this;
+    }
+
+    /**
+     * Adds a header value. If the value for selected key exists, the value is appended
+     * as a new field and old one is not touched.
+     * You can set multiple values as well by using [{key:'', value:''}] or
+     * {key: 'value'} as the first argument.
+     *
+     * @param {String|Array|Object} key Header key or a list of key value pairs
+     * @param {String} value Header value
+     * @return {Object} current node
+     */
+    addHeader(key, value) {
+        // Allow setting multiple headers at once
+        if (!value && key && typeof key === 'object') {
+            // allow {key:'content-type', value: 'text/plain'}
+            if (key.key && key.value) {
+                this.addHeader(key.key, key.value);
+            } else if (Array.isArray(key)) {
+                // allow [{key:'content-type', value: 'text/plain'}]
+                key.forEach(i => {
+                    this.addHeader(i.key, i.value);
+                });
+            } else {
+                // allow {'content-type': 'text/plain'}
+                Object.keys(key).forEach(i => {
+                    this.addHeader(i, key[i]);
+                });
+            }
+            return this;
+        } else if (Array.isArray(value)) {
+            value.forEach(val => {
+                this.addHeader(key, val);
+            });
+            return this;
+        }
+
+        this._headers.push({
+            key: this._normalizeHeaderKey(key),
+            value
+        });
+
+        return this;
+    }
+
+    /**
+     * Retrieves the first mathcing value of a selected key
+     *
+     * @param {String} key Key to search for
+     * @retun {String} Value for the key
+     */
+    getHeader(key) {
+        key = this._normalizeHeaderKey(key);
+        for (let i = 0, len = this._headers.length; i < len; i++) {
+            if (this._headers[i].key === key) {
+                return this._headers[i].value;
+            }
+        }
+    }
+
+    /**
+     * Sets body content for current node. If the value is a string, charset is added automatically
+     * to Content-Type (if it is text/*). If the value is a Buffer, you need to specify
+     * the charset yourself
+     *
+     * @param (String|Buffer) content Body content
+     * @return {Object} current node
+     */
+    setContent(content) {
+        this.content = content;
+        if (typeof this.content.pipe === 'function') {
+            // pre-stream handler. might be triggered if a stream is set as content
+            // and 'error' fires before anything is done with this stream
+            this._contentErrorHandler = err => {
+                this.content.removeListener('error', this._contentErrorHandler);
+                this.content = err;
+            };
+            this.content.once('error', this._contentErrorHandler);
+        } else if (typeof this.content === 'string') {
+            this._isPlainText = mimeFuncs.isPlainText(this.content);
+            if (this._isPlainText && mimeFuncs.hasLongerLines(this.content, 76)) {
+                // If there are lines longer than 76 symbols/bytes do not use 7bit
+                this._hasLongLines = true;
+            }
+        }
+        return this;
+    }
+
+    build(callback) {
+        let promise;
+
+        if (!callback) {
+            promise = new Promise((resolve, reject) => {
+                callback = shared.callbackPromise(resolve, reject);
+            });
+        }
+
+        let stream = this.createReadStream();
+        let buf = [];
+        let buflen = 0;
+        let returned = false;
+
+        stream.on('readable', () => {
+            let chunk;
+
+            while ((chunk = stream.read()) !== null) {
+                buf.push(chunk);
+                buflen += chunk.length;
+            }
+        });
+
+        stream.once('error', err => {
+            if (returned) {
+                return;
+            }
+            returned = true;
+
+            return callback(err);
+        });
+
+        stream.once('end', chunk => {
+            if (returned) {
+                return;
+            }
+            returned = true;
+
+            if (chunk && chunk.length) {
+                buf.push(chunk);
+                buflen += chunk.length;
+            }
+            return callback(null, Buffer.concat(buf, buflen));
+        });
+
+        return promise;
+    }
+
+    getTransferEncoding() {
+        let transferEncoding = false;
+        let contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim();
+
+        if (this.content) {
+            transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim();
+            if (!transferEncoding || !['base64', 'quoted-printable'].includes(transferEncoding)) {
+                if (/^text\//i.test(contentType)) {
+                    // If there are no special symbols, no need to modify the text
+                    if (this._isPlainText && !this._hasLongLines) {
+                        transferEncoding = '7bit';
+                    } else if (typeof this.content === 'string' || this.content instanceof Buffer) {
+                        // detect preferred encoding for string value
+                        transferEncoding = this._getTextEncoding(this.content) === 'Q' ? 'quoted-printable' : 'base64';
+                    } else {
+                        // we can not check content for a stream, so either use preferred encoding or fallback to QP
+                        transferEncoding = this.textEncoding === 'B' ? 'base64' : 'quoted-printable';
+                    }
+                } else if (!/^(multipart|message)\//i.test(contentType)) {
+                    transferEncoding = transferEncoding || 'base64';
+                }
+            }
+        }
+        return transferEncoding;
+    }
+
+    /**
+     * Builds the header block for the mime node. Append \r\n\r\n before writing the content
+     *
+     * @returns {String} Headers
+     */
+    buildHeaders() {
+        let transferEncoding = this.getTransferEncoding();
+        let headers = [];
+
+        if (transferEncoding) {
+            this.setHeader('Content-Transfer-Encoding', transferEncoding);
+        }
+
+        if (this.filename && !this.getHeader('Content-Disposition')) {
+            this.setHeader('Content-Disposition', 'attachment');
+        }
+
+        // Ensure mandatory header fields
+        if (this.rootNode === this) {
+            if (!this.getHeader('Date')) {
+                this.setHeader('Date', this.date.toUTCString().replace(/GMT/, '+0000'));
+            }
+
+            // ensure that Message-Id is present
+            this.messageId();
+
+            if (!this.getHeader('MIME-Version')) {
+                this.setHeader('MIME-Version', '1.0');
+            }
+
+            // Ensure that Content-Type is the last header for the root node
+            for (let i = this._headers.length - 2; i >= 0; i--) {
+                let header = this._headers[i];
+                if (header.key === 'Content-Type') {
+                    this._headers.splice(i, 1);
+                    this._headers.push(header);
+                }
+            }
+        }
+
+        this._headers.forEach(header => {
+            let key = header.key;
+            let value = header.value;
+            let structured;
+            let param;
+            let options = {};
+            let formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
+
+            if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
+                Object.keys(value).forEach(key => {
+                    if (key !== 'value') {
+                        options[key] = value[key];
+                    }
+                });
+                value = (value.value || '').toString();
+                if (!value.trim()) {
+                    return;
+                }
+            }
+
+            if (options.prepared) {
+                // header value is
+                if (options.foldLines) {
+                    headers.push(mimeFuncs.foldLines(key + ': ' + value));
+                } else {
+                    headers.push(key + ': ' + value);
+                }
+                return;
+            }
+
+            switch (header.key) {
+                case 'Content-Disposition':
+                    structured = mimeFuncs.parseHeaderValue(value);
+                    if (this.filename) {
+                        structured.params.filename = this.filename;
+                    }
+                    value = mimeFuncs.buildHeaderValue(structured);
+                    break;
+
+                case 'Content-Type':
+                    structured = mimeFuncs.parseHeaderValue(value);
+
+                    this._handleContentType(structured);
+
+                    if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
+                        structured.params.charset = 'utf-8';
+                    }
+
+                    value = mimeFuncs.buildHeaderValue(structured);
+
+                    if (this.filename) {
+                        // add support for non-compliant clients like QQ webmail
+                        // we can't build the value with buildHeaderValue as the value is non standard and
+                        // would be converted to parameter continuation encoding that we do not want
+                        param = this._encodeWords(this.filename);
+
+                        if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
+                            // include value in quotes if needed
+                            param = '"' + param + '"';
+                        }
+                        value += '; name=' + param;
+                    }
+                    break;
+
+                case 'Bcc':
+                    if (!this.keepBcc) {
+                        // skip BCC values
+                        return;
+                    }
+                    break;
+            }
+
+            value = this._encodeHeaderValue(key, value);
+
+            // skip empty lines
+            if (!(value || '').toString().trim()) {
+                return;
+            }
+
+            if (typeof this.normalizeHeaderKey === 'function') {
+                let normalized = this.normalizeHeaderKey(key, value);
+                if (normalized && typeof normalized === 'string' && normalized.length) {
+                    key = normalized;
+                }
+            }
+
+            headers.push(mimeFuncs.foldLines(key + ': ' + value, 76));
+        });
+
+        return headers.join('\r\n');
+    }
+
+    /**
+     * Streams the rfc2822 message from the current node. If this is a root node,
+     * mandatory header fields are set if missing (Date, Message-Id, MIME-Version)
+     *
+     * @return {String} Compiled message
+     */
+    createReadStream(options) {
+        options = options || {};
+
+        let stream = new PassThrough(options);
+        let outputStream = stream;
+        let transform;
+
+        this.stream(stream, options, err => {
+            if (err) {
+                outputStream.emit('error', err);
+                return;
+            }
+            stream.end();
+        });
+
+        for (let i = 0, len = this._transforms.length; i < len; i++) {
+            transform = typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
+            outputStream.once('error', err => {
+                transform.emit('error', err);
+            });
+            outputStream = outputStream.pipe(transform);
+        }
+
+        // ensure terminating newline after possible user transforms
+        transform = new LastNewline();
+        outputStream.once('error', err => {
+            transform.emit('error', err);
+        });
+        outputStream = outputStream.pipe(transform);
+
+        // dkim and stuff
+        for (let i = 0, len = this._processFuncs.length; i < len; i++) {
+            transform = this._processFuncs[i];
+            outputStream = transform(outputStream);
+        }
+
+        if (this.newline) {
+            const winbreak = ['win', 'windows', 'dos', '\r\n'].includes(this.newline.toString().toLowerCase());
+            const newlineTransform = winbreak ? new LeWindows() : new LeUnix();
+
+            const stream = outputStream.pipe(newlineTransform);
+            outputStream.on('error', err => stream.emit('error', err));
+            return stream;
+        }
+
+        return outputStream;
+    }
+
+    /**
+     * Appends a transform stream object to the transforms list. Final output
+     * is passed through this stream before exposing
+     *
+     * @param {Object} transform Read-Write stream
+     */
+    transform(transform) {
+        this._transforms.push(transform);
+    }
+
+    /**
+     * Appends a post process function. The functon is run after transforms and
+     * uses the following syntax
+     *
+     *   processFunc(input) -> outputStream
+     *
+     * @param {Object} processFunc Read-Write stream
+     */
+    processFunc(processFunc) {
+        this._processFuncs.push(processFunc);
+    }
+
+    stream(outputStream, options, done) {
+        let transferEncoding = this.getTransferEncoding();
+        let contentStream;
+        let localStream;
+
+        // protect actual callback against multiple triggering
+        let returned = false;
+        let callback = err => {
+            if (returned) {
+                return;
+            }
+            returned = true;
+            done(err);
+        };
+
+        // for multipart nodes, push child nodes
+        // for content nodes end the stream
+        let finalize = () => {
+            let childId = 0;
+            let processChildNode = () => {
+                if (childId >= this.childNodes.length) {
+                    outputStream.write('\r\n--' + this.boundary + '--\r\n');
+                    return callback();
+                }
+                let child = this.childNodes[childId++];
+                outputStream.write((childId > 1 ? '\r\n' : '') + '--' + this.boundary + '\r\n');
+                child.stream(outputStream, options, err => {
+                    if (err) {
+                        return callback(err);
+                    }
+                    setImmediate(processChildNode);
+                });
+            };
+
+            if (this.multipart) {
+                setImmediate(processChildNode);
+            } else {
+                return callback();
+            }
+        };
+
+        // pushes node content
+        let sendContent = () => {
+            if (this.content) {
+                if (Object.prototype.toString.call(this.content) === '[object Error]') {
+                    // content is already errored
+                    return callback(this.content);
+                }
+
+                if (typeof this.content.pipe === 'function') {
+                    this.content.removeListener('error', this._contentErrorHandler);
+                    this._contentErrorHandler = err => callback(err);
+                    this.content.once('error', this._contentErrorHandler);
+                }
+
+                let createStream = () => {
+                    if (['quoted-printable', 'base64'].includes(transferEncoding)) {
+                        contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
+
+                        contentStream.pipe(outputStream, {
+                            end: false
+                        });
+                        contentStream.once('end', finalize);
+                        contentStream.once('error', err => callback(err));
+
+                        localStream = this._getStream(this.content);
+                        localStream.pipe(contentStream);
+                    } else {
+                        // anything that is not QP or Base54 passes as-is
+                        localStream = this._getStream(this.content);
+                        localStream.pipe(outputStream, {
+                            end: false
+                        });
+                        localStream.once('end', finalize);
+                    }
+
+                    localStream.once('error', err => callback(err));
+                };
+
+                if (this.content._resolve) {
+                    let chunks = [];
+                    let chunklen = 0;
+                    let returned = false;
+                    let sourceStream = this._getStream(this.content);
+                    sourceStream.on('error', err => {
+                        if (returned) {
+                            return;
+                        }
+                        returned = true;
+                        callback(err);
+                    });
+                    sourceStream.on('readable', () => {
+                        let chunk;
+                        while ((chunk = sourceStream.read()) !== null) {
+                            chunks.push(chunk);
+                            chunklen += chunk.length;
+                        }
+                    });
+                    sourceStream.on('end', () => {
+                        if (returned) {
+                            return;
+                        }
+                        returned = true;
+                        this.content._resolve = false;
+                        this.content._resolvedValue = Buffer.concat(chunks, chunklen);
+                        setImmediate(createStream);
+                    });
+                } else {
+                    setImmediate(createStream);
+                }
+                return;
+            } else {
+                return setImmediate(finalize);
+            }
+        };
+
+        if (this._raw) {
+            setImmediate(() => {
+                if (Object.prototype.toString.call(this._raw) === '[object Error]') {
+                    // content is already errored
+                    return callback(this._raw);
+                }
+
+                // remove default error handler (if set)
+                if (typeof this._raw.pipe === 'function') {
+                    this._raw.removeListener('error', this._contentErrorHandler);
+                }
+
+                let raw = this._getStream(this._raw);
+                raw.pipe(outputStream, {
+                    end: false
+                });
+                raw.on('error', err => outputStream.emit('error', err));
+                raw.on('end', finalize);
+            });
+        } else {
+            outputStream.write(this.buildHeaders() + '\r\n\r\n');
+            setImmediate(sendContent);
+        }
+    }
+
+    /**
+     * Sets envelope to be used instead of the generated one
+     *
+     * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
+     */
+    setEnvelope(envelope) {
+        let list;
+
+        this._envelope = {
+            from: false,
+            to: []
+        };
+
+        if (envelope.from) {
+            list = [];
+            this._convertAddresses(this._parseAddresses(envelope.from), list);
+            list = list.filter(address => address && address.address);
+            if (list.length && list[0]) {
+                this._envelope.from = list[0].address;
+            }
+        }
+        ['to', 'cc', 'bcc'].forEach(key => {
+            if (envelope[key]) {
+                this._convertAddresses(this._parseAddresses(envelope[key]), this._envelope.to);
+            }
+        });
+
+        this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address);
+
+        let standardFields = ['to', 'cc', 'bcc', 'from'];
+        Object.keys(envelope).forEach(key => {
+            if (!standardFields.includes(key)) {
+                this._envelope[key] = envelope[key];
+            }
+        });
+
+        return this;
+    }
+
+    /**
+     * Generates and returns an object with parsed address fields
+     *
+     * @return {Object} Address object
+     */
+    getAddresses() {
+        let addresses = {};
+
+        this._headers.forEach(header => {
+            let key = header.key.toLowerCase();
+            if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
+                if (!Array.isArray(addresses[key])) {
+                    addresses[key] = [];
+                }
+
+                this._convertAddresses(this._parseAddresses(header.value), addresses[key]);
+            }
+        });
+
+        return addresses;
+    }
+
+    /**
+     * Generates and returns SMTP envelope with the sender address and a list of recipients addresses
+     *
+     * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
+     */
+    getEnvelope() {
+        if (this._envelope) {
+            return this._envelope;
+        }
+
+        let envelope = {
+            from: false,
+            to: []
+        };
+        this._headers.forEach(header => {
+            let list = [];
+            if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
+                this._convertAddresses(this._parseAddresses(header.value), list);
+                if (list.length && list[0]) {
+                    envelope.from = list[0].address;
+                }
+            } else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
+                this._convertAddresses(this._parseAddresses(header.value), envelope.to);
+            }
+        });
+
+        envelope.to = envelope.to.map(to => to.address);
+
+        return envelope;
+    }
+
+    /**
+     * Returns Message-Id value. If it does not exist, then creates one
+     *
+     * @return {String} Message-Id value
+     */
+    messageId() {
+        let messageId = this.getHeader('Message-ID');
+        // You really should define your own Message-Id field!
+        if (!messageId) {
+            messageId = this._generateMessageId();
+            this.setHeader('Message-ID', messageId);
+        }
+        return messageId;
+    }
+
+    /**
+     * Sets pregenerated content that will be used as the output of this node
+     *
+     * @param {String|Buffer|Stream} Raw MIME contents
+     */
+    setRaw(raw) {
+        this._raw = raw;
+
+        if (this._raw && typeof this._raw.pipe === 'function') {
+            // pre-stream handler. might be triggered if a stream is set as content
+            // and 'error' fires before anything is done with this stream
+            this._contentErrorHandler = err => {
+                this._raw.removeListener('error', this._contentErrorHandler);
+                this._raw = err;
+            };
+            this._raw.once('error', this._contentErrorHandler);
+        }
+
+        return this;
+    }
+
+    /////// PRIVATE METHODS
+
+    /**
+     * Detects and returns handle to a stream related with the content.
+     *
+     * @param {Mixed} content Node content
+     * @returns {Object} Stream object
+     */
+    _getStream(content) {
+        let contentStream;
+
+        if (content._resolvedValue) {
+            // pass string or buffer content as a stream
+            contentStream = new PassThrough();
+
+            setImmediate(() => {
+                try {
+                    contentStream.end(content._resolvedValue);
+                } catch (err) {
+                    contentStream.emit('error', err);
+                }
+            });
+
+            return contentStream;
+        } else if (typeof content.pipe === 'function') {
+            // assume as stream
+            return content;
+        } else if (content && typeof content.path === 'string' && !content.href) {
+            if (this.disableFileAccess) {
+                contentStream = new PassThrough();
+                setImmediate(() => contentStream.emit('error', new Error('File access rejected for ' + content.path)));
+                return contentStream;
+            }
+            // read file
+            return fs.createReadStream(content.path);
+        } else if (content && typeof content.href === 'string') {
+            if (this.disableUrlAccess) {
+                contentStream = new PassThrough();
+                setImmediate(() => contentStream.emit('error', new Error('Url access rejected for ' + content.href)));
+                return contentStream;
+            }
+            // fetch URL
+            return nmfetch(content.href, { headers: content.httpHeaders });
+        } else {
+            // pass string or buffer content as a stream
+            contentStream = new PassThrough();
+
+            setImmediate(() => {
+                try {
+                    contentStream.end(content || '');
+                } catch (err) {
+                    contentStream.emit('error', err);
+                }
+            });
+            return contentStream;
+        }
+    }
+
+    /**
+     * Parses addresses. Takes in a single address or an array or an
+     * array of address arrays (eg. To: [[first group], [second group],...])
+     *
+     * @param {Mixed} addresses Addresses to be parsed
+     * @return {Array} An array of address objects
+     */
+    _parseAddresses(addresses) {
+        return [].concat.apply(
+            [],
+            [].concat(addresses).map(address => {
+                // eslint-disable-line prefer-spread
+                if (address && address.address) {
+                    address.address = this._normalizeAddress(address.address);
+                    address.name = address.name || '';
+                    return [address];
+                }
+                return addressparser(address);
+            })
+        );
+    }
+
+    /**
+     * Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
+     *
+     * @param {String} key Key to be normalized
+     * @return {String} key in Camel-Case form
+     */
+    _normalizeHeaderKey(key) {
+        key = (key || '')
+            .toString()
+            // no newlines in keys
+            .replace(/\r?\n|\r/g, ' ')
+            .trim()
+            .toLowerCase()
+            // use uppercase words, except MIME
+            .replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
+            // special case
+            .replace(/^Content-Features$/i, 'Content-features');
+
+        return key;
+    }
+
+    /**
+     * Checks if the content type is multipart and defines boundary if needed.
+     * Doesn't return anything, modifies object argument instead.
+     *
+     * @param {Object} structured Parsed header value for 'Content-Type' key
+     */
+    _handleContentType(structured) {
+        this.contentType = structured.value.trim().toLowerCase();
+
+        this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
+
+        if (this.multipart) {
+            this.boundary = structured.params.boundary = structured.params.boundary || this.boundary || this._generateBoundary();
+        } else {
+            this.boundary = false;
+        }
+    }
+
+    /**
+     * Generates a multipart boundary value
+     *
+     * @return {String} boundary value
+     */
+    _generateBoundary() {
+        return this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary + '-Part_' + this._nodeId;
+    }
+
+    /**
+     * Encodes a header value for use in the generated rfc2822 email.
+     *
+     * @param {String} key Header key
+     * @param {String} value Header value
+     */
+    _encodeHeaderValue(key, value) {
+        key = this._normalizeHeaderKey(key);
+
+        switch (key) {
+            // Structured headers
+            case 'From':
+            case 'Sender':
+            case 'To':
+            case 'Cc':
+            case 'Bcc':
+            case 'Reply-To':
+                return this._convertAddresses(this._parseAddresses(value));
+
+            // values enclosed in <>
+            case 'Message-ID':
+            case 'In-Reply-To':
+            case 'Content-Id':
+                value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
+
+                if (value.charAt(0) !== '<') {
+                    value = '<' + value;
+                }
+
+                if (value.charAt(value.length - 1) !== '>') {
+                    value = value + '>';
+                }
+                return value;
+
+            // space separated list of values enclosed in <>
+            case 'References':
+                value = [].concat
+                    .apply(
+                        [],
+                        [].concat(value || '').map(elm => {
+                            // eslint-disable-line prefer-spread
+                            elm = (elm || '')
+                                .toString()
+                                .replace(/\r?\n|\r/g, ' ')
+                                .trim();
+                            return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
+                        })
+                    )
+                    .map(elm => {
+                        if (elm.charAt(0) !== '<') {
+                            elm = '<' + elm;
+                        }
+                        if (elm.charAt(elm.length - 1) !== '>') {
+                            elm = elm + '>';
+                        }
+                        return elm;
+                    });
+
+                return value.join(' ').trim();
+
+            case 'Date':
+                if (Object.prototype.toString.call(value) === '[object Date]') {
+                    return value.toUTCString().replace(/GMT/, '+0000');
+                }
+
+                value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
+                return this._encodeWords(value);
+
+            case 'Content-Type':
+            case 'Content-Disposition':
+                // if it includes a filename then it is already encoded
+                return (value || '').toString().replace(/\r?\n|\r/g, ' ');
+
+            default:
+                value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
+                // encodeWords only encodes if needed, otherwise the original string is returned
+                return this._encodeWords(value);
+        }
+    }
+
+    /**
+     * Rebuilds address object using punycode and other adjustments
+     *
+     * @param {Array} addresses An array of address objects
+     * @param {Array} [uniqueList] An array to be populated with addresses
+     * @return {String} address string
+     */
+    _convertAddresses(addresses, uniqueList) {
+        let values = [];
+
+        uniqueList = uniqueList || [];
+
+        [].concat(addresses || []).forEach(address => {
+            if (address.address) {
+                address.address = this._normalizeAddress(address.address);
+
+                if (!address.name) {
+                    values.push(address.address.indexOf(' ') >= 0 ? `<${address.address}>` : `${address.address}`);
+                } else if (address.name) {
+                    values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
+                }
+
+                if (address.address) {
+                    if (!uniqueList.filter(a => a.address === address.address).length) {
+                        uniqueList.push(address);
+                    }
+                }
+            } else if (address.group) {
+                let groupListAddresses = (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim();
+                values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`);
+            }
+        });
+
+        return values.join(', ');
+    }
+
+    /**
+     * Normalizes an email address
+     *
+     * @param {Array} address An array of address objects
+     * @return {String} address string
+     */
+    _normalizeAddress(address) {
+        address = (address || '')
+            .toString()
+            .replace(/[\x00-\x1F<>]+/g, ' ') // remove unallowed characters
+            .trim();
+
+        let lastAt = address.lastIndexOf('@');
+        if (lastAt < 0) {
+            // Bare username
+            return address;
+        }
+
+        let user = address.substr(0, lastAt);
+        let domain = address.substr(lastAt + 1);
+
+        // Usernames are not touched and are kept as is even if these include unicode
+        // Domains are punycoded by default
+        // 'jõgeva.ee' will be converted to 'xn--jgeva-dua.ee'
+        // non-unicode domains are left as is
+
+        let encodedDomain;
+
+        try {
+            encodedDomain = punycode.toASCII(domain.toLowerCase());
+        } catch (err) {
+            // keep as is?
+        }
+
+        if (user.indexOf(' ') >= 0) {
+            if (user.charAt(0) !== '"') {
+                user = '"' + user;
+            }
+            if (user.substr(-1) !== '"') {
+                user = user + '"';
+            }
+        }
+
+        return `${user}@${encodedDomain}`;
+    }
+
+    /**
+     * If needed, mime encodes the name part
+     *
+     * @param {String} name Name part of an address
+     * @returns {String} Mime word encoded string if needed
+     */
+    _encodeAddressName(name) {
+        if (!/^[\w ]*$/.test(name)) {
+            if (/^[\x20-\x7e]*$/.test(name)) {
+                return '"' + name.replace(/([\\"])/g, '\\$1') + '"';
+            } else {
+                return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
+            }
+        }
+        return name;
+    }
+
+    /**
+     * If needed, mime encodes the name part
+     *
+     * @param {String} name Name part of an address
+     * @returns {String} Mime word encoded string if needed
+     */
+    _encodeWords(value) {
+        // set encodeAll parameter to true even though it is against the recommendation of RFC2047,
+        // by default only words that include non-ascii should be converted into encoded words
+        // but some clients (eg. Zimbra) do not handle it properly and remove surrounding whitespace
+        return mimeFuncs.encodeWords(value, this._getTextEncoding(value), 52, true);
+    }
+
+    /**
+     * Detects best mime encoding for a text value
+     *
+     * @param {String} value Value to check for
+     * @return {String} either 'Q' or 'B'
+     */
+    _getTextEncoding(value) {
+        value = (value || '').toString();
+
+        let encoding = this.textEncoding;
+        let latinLen;
+        let nonLatinLen;
+
+        if (!encoding) {
+            // count latin alphabet symbols and 8-bit range symbols + control symbols
+            // if there are more latin characters, then use quoted-printable
+            // encoding, otherwise use base64
+            nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length; // eslint-disable-line no-control-regex
+            latinLen = (value.match(/[a-z]/gi) || []).length;
+            // if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
+            encoding = nonLatinLen < latinLen ? 'Q' : 'B';
+        }
+        return encoding;
+    }
+
+    /**
+     * Generates a message id
+     *
+     * @return {String} Random Message-ID value
+     */
+    _generateMessageId() {
+        return (
+            '<' +
+            [2, 2, 2, 6].reduce(
+                // crux to generate UUID-like random strings
+                (prev, len) => prev + '-' + crypto.randomBytes(len).toString('hex'),
+                crypto.randomBytes(4).toString('hex')
+            ) +
+            '@' +
+            // try to use the domain of the FROM address or fallback to server hostname
+            (this.getEnvelope().from || this.hostname || 'localhost').split('@').pop() +
+            '>'
+        );
+    }
+}
+
+module.exports = MimeNode;
Index: node_modules/nodemailer/lib/mime-node/last-newline.js
===================================================================
--- node_modules/nodemailer/lib/mime-node/last-newline.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
+++ node_modules/nodemailer/lib/mime-node/last-newline.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
@@ -0,0 +1,33 @@
+'use strict';
+
+const Transform = require('stream').Transform;
+
+class LastNewline extends Transform {
+    constructor() {
+        super();
+        this.lastByte = false;
+    }
+
+    _transform(chunk, encoding, done) {
+        if (chunk.length) {
+            this.lastByte = chunk[chunk.length - 1];
+        }
+
+        this.push(chunk);
+        done();
+    }
+
+    _flush(done) {
+        if (this.lastByte === 0x0a) {
+            return done();
+        }
+        if (this.lastByte === 0x0d) {
+            this.push(Buffer.from('\n'));
+            return done();
+        }
+        this.push(Buffer.from('\r\n'));
+        return done();
+    }
+}
+
+module.exports = LastNewline;
Index: node_modules/nodemailer/lib/mime-node/le-unix.js
===================================================================
--- node_modules/nodemailer/lib/mime-node/le-unix.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
+++ node_modules/nodemailer/lib/mime-node/le-unix.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
@@ -0,0 +1,43 @@
+'use strict';
+
+const stream = require('stream');
+const Transform = stream.Transform;
+
+/**
+ * Ensures that only <LF> is used for linebreaks
+ *
+ * @param {Object} options Stream options
+ */
+class LeWindows extends Transform {
+    constructor(options) {
+        super(options);
+        // init Transform
+        this.options = options || {};
+    }
+
+    /**
+     * Escapes dots
+     */
+    _transform(chunk, encoding, done) {
+        let buf;
+        let lastPos = 0;
+
+        for (let i = 0, len = chunk.length; i < len; i++) {
+            if (chunk[i] === 0x0d) {
+                // \n
+                buf = chunk.slice(lastPos, i);
+                lastPos = i + 1;
+                this.push(buf);
+            }
+        }
+        if (lastPos && lastPos < chunk.length) {
+            buf = chunk.slice(lastPos);
+            this.push(buf);
+        } else if (!lastPos) {
+            this.push(chunk);
+        }
+        done();
+    }
+}
+
+module.exports = LeWindows;
Index: node_modules/nodemailer/lib/mime-node/le-windows.js
===================================================================
--- node_modules/nodemailer/lib/mime-node/le-windows.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
+++ node_modules/nodemailer/lib/mime-node/le-windows.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
@@ -0,0 +1,52 @@
+'use strict';
+
+const stream = require('stream');
+const Transform = stream.Transform;
+
+/**
+ * Ensures that only <CR><LF> sequences are used for linebreaks
+ *
+ * @param {Object} options Stream options
+ */
+class LeWindows extends Transform {
+    constructor(options) {
+        super(options);
+        // init Transform
+        this.options = options || {};
+        this.lastByte = false;
+    }
+
+    /**
+     * Escapes dots
+     */
+    _transform(chunk, encoding, done) {
+        let buf;
+        let lastPos = 0;
+
+        for (let i = 0, len = chunk.length; i < len; i++) {
+            if (chunk[i] === 0x0a) {
+                // \n
+                if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) {
+                    if (i > lastPos) {
+                        buf = chunk.slice(lastPos, i);
+                        this.push(buf);
+                    }
+                    this.push(Buffer.from('\r\n'));
+                    lastPos = i + 1;
+                }
+            }
+        }
+
+        if (lastPos && lastPos < chunk.length) {
+            buf = chunk.slice(lastPos);
+            this.push(buf);
+        } else if (!lastPos) {
+            this.push(chunk);
+        }
+
+        this.lastByte = chunk[chunk.length - 1];
+        done();
+    }
+}
+
+module.exports = LeWindows;
