123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- /* eslint no-undefined: 0 */
- 'use strict';
- const MimeNode = require('../mime-node');
- const mimeFuncs = require('../mime-funcs');
- /**
- * 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().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);
- }
- data = {
- contentType: attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin'),
- contentDisposition: attachment.contentDisposition || (isMessageNode ? 'inline' : 'attachment'),
- 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
- };
- } 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' + (!text.encoding && mimeFuncs.isPlainText(text.content) ? '' : '; 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' + (!watchHtml.encoding && mimeFuncs.isPlainText(watchHtml.content) ? '' : '; 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' + (!amp.encoding && mimeFuncs.isPlainText(amp.content) ? '' : '; charset=utf-8');
- }
- // only include the calendar alternative if there are no attachments
- // otherwise you might end up in a blank screen on some clients
- if (this.mail.icalEvent && !(this.mail.attachments && this.mail.attachments.length)) {
- 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' + (!html.encoding && mimeFuncs.isPlainText(html.content) ? '' : '; 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
- });
- } else {
- node = parentNode.createChild('multipart/mixed', {
- disableUrlAccess: this.mail.disableUrlAccess,
- disableFileAccess: this.mail.disableFileAccess,
- normalizeHeaderKey: this.mail.normalizeHeaderKey
- });
- }
- 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
- });
- } else {
- node = parentNode.createChild('multipart/alternative', {
- disableUrlAccess: this.mail.disableUrlAccess,
- disableFileAccess: this.mail.disableFileAccess,
- normalizeHeaderKey: this.mail.normalizeHeaderKey
- });
- }
- 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
- });
- } else {
- node = parentNode.createChild('multipart/related; type="text/html"', {
- disableUrlAccess: this.mail.disableUrlAccess,
- disableFileAccess: this.mail.disableFileAccess,
- normalizeHeaderKey: this.mail.normalizeHeaderKey
- });
- }
- 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
- });
- } else {
- node = parentNode.createChild(element.contentType, {
- filename: element.filename,
- disableUrlAccess: this.mail.disableUrlAccess,
- disableFileAccess: this.mail.disableFileAccess,
- normalizeHeaderKey: this.mail.normalizeHeaderKey
- });
- }
- // 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 ? '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 parts = (element.path || element.href).match(/^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/i);
- if (!parts) {
- return element;
- }
- element.content = /\bbase64$/i.test(parts[1]) ? Buffer.from(parts[2], 'base64') : Buffer.from(decodeURIComponent(parts[2]));
- if ('path' in element) {
- element.path = false;
- }
- if ('href' in element) {
- element.href = false;
- }
- parts[1].split(';').forEach(item => {
- if (/^\w+\/[^/]+$/i.test(item)) {
- element.contentType = element.contentType || item.toLowerCase();
- }
- });
- return element;
- }
- }
- module.exports = MailComposer;
|