316 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
		
		
			
		
	
	
			316 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
|  | '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']; | ||
|  |         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 <protocol:url>
 | ||
|  |         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" <domain>
 | ||
|  |                                 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-*: <http://domain> (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 '<mailto:' + url + '>'; | ||
|  |         } | ||
|  | 
 | ||
|  |         return '<http://' + url + '>'; | ||
|  |     } | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = MailMessage; |