Index: node_modules/nodemailer/lib/mail-composer/index.js
===================================================================
--- node_modules/nodemailer/lib/mail-composer/index.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
+++ node_modules/nodemailer/lib/mail-composer/index.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
@@ -0,0 +1,565 @@
+/* eslint no-undefined: 0 */
+
+'use strict';
+
+const MimeNode = require('../mime-node');
+const mimeFuncs = require('../mime-funcs');
+const parseDataURI = require('../shared').parseDataURI;
+
+/**
+ * Creates the object for composing a MimeNode instance out from the mail options
+ *
+ * @constructor
+ * @param {Object} mail Mail options
+ */
+class MailComposer {
+    constructor(mail) {
+        this.mail = mail || {};
+        this.message = false;
+    }
+
+    /**
+     * Builds MimeNode instance
+     */
+    compile() {
+        this._alternatives = this.getAlternatives();
+        this._htmlNode = this._alternatives.filter(alternative => /^text\/html\b/i.test(alternative.contentType)).pop();
+        this._attachments = this.getAttachments(!!this._htmlNode);
+
+        this._useRelated = !!(this._htmlNode && this._attachments.related.length);
+        this._useAlternative = this._alternatives.length > 1;
+        this._useMixed = this._attachments.attached.length > 1 || (this._alternatives.length && this._attachments.attached.length === 1);
+
+        // Compose MIME tree
+        if (this.mail.raw) {
+            this.message = new MimeNode('message/rfc822', { newline: this.mail.newline }).setRaw(this.mail.raw);
+        } else if (this._useMixed) {
+            this.message = this._createMixed();
+        } else if (this._useAlternative) {
+            this.message = this._createAlternative();
+        } else if (this._useRelated) {
+            this.message = this._createRelated();
+        } else {
+            this.message = this._createContentNode(
+                false,
+                []
+                    .concat(this._alternatives || [])
+                    .concat(this._attachments.attached || [])
+                    .shift() || {
+                    contentType: 'text/plain',
+                    content: ''
+                }
+            );
+        }
+
+        // Add custom headers
+        if (this.mail.headers) {
+            this.message.addHeader(this.mail.headers);
+        }
+
+        // Add headers to the root node, always overrides custom headers
+        ['from', 'sender', 'to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'references', 'subject', 'message-id', 'date'].forEach(header => {
+            let key = header.replace(/-(\w)/g, (o, c) => c.toUpperCase());
+            if (this.mail[key]) {
+                this.message.setHeader(header, this.mail[key]);
+            }
+        });
+
+        // Sets custom envelope
+        if (this.mail.envelope) {
+            this.message.setEnvelope(this.mail.envelope);
+        }
+
+        // ensure Message-Id value
+        this.message.messageId();
+
+        return this.message;
+    }
+
+    /**
+     * List all attachments. Resulting attachment objects can be used as input for MimeNode nodes
+     *
+     * @param {Boolean} findRelated If true separate related attachments from attached ones
+     * @returns {Object} An object of arrays (`related` and `attached`)
+     */
+    getAttachments(findRelated) {
+        let icalEvent, eventObject;
+        let attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
+            let data;
+            let isMessageNode = /^message\//i.test(attachment.contentType);
+
+            if (/^data:/i.test(attachment.path || attachment.href)) {
+                attachment = this._processDataUrl(attachment);
+            }
+
+            let contentType = attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
+            let isImage = /^image\//i.test(contentType);
+            let contentDisposition = attachment.contentDisposition || (isMessageNode || (isImage && attachment.cid) ? 'inline' : 'attachment');
+
+            data = {
+                contentType,
+                contentDisposition,
+                contentTransferEncoding: 'contentTransferEncoding' in attachment ? attachment.contentTransferEncoding : 'base64'
+            };
+
+            if (attachment.filename) {
+                data.filename = attachment.filename;
+            } else if (!isMessageNode && attachment.filename !== false) {
+                data.filename = (attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
+                if (data.filename.indexOf('.') < 0) {
+                    data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
+                }
+            }
+
+            if (/^https?:\/\//i.test(attachment.path)) {
+                attachment.href = attachment.path;
+                attachment.path = undefined;
+            }
+
+            if (attachment.cid) {
+                data.cid = attachment.cid;
+            }
+
+            if (attachment.raw) {
+                data.raw = attachment.raw;
+            } else if (attachment.path) {
+                data.content = {
+                    path: attachment.path
+                };
+            } else if (attachment.href) {
+                data.content = {
+                    href: attachment.href,
+                    httpHeaders: attachment.httpHeaders
+                };
+            } else {
+                data.content = attachment.content || '';
+            }
+
+            if (attachment.encoding) {
+                data.encoding = attachment.encoding;
+            }
+
+            if (attachment.headers) {
+                data.headers = attachment.headers;
+            }
+
+            return data;
+        });
+
+        if (this.mail.icalEvent) {
+            if (
+                typeof this.mail.icalEvent === 'object' &&
+                (this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
+            ) {
+                icalEvent = this.mail.icalEvent;
+            } else {
+                icalEvent = {
+                    content: this.mail.icalEvent
+                };
+            }
+
+            eventObject = {};
+            Object.keys(icalEvent).forEach(key => {
+                eventObject[key] = icalEvent[key];
+            });
+
+            eventObject.contentType = 'application/ics';
+            if (!eventObject.headers) {
+                eventObject.headers = {};
+            }
+            eventObject.filename = eventObject.filename || 'invite.ics';
+            eventObject.headers['Content-Disposition'] = 'attachment';
+            eventObject.headers['Content-Transfer-Encoding'] = 'base64';
+        }
+
+        if (!findRelated) {
+            return {
+                attached: attachments.concat(eventObject || []),
+                related: []
+            };
+        } else {
+            return {
+                attached: attachments.filter(attachment => !attachment.cid).concat(eventObject || []),
+                related: attachments.filter(attachment => !!attachment.cid)
+            };
+        }
+    }
+
+    /**
+     * List alternatives. Resulting objects can be used as input for MimeNode nodes
+     *
+     * @returns {Array} An array of alternative elements. Includes the `text` and `html` values as well
+     */
+    getAlternatives() {
+        let alternatives = [],
+            text,
+            html,
+            watchHtml,
+            amp,
+            icalEvent,
+            eventObject;
+
+        if (this.mail.text) {
+            if (typeof this.mail.text === 'object' && (this.mail.text.content || this.mail.text.path || this.mail.text.href || this.mail.text.raw)) {
+                text = this.mail.text;
+            } else {
+                text = {
+                    content: this.mail.text
+                };
+            }
+            text.contentType = 'text/plain; charset=utf-8';
+        }
+
+        if (this.mail.watchHtml) {
+            if (
+                typeof this.mail.watchHtml === 'object' &&
+                (this.mail.watchHtml.content || this.mail.watchHtml.path || this.mail.watchHtml.href || this.mail.watchHtml.raw)
+            ) {
+                watchHtml = this.mail.watchHtml;
+            } else {
+                watchHtml = {
+                    content: this.mail.watchHtml
+                };
+            }
+            watchHtml.contentType = 'text/watch-html; charset=utf-8';
+        }
+
+        if (this.mail.amp) {
+            if (typeof this.mail.amp === 'object' && (this.mail.amp.content || this.mail.amp.path || this.mail.amp.href || this.mail.amp.raw)) {
+                amp = this.mail.amp;
+            } else {
+                amp = {
+                    content: this.mail.amp
+                };
+            }
+            amp.contentType = 'text/x-amp-html; charset=utf-8';
+        }
+
+        // NB! when including attachments with a calendar alternative you might end up in a blank screen on some clients
+        if (this.mail.icalEvent) {
+            if (
+                typeof this.mail.icalEvent === 'object' &&
+                (this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
+            ) {
+                icalEvent = this.mail.icalEvent;
+            } else {
+                icalEvent = {
+                    content: this.mail.icalEvent
+                };
+            }
+
+            eventObject = {};
+            Object.keys(icalEvent).forEach(key => {
+                eventObject[key] = icalEvent[key];
+            });
+
+            if (eventObject.content && typeof eventObject.content === 'object') {
+                // we are going to have the same attachment twice, so mark this to be
+                // resolved just once
+                eventObject.content._resolve = true;
+            }
+
+            eventObject.filename = false;
+            eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
+            if (!eventObject.headers) {
+                eventObject.headers = {};
+            }
+        }
+
+        if (this.mail.html) {
+            if (typeof this.mail.html === 'object' && (this.mail.html.content || this.mail.html.path || this.mail.html.href || this.mail.html.raw)) {
+                html = this.mail.html;
+            } else {
+                html = {
+                    content: this.mail.html
+                };
+            }
+            html.contentType = 'text/html; charset=utf-8';
+        }
+
+        []
+            .concat(text || [])
+            .concat(watchHtml || [])
+            .concat(amp || [])
+            .concat(html || [])
+            .concat(eventObject || [])
+            .concat(this.mail.alternatives || [])
+            .forEach(alternative => {
+                let data;
+
+                if (/^data:/i.test(alternative.path || alternative.href)) {
+                    alternative = this._processDataUrl(alternative);
+                }
+
+                data = {
+                    contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
+                    contentTransferEncoding: alternative.contentTransferEncoding
+                };
+
+                if (alternative.filename) {
+                    data.filename = alternative.filename;
+                }
+
+                if (/^https?:\/\//i.test(alternative.path)) {
+                    alternative.href = alternative.path;
+                    alternative.path = undefined;
+                }
+
+                if (alternative.raw) {
+                    data.raw = alternative.raw;
+                } else if (alternative.path) {
+                    data.content = {
+                        path: alternative.path
+                    };
+                } else if (alternative.href) {
+                    data.content = {
+                        href: alternative.href
+                    };
+                } else {
+                    data.content = alternative.content || '';
+                }
+
+                if (alternative.encoding) {
+                    data.encoding = alternative.encoding;
+                }
+
+                if (alternative.headers) {
+                    data.headers = alternative.headers;
+                }
+
+                alternatives.push(data);
+            });
+
+        return alternatives;
+    }
+
+    /**
+     * Builds multipart/mixed node. It should always contain different type of elements on the same level
+     * eg. text + attachments
+     *
+     * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
+     * @returns {Object} MimeNode node element
+     */
+    _createMixed(parentNode) {
+        let node;
+
+        if (!parentNode) {
+            node = new MimeNode('multipart/mixed', {
+                baseBoundary: this.mail.baseBoundary,
+                textEncoding: this.mail.textEncoding,
+                boundaryPrefix: this.mail.boundaryPrefix,
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess,
+                normalizeHeaderKey: this.mail.normalizeHeaderKey,
+                newline: this.mail.newline
+            });
+        } else {
+            node = parentNode.createChild('multipart/mixed', {
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess,
+                normalizeHeaderKey: this.mail.normalizeHeaderKey,
+                newline: this.mail.newline
+            });
+        }
+
+        if (this._useAlternative) {
+            this._createAlternative(node);
+        } else if (this._useRelated) {
+            this._createRelated(node);
+        }
+
+        []
+            .concat((!this._useAlternative && this._alternatives) || [])
+            .concat(this._attachments.attached || [])
+            .forEach(element => {
+                // if the element is a html node from related subpart then ignore it
+                if (!this._useRelated || element !== this._htmlNode) {
+                    this._createContentNode(node, element);
+                }
+            });
+
+        return node;
+    }
+
+    /**
+     * Builds multipart/alternative node. It should always contain same type of elements on the same level
+     * eg. text + html view of the same data
+     *
+     * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
+     * @returns {Object} MimeNode node element
+     */
+    _createAlternative(parentNode) {
+        let node;
+
+        if (!parentNode) {
+            node = new MimeNode('multipart/alternative', {
+                baseBoundary: this.mail.baseBoundary,
+                textEncoding: this.mail.textEncoding,
+                boundaryPrefix: this.mail.boundaryPrefix,
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess,
+                normalizeHeaderKey: this.mail.normalizeHeaderKey,
+                newline: this.mail.newline
+            });
+        } else {
+            node = parentNode.createChild('multipart/alternative', {
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess,
+                normalizeHeaderKey: this.mail.normalizeHeaderKey,
+                newline: this.mail.newline
+            });
+        }
+
+        this._alternatives.forEach(alternative => {
+            if (this._useRelated && this._htmlNode === alternative) {
+                this._createRelated(node);
+            } else {
+                this._createContentNode(node, alternative);
+            }
+        });
+
+        return node;
+    }
+
+    /**
+     * Builds multipart/related node. It should always contain html node with related attachments
+     *
+     * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
+     * @returns {Object} MimeNode node element
+     */
+    _createRelated(parentNode) {
+        let node;
+
+        if (!parentNode) {
+            node = new MimeNode('multipart/related; type="text/html"', {
+                baseBoundary: this.mail.baseBoundary,
+                textEncoding: this.mail.textEncoding,
+                boundaryPrefix: this.mail.boundaryPrefix,
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess,
+                normalizeHeaderKey: this.mail.normalizeHeaderKey,
+                newline: this.mail.newline
+            });
+        } else {
+            node = parentNode.createChild('multipart/related; type="text/html"', {
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess,
+                normalizeHeaderKey: this.mail.normalizeHeaderKey,
+                newline: this.mail.newline
+            });
+        }
+
+        this._createContentNode(node, this._htmlNode);
+
+        this._attachments.related.forEach(alternative => this._createContentNode(node, alternative));
+
+        return node;
+    }
+
+    /**
+     * Creates a regular node with contents
+     *
+     * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
+     * @param {Object} element Node data
+     * @returns {Object} MimeNode node element
+     */
+    _createContentNode(parentNode, element) {
+        element = element || {};
+        element.content = element.content || '';
+
+        let node;
+        let encoding = (element.encoding || 'utf8')
+            .toString()
+            .toLowerCase()
+            .replace(/[-_\s]/g, '');
+
+        if (!parentNode) {
+            node = new MimeNode(element.contentType, {
+                filename: element.filename,
+                baseBoundary: this.mail.baseBoundary,
+                textEncoding: this.mail.textEncoding,
+                boundaryPrefix: this.mail.boundaryPrefix,
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess,
+                normalizeHeaderKey: this.mail.normalizeHeaderKey,
+                newline: this.mail.newline
+            });
+        } else {
+            node = parentNode.createChild(element.contentType, {
+                filename: element.filename,
+                textEncoding: this.mail.textEncoding,
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess,
+                normalizeHeaderKey: this.mail.normalizeHeaderKey,
+                newline: this.mail.newline
+            });
+        }
+
+        // add custom headers
+        if (element.headers) {
+            node.addHeader(element.headers);
+        }
+
+        if (element.cid) {
+            node.setHeader('Content-Id', '<' + element.cid.replace(/[<>]/g, '') + '>');
+        }
+
+        if (element.contentTransferEncoding) {
+            node.setHeader('Content-Transfer-Encoding', element.contentTransferEncoding);
+        } else if (this.mail.encoding && /^text\//i.test(element.contentType)) {
+            node.setHeader('Content-Transfer-Encoding', this.mail.encoding);
+        }
+
+        if (!/^text\//i.test(element.contentType) || element.contentDisposition) {
+            node.setHeader(
+                'Content-Disposition',
+                element.contentDisposition || (element.cid && /^image\//i.test(element.contentType) ? 'inline' : 'attachment')
+            );
+        }
+
+        if (typeof element.content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
+            element.content = Buffer.from(element.content, encoding);
+        }
+
+        // prefer pregenerated raw content
+        if (element.raw) {
+            node.setRaw(element.raw);
+        } else {
+            node.setContent(element.content);
+        }
+
+        return node;
+    }
+
+    /**
+     * Parses data uri and converts it to a Buffer
+     *
+     * @param {Object} element Content element
+     * @return {Object} Parsed element
+     */
+    _processDataUrl(element) {
+        let parsedDataUri;
+        if ((element.path || element.href).match(/^data:/)) {
+            parsedDataUri = parseDataURI(element.path || element.href);
+        }
+
+        if (!parsedDataUri) {
+            return element;
+        }
+
+        element.content = parsedDataUri.data;
+        element.contentType = element.contentType || parsedDataUri.contentType;
+
+        if ('path' in element) {
+            element.path = false;
+        }
+
+        if ('href' in element) {
+            element.href = false;
+        }
+
+        return element;
+    }
+}
+
+module.exports = MailComposer;
