mail-message.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. 'use strict';
  2. const shared = require('../shared');
  3. const MimeNode = require('../mime-node');
  4. const mimeFuncs = require('../mime-funcs');
  5. class MailMessage {
  6. constructor(mailer, data) {
  7. this.mailer = mailer;
  8. this.data = {};
  9. this.message = null;
  10. data = data || {};
  11. let options = mailer.options || {};
  12. let defaults = mailer._defaults || {};
  13. Object.keys(data).forEach(key => {
  14. this.data[key] = data[key];
  15. });
  16. this.data.headers = this.data.headers || {};
  17. // apply defaults
  18. Object.keys(defaults).forEach(key => {
  19. if (!(key in this.data)) {
  20. this.data[key] = defaults[key];
  21. } else if (key === 'headers') {
  22. // headers is a special case. Allow setting individual default headers
  23. Object.keys(defaults.headers).forEach(key => {
  24. if (!(key in this.data.headers)) {
  25. this.data.headers[key] = defaults.headers[key];
  26. }
  27. });
  28. }
  29. });
  30. // force specific keys from transporter options
  31. ['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey'].forEach(key => {
  32. if (key in options) {
  33. this.data[key] = options[key];
  34. }
  35. });
  36. }
  37. resolveContent(...args) {
  38. return shared.resolveContent(...args);
  39. }
  40. resolveAll(callback) {
  41. let keys = [[this.data, 'html'], [this.data, 'text'], [this.data, 'watchHtml'], [this.data, 'amp'], [this.data, 'icalEvent']];
  42. if (this.data.alternatives && this.data.alternatives.length) {
  43. this.data.alternatives.forEach((alternative, i) => {
  44. keys.push([this.data.alternatives, i]);
  45. });
  46. }
  47. if (this.data.attachments && this.data.attachments.length) {
  48. this.data.attachments.forEach((attachment, i) => {
  49. if (!attachment.filename) {
  50. attachment.filename =
  51. (attachment.path || attachment.href || '')
  52. .split('/')
  53. .pop()
  54. .split('?')
  55. .shift() || 'attachment-' + (i + 1);
  56. if (attachment.filename.indexOf('.') < 0) {
  57. attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
  58. }
  59. }
  60. if (!attachment.contentType) {
  61. attachment.contentType = mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
  62. }
  63. keys.push([this.data.attachments, i]);
  64. });
  65. }
  66. let mimeNode = new MimeNode();
  67. let addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo'];
  68. addressKeys.forEach(address => {
  69. let value;
  70. if (this.message) {
  71. value = [].concat(mimeNode._parseAddresses(this.message.getHeader(address === 'replyTo' ? 'reply-to' : address)) || []);
  72. } else if (this.data[address]) {
  73. value = [].concat(mimeNode._parseAddresses(this.data[address]) || []);
  74. }
  75. if (value && value.length) {
  76. this.data[address] = value;
  77. } else if (address in this.data) {
  78. this.data[address] = null;
  79. }
  80. });
  81. let singleKeys = ['from', 'sender', 'replyTo'];
  82. singleKeys.forEach(address => {
  83. if (this.data[address]) {
  84. this.data[address] = this.data[address].shift();
  85. }
  86. });
  87. let pos = 0;
  88. let resolveNext = () => {
  89. if (pos >= keys.length) {
  90. return callback(null, this.data);
  91. }
  92. let args = keys[pos++];
  93. if (!args[0] || !args[0][args[1]]) {
  94. return resolveNext();
  95. }
  96. shared.resolveContent(...args, (err, value) => {
  97. if (err) {
  98. return callback(err);
  99. }
  100. let node = {
  101. content: value
  102. };
  103. if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
  104. Object.keys(args[0][args[1]]).forEach(key => {
  105. if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) {
  106. node[key] = args[0][args[1]][key];
  107. }
  108. });
  109. }
  110. args[0][args[1]] = node;
  111. resolveNext();
  112. });
  113. };
  114. setImmediate(() => resolveNext());
  115. }
  116. normalize(callback) {
  117. let envelope = this.data.envelope || this.message.getEnvelope();
  118. let messageId = this.message.messageId();
  119. this.resolveAll((err, data) => {
  120. if (err) {
  121. return callback(err);
  122. }
  123. data.envelope = envelope;
  124. data.messageId = messageId;
  125. ['html', 'text', 'watchHtml', 'amp'].forEach(key => {
  126. if (data[key] && data[key].content) {
  127. if (typeof data[key].content === 'string') {
  128. data[key] = data[key].content;
  129. } else if (Buffer.isBuffer(data[key].content)) {
  130. data[key] = data[key].content.toString();
  131. }
  132. }
  133. });
  134. if (data.icalEvent && Buffer.isBuffer(data.icalEvent.content)) {
  135. data.icalEvent.content = data.icalEvent.content.toString('base64');
  136. data.icalEvent.encoding = 'base64';
  137. }
  138. if (data.alternatives && data.alternatives.length) {
  139. data.alternatives.forEach(alternative => {
  140. if (alternative && alternative.content && Buffer.isBuffer(alternative.content)) {
  141. alternative.content = alternative.content.toString('base64');
  142. alternative.encoding = 'base64';
  143. }
  144. });
  145. }
  146. if (data.attachments && data.attachments.length) {
  147. data.attachments.forEach(attachment => {
  148. if (attachment && attachment.content && Buffer.isBuffer(attachment.content)) {
  149. attachment.content = attachment.content.toString('base64');
  150. attachment.encoding = 'base64';
  151. }
  152. });
  153. }
  154. data.normalizedHeaders = {};
  155. Object.keys(data.headers || {}).forEach(key => {
  156. let value = [].concat(data.headers[key] || []).shift();
  157. value = (value && value.value) || value;
  158. if (value) {
  159. if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
  160. value = this.message._encodeHeaderValue(key, value);
  161. }
  162. data.normalizedHeaders[key] = value;
  163. }
  164. });
  165. if (data.list && typeof data.list === 'object') {
  166. let listHeaders = this._getListHeaders(data.list);
  167. listHeaders.forEach(entry => {
  168. data.normalizedHeaders[entry.key] = entry.value.map(val => (val && val.value) || val).join(', ');
  169. });
  170. }
  171. if (data.references) {
  172. data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references);
  173. }
  174. if (data.inReplyTo) {
  175. data.normalizedHeaders['in-reply-to'] = this.message._encodeHeaderValue('in-reply-to', data.inReplyTo);
  176. }
  177. return callback(null, data);
  178. });
  179. }
  180. setMailerHeader() {
  181. if (!this.message || !this.data.xMailer) {
  182. return;
  183. }
  184. this.message.setHeader('X-Mailer', this.data.xMailer);
  185. }
  186. setPriorityHeaders() {
  187. if (!this.message || !this.data.priority) {
  188. return;
  189. }
  190. switch ((this.data.priority || '').toString().toLowerCase()) {
  191. case 'high':
  192. this.message.setHeader('X-Priority', '1 (Highest)');
  193. this.message.setHeader('X-MSMail-Priority', 'High');
  194. this.message.setHeader('Importance', 'High');
  195. break;
  196. case 'low':
  197. this.message.setHeader('X-Priority', '5 (Lowest)');
  198. this.message.setHeader('X-MSMail-Priority', 'Low');
  199. this.message.setHeader('Importance', 'Low');
  200. break;
  201. default:
  202. // do not add anything, since all messages are 'Normal' by default
  203. }
  204. }
  205. setListHeaders() {
  206. if (!this.message || !this.data.list || typeof this.data.list !== 'object') {
  207. return;
  208. }
  209. // add optional List-* headers
  210. if (this.data.list && typeof this.data.list === 'object') {
  211. this._getListHeaders(this.data.list).forEach(listHeader => {
  212. listHeader.value.forEach(value => {
  213. this.message.addHeader(listHeader.key, value);
  214. });
  215. });
  216. }
  217. }
  218. _getListHeaders(listData) {
  219. // make sure an url looks like <protocol:url>
  220. return Object.keys(listData).map(key => ({
  221. key: 'list-' + key.toLowerCase().trim(),
  222. value: [].concat(listData[key] || []).map(value => ({
  223. prepared: true,
  224. foldLines: true,
  225. value: []
  226. .concat(value || [])
  227. .map(value => {
  228. if (typeof value === 'string') {
  229. value = {
  230. url: value
  231. };
  232. }
  233. if (value && value.url) {
  234. if (key.toLowerCase().trim() === 'id') {
  235. // List-ID: "comment" <domain>
  236. let comment = value.comment || '';
  237. if (mimeFuncs.isPlainText(comment)) {
  238. comment = '"' + comment + '"';
  239. } else {
  240. comment = mimeFuncs.encodeWord(comment);
  241. }
  242. return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+\/{,2}/, '');
  243. }
  244. // List-*: <http://domain> (comment)
  245. let comment = value.comment || '';
  246. if (!mimeFuncs.isPlainText(comment)) {
  247. comment = mimeFuncs.encodeWord(comment);
  248. }
  249. return this._formatListUrl(value.url) + (value.comment ? ' (' + comment + ')' : '');
  250. }
  251. return '';
  252. })
  253. .filter(value => value)
  254. .join(', ')
  255. }))
  256. }));
  257. }
  258. _formatListUrl(url) {
  259. url = url.replace(/[\s<]+|[\s>]+/g, '');
  260. if (/^(https?|mailto|ftp):/.test(url)) {
  261. return '<' + url + '>';
  262. }
  263. if (/^[^@]+@[^@]+$/.test(url)) {
  264. return '<mailto:' + url + '>';
  265. }
  266. return '<http://' + url + '>';
  267. }
  268. }
  269. module.exports = MailMessage;