'use strict'; const shared = require('../shared'); const MimeNode = require('../mime-node'); const mimeFuncs = require('../mime-funcs'); class MailMessage { constructor(mailer, data) { this.mailer = mailer; this.data = {}; this.message = null; data = data || {}; let options = mailer.options || {}; let defaults = mailer._defaults || {}; Object.keys(data).forEach(key => { this.data[key] = data[key]; }); this.data.headers = this.data.headers || {}; // apply defaults Object.keys(defaults).forEach(key => { if (!(key in this.data)) { this.data[key] = defaults[key]; } else if (key === 'headers') { // headers is a special case. Allow setting individual default headers Object.keys(defaults.headers).forEach(key => { if (!(key in this.data.headers)) { this.data.headers[key] = defaults.headers[key]; } }); } }); // force specific keys from transporter options ['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey'].forEach(key => { if (key in options) { this.data[key] = options[key]; } }); } resolveContent(...args) { return shared.resolveContent(...args); } resolveAll(callback) { let keys = [[this.data, 'html'], [this.data, 'text'], [this.data, 'watchHtml'], [this.data, 'amp'], [this.data, 'icalEvent']]; if (this.data.alternatives && this.data.alternatives.length) { this.data.alternatives.forEach((alternative, i) => { keys.push([this.data.alternatives, i]); }); } if (this.data.attachments && this.data.attachments.length) { this.data.attachments.forEach((attachment, i) => { if (!attachment.filename) { attachment.filename = (attachment.path || attachment.href || '') .split('/') .pop() .split('?') .shift() || 'attachment-' + (i + 1); if (attachment.filename.indexOf('.') < 0) { attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType); } } if (!attachment.contentType) { attachment.contentType = mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin'); } keys.push([this.data.attachments, i]); }); } let mimeNode = new MimeNode(); let addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo']; addressKeys.forEach(address => { let value; if (this.message) { value = [].concat(mimeNode._parseAddresses(this.message.getHeader(address === 'replyTo' ? 'reply-to' : address)) || []); } else if (this.data[address]) { value = [].concat(mimeNode._parseAddresses(this.data[address]) || []); } if (value && value.length) { this.data[address] = value; } else if (address in this.data) { this.data[address] = null; } }); let singleKeys = ['from', 'sender', 'replyTo']; singleKeys.forEach(address => { if (this.data[address]) { this.data[address] = this.data[address].shift(); } }); let pos = 0; let resolveNext = () => { if (pos >= keys.length) { return callback(null, this.data); } let args = keys[pos++]; if (!args[0] || !args[0][args[1]]) { return resolveNext(); } shared.resolveContent(...args, (err, value) => { if (err) { return callback(err); } let node = { content: value }; if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) { Object.keys(args[0][args[1]]).forEach(key => { if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) { node[key] = args[0][args[1]][key]; } }); } args[0][args[1]] = node; resolveNext(); }); }; setImmediate(() => resolveNext()); } normalize(callback) { let envelope = this.data.envelope || this.message.getEnvelope(); let messageId = this.message.messageId(); this.resolveAll((err, data) => { if (err) { return callback(err); } data.envelope = envelope; data.messageId = messageId; ['html', 'text', 'watchHtml', 'amp'].forEach(key => { if (data[key] && data[key].content) { if (typeof data[key].content === 'string') { data[key] = data[key].content; } else if (Buffer.isBuffer(data[key].content)) { data[key] = data[key].content.toString(); } } }); if (data.icalEvent && Buffer.isBuffer(data.icalEvent.content)) { data.icalEvent.content = data.icalEvent.content.toString('base64'); data.icalEvent.encoding = 'base64'; } if (data.alternatives && data.alternatives.length) { data.alternatives.forEach(alternative => { if (alternative && alternative.content && Buffer.isBuffer(alternative.content)) { alternative.content = alternative.content.toString('base64'); alternative.encoding = 'base64'; } }); } if (data.attachments && data.attachments.length) { data.attachments.forEach(attachment => { if (attachment && attachment.content && Buffer.isBuffer(attachment.content)) { attachment.content = attachment.content.toString('base64'); attachment.encoding = 'base64'; } }); } data.normalizedHeaders = {}; Object.keys(data.headers || {}).forEach(key => { let value = [].concat(data.headers[key] || []).shift(); value = (value && value.value) || value; if (value) { if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) { value = this.message._encodeHeaderValue(key, value); } data.normalizedHeaders[key] = value; } }); if (data.list && typeof data.list === 'object') { let listHeaders = this._getListHeaders(data.list); listHeaders.forEach(entry => { data.normalizedHeaders[entry.key] = entry.value.map(val => (val && val.value) || val).join(', '); }); } if (data.references) { data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references); } if (data.inReplyTo) { data.normalizedHeaders['in-reply-to'] = this.message._encodeHeaderValue('in-reply-to', data.inReplyTo); } return callback(null, data); }); } setMailerHeader() { if (!this.message || !this.data.xMailer) { return; } this.message.setHeader('X-Mailer', this.data.xMailer); } setPriorityHeaders() { if (!this.message || !this.data.priority) { return; } switch ((this.data.priority || '').toString().toLowerCase()) { case 'high': this.message.setHeader('X-Priority', '1 (Highest)'); this.message.setHeader('X-MSMail-Priority', 'High'); this.message.setHeader('Importance', 'High'); break; case 'low': this.message.setHeader('X-Priority', '5 (Lowest)'); this.message.setHeader('X-MSMail-Priority', 'Low'); this.message.setHeader('Importance', 'Low'); break; default: // do not add anything, since all messages are 'Normal' by default } } setListHeaders() { if (!this.message || !this.data.list || typeof this.data.list !== 'object') { return; } // add optional List-* headers if (this.data.list && typeof this.data.list === 'object') { this._getListHeaders(this.data.list).forEach(listHeader => { listHeader.value.forEach(value => { this.message.addHeader(listHeader.key, value); }); }); } } _getListHeaders(listData) { // make sure an url looks like return Object.keys(listData).map(key => ({ key: 'list-' + key.toLowerCase().trim(), value: [].concat(listData[key] || []).map(value => ({ prepared: true, foldLines: true, value: [] .concat(value || []) .map(value => { if (typeof value === 'string') { value = { url: value }; } if (value && value.url) { if (key.toLowerCase().trim() === 'id') { // List-ID: "comment" let comment = value.comment || ''; if (mimeFuncs.isPlainText(comment)) { comment = '"' + comment + '"'; } else { comment = mimeFuncs.encodeWord(comment); } return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+\/{,2}/, ''); } // List-*: (comment) let comment = value.comment || ''; if (!mimeFuncs.isPlainText(comment)) { comment = mimeFuncs.encodeWord(comment); } return this._formatListUrl(value.url) + (value.comment ? ' (' + comment + ')' : ''); } return ''; }) .filter(value => value) .join(', ') })) })); } _formatListUrl(url) { url = url.replace(/[\s<]+|[\s>]+/g, ''); if (/^(https?|mailto|ftp):/.test(url)) { return '<' + url + '>'; } if (/^[^@]+@[^@]+$/.test(url)) { return ''; } return ''; } } module.exports = MailMessage;