You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
316 lines
11 KiB
316 lines
11 KiB
1 month ago
|
'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 <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;
|