source: node_modules/nodemailer/lib/mail-composer/index.js@ 81bc7da

finki-main main
Last change on this file since 81bc7da was 81bc7da, checked in by Klimentina Efremova <klimentina08642@…>, 3 months ago

Initial commit

  • Property mode set to 100644
File size: 19.9 KB
Line 
1/* eslint no-undefined: 0 */
2
3'use strict';
4
5const MimeNode = require('../mime-node');
6const mimeFuncs = require('../mime-funcs');
7const parseDataURI = require('../shared').parseDataURI;
8
9/**
10 * Creates the object for composing a MimeNode instance out from the mail options
11 *
12 * @constructor
13 * @param {Object} mail Mail options
14 */
15class MailComposer {
16 constructor(mail) {
17 this.mail = mail || {};
18 this.message = false;
19 }
20
21 /**
22 * Builds MimeNode instance
23 */
24 compile() {
25 this._alternatives = this.getAlternatives();
26 this._htmlNode = this._alternatives.filter(alternative => /^text\/html\b/i.test(alternative.contentType)).pop();
27 this._attachments = this.getAttachments(!!this._htmlNode);
28
29 this._useRelated = !!(this._htmlNode && this._attachments.related.length);
30 this._useAlternative = this._alternatives.length > 1;
31 this._useMixed = this._attachments.attached.length > 1 || (this._alternatives.length && this._attachments.attached.length === 1);
32
33 // Compose MIME tree
34 if (this.mail.raw) {
35 this.message = new MimeNode('message/rfc822', { newline: this.mail.newline }).setRaw(this.mail.raw);
36 } else if (this._useMixed) {
37 this.message = this._createMixed();
38 } else if (this._useAlternative) {
39 this.message = this._createAlternative();
40 } else if (this._useRelated) {
41 this.message = this._createRelated();
42 } else {
43 this.message = this._createContentNode(
44 false,
45 []
46 .concat(this._alternatives || [])
47 .concat(this._attachments.attached || [])
48 .shift() || {
49 contentType: 'text/plain',
50 content: ''
51 }
52 );
53 }
54
55 // Add custom headers
56 if (this.mail.headers) {
57 this.message.addHeader(this.mail.headers);
58 }
59
60 // Add headers to the root node, always overrides custom headers
61 ['from', 'sender', 'to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'references', 'subject', 'message-id', 'date'].forEach(header => {
62 let key = header.replace(/-(\w)/g, (o, c) => c.toUpperCase());
63 if (this.mail[key]) {
64 this.message.setHeader(header, this.mail[key]);
65 }
66 });
67
68 // Sets custom envelope
69 if (this.mail.envelope) {
70 this.message.setEnvelope(this.mail.envelope);
71 }
72
73 // ensure Message-Id value
74 this.message.messageId();
75
76 return this.message;
77 }
78
79 /**
80 * List all attachments. Resulting attachment objects can be used as input for MimeNode nodes
81 *
82 * @param {Boolean} findRelated If true separate related attachments from attached ones
83 * @returns {Object} An object of arrays (`related` and `attached`)
84 */
85 getAttachments(findRelated) {
86 let icalEvent, eventObject;
87 let attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
88 let data;
89 let isMessageNode = /^message\//i.test(attachment.contentType);
90
91 if (/^data:/i.test(attachment.path || attachment.href)) {
92 attachment = this._processDataUrl(attachment);
93 }
94
95 let contentType = attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
96 let isImage = /^image\//i.test(contentType);
97 let contentDisposition = attachment.contentDisposition || (isMessageNode || (isImage && attachment.cid) ? 'inline' : 'attachment');
98
99 data = {
100 contentType,
101 contentDisposition,
102 contentTransferEncoding: 'contentTransferEncoding' in attachment ? attachment.contentTransferEncoding : 'base64'
103 };
104
105 if (attachment.filename) {
106 data.filename = attachment.filename;
107 } else if (!isMessageNode && attachment.filename !== false) {
108 data.filename = (attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
109 if (data.filename.indexOf('.') < 0) {
110 data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
111 }
112 }
113
114 if (/^https?:\/\//i.test(attachment.path)) {
115 attachment.href = attachment.path;
116 attachment.path = undefined;
117 }
118
119 if (attachment.cid) {
120 data.cid = attachment.cid;
121 }
122
123 if (attachment.raw) {
124 data.raw = attachment.raw;
125 } else if (attachment.path) {
126 data.content = {
127 path: attachment.path
128 };
129 } else if (attachment.href) {
130 data.content = {
131 href: attachment.href,
132 httpHeaders: attachment.httpHeaders
133 };
134 } else {
135 data.content = attachment.content || '';
136 }
137
138 if (attachment.encoding) {
139 data.encoding = attachment.encoding;
140 }
141
142 if (attachment.headers) {
143 data.headers = attachment.headers;
144 }
145
146 return data;
147 });
148
149 if (this.mail.icalEvent) {
150 if (
151 typeof this.mail.icalEvent === 'object' &&
152 (this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
153 ) {
154 icalEvent = this.mail.icalEvent;
155 } else {
156 icalEvent = {
157 content: this.mail.icalEvent
158 };
159 }
160
161 eventObject = {};
162 Object.keys(icalEvent).forEach(key => {
163 eventObject[key] = icalEvent[key];
164 });
165
166 eventObject.contentType = 'application/ics';
167 if (!eventObject.headers) {
168 eventObject.headers = {};
169 }
170 eventObject.filename = eventObject.filename || 'invite.ics';
171 eventObject.headers['Content-Disposition'] = 'attachment';
172 eventObject.headers['Content-Transfer-Encoding'] = 'base64';
173 }
174
175 if (!findRelated) {
176 return {
177 attached: attachments.concat(eventObject || []),
178 related: []
179 };
180 } else {
181 return {
182 attached: attachments.filter(attachment => !attachment.cid).concat(eventObject || []),
183 related: attachments.filter(attachment => !!attachment.cid)
184 };
185 }
186 }
187
188 /**
189 * List alternatives. Resulting objects can be used as input for MimeNode nodes
190 *
191 * @returns {Array} An array of alternative elements. Includes the `text` and `html` values as well
192 */
193 getAlternatives() {
194 let alternatives = [],
195 text,
196 html,
197 watchHtml,
198 amp,
199 icalEvent,
200 eventObject;
201
202 if (this.mail.text) {
203 if (typeof this.mail.text === 'object' && (this.mail.text.content || this.mail.text.path || this.mail.text.href || this.mail.text.raw)) {
204 text = this.mail.text;
205 } else {
206 text = {
207 content: this.mail.text
208 };
209 }
210 text.contentType = 'text/plain; charset=utf-8';
211 }
212
213 if (this.mail.watchHtml) {
214 if (
215 typeof this.mail.watchHtml === 'object' &&
216 (this.mail.watchHtml.content || this.mail.watchHtml.path || this.mail.watchHtml.href || this.mail.watchHtml.raw)
217 ) {
218 watchHtml = this.mail.watchHtml;
219 } else {
220 watchHtml = {
221 content: this.mail.watchHtml
222 };
223 }
224 watchHtml.contentType = 'text/watch-html; charset=utf-8';
225 }
226
227 if (this.mail.amp) {
228 if (typeof this.mail.amp === 'object' && (this.mail.amp.content || this.mail.amp.path || this.mail.amp.href || this.mail.amp.raw)) {
229 amp = this.mail.amp;
230 } else {
231 amp = {
232 content: this.mail.amp
233 };
234 }
235 amp.contentType = 'text/x-amp-html; charset=utf-8';
236 }
237
238 // NB! when including attachments with a calendar alternative you might end up in a blank screen on some clients
239 if (this.mail.icalEvent) {
240 if (
241 typeof this.mail.icalEvent === 'object' &&
242 (this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
243 ) {
244 icalEvent = this.mail.icalEvent;
245 } else {
246 icalEvent = {
247 content: this.mail.icalEvent
248 };
249 }
250
251 eventObject = {};
252 Object.keys(icalEvent).forEach(key => {
253 eventObject[key] = icalEvent[key];
254 });
255
256 if (eventObject.content && typeof eventObject.content === 'object') {
257 // we are going to have the same attachment twice, so mark this to be
258 // resolved just once
259 eventObject.content._resolve = true;
260 }
261
262 eventObject.filename = false;
263 eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
264 if (!eventObject.headers) {
265 eventObject.headers = {};
266 }
267 }
268
269 if (this.mail.html) {
270 if (typeof this.mail.html === 'object' && (this.mail.html.content || this.mail.html.path || this.mail.html.href || this.mail.html.raw)) {
271 html = this.mail.html;
272 } else {
273 html = {
274 content: this.mail.html
275 };
276 }
277 html.contentType = 'text/html; charset=utf-8';
278 }
279
280 []
281 .concat(text || [])
282 .concat(watchHtml || [])
283 .concat(amp || [])
284 .concat(html || [])
285 .concat(eventObject || [])
286 .concat(this.mail.alternatives || [])
287 .forEach(alternative => {
288 let data;
289
290 if (/^data:/i.test(alternative.path || alternative.href)) {
291 alternative = this._processDataUrl(alternative);
292 }
293
294 data = {
295 contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
296 contentTransferEncoding: alternative.contentTransferEncoding
297 };
298
299 if (alternative.filename) {
300 data.filename = alternative.filename;
301 }
302
303 if (/^https?:\/\//i.test(alternative.path)) {
304 alternative.href = alternative.path;
305 alternative.path = undefined;
306 }
307
308 if (alternative.raw) {
309 data.raw = alternative.raw;
310 } else if (alternative.path) {
311 data.content = {
312 path: alternative.path
313 };
314 } else if (alternative.href) {
315 data.content = {
316 href: alternative.href
317 };
318 } else {
319 data.content = alternative.content || '';
320 }
321
322 if (alternative.encoding) {
323 data.encoding = alternative.encoding;
324 }
325
326 if (alternative.headers) {
327 data.headers = alternative.headers;
328 }
329
330 alternatives.push(data);
331 });
332
333 return alternatives;
334 }
335
336 /**
337 * Builds multipart/mixed node. It should always contain different type of elements on the same level
338 * eg. text + attachments
339 *
340 * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
341 * @returns {Object} MimeNode node element
342 */
343 _createMixed(parentNode) {
344 let node;
345
346 if (!parentNode) {
347 node = new MimeNode('multipart/mixed', {
348 baseBoundary: this.mail.baseBoundary,
349 textEncoding: this.mail.textEncoding,
350 boundaryPrefix: this.mail.boundaryPrefix,
351 disableUrlAccess: this.mail.disableUrlAccess,
352 disableFileAccess: this.mail.disableFileAccess,
353 normalizeHeaderKey: this.mail.normalizeHeaderKey,
354 newline: this.mail.newline
355 });
356 } else {
357 node = parentNode.createChild('multipart/mixed', {
358 disableUrlAccess: this.mail.disableUrlAccess,
359 disableFileAccess: this.mail.disableFileAccess,
360 normalizeHeaderKey: this.mail.normalizeHeaderKey,
361 newline: this.mail.newline
362 });
363 }
364
365 if (this._useAlternative) {
366 this._createAlternative(node);
367 } else if (this._useRelated) {
368 this._createRelated(node);
369 }
370
371 []
372 .concat((!this._useAlternative && this._alternatives) || [])
373 .concat(this._attachments.attached || [])
374 .forEach(element => {
375 // if the element is a html node from related subpart then ignore it
376 if (!this._useRelated || element !== this._htmlNode) {
377 this._createContentNode(node, element);
378 }
379 });
380
381 return node;
382 }
383
384 /**
385 * Builds multipart/alternative node. It should always contain same type of elements on the same level
386 * eg. text + html view of the same data
387 *
388 * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
389 * @returns {Object} MimeNode node element
390 */
391 _createAlternative(parentNode) {
392 let node;
393
394 if (!parentNode) {
395 node = new MimeNode('multipart/alternative', {
396 baseBoundary: this.mail.baseBoundary,
397 textEncoding: this.mail.textEncoding,
398 boundaryPrefix: this.mail.boundaryPrefix,
399 disableUrlAccess: this.mail.disableUrlAccess,
400 disableFileAccess: this.mail.disableFileAccess,
401 normalizeHeaderKey: this.mail.normalizeHeaderKey,
402 newline: this.mail.newline
403 });
404 } else {
405 node = parentNode.createChild('multipart/alternative', {
406 disableUrlAccess: this.mail.disableUrlAccess,
407 disableFileAccess: this.mail.disableFileAccess,
408 normalizeHeaderKey: this.mail.normalizeHeaderKey,
409 newline: this.mail.newline
410 });
411 }
412
413 this._alternatives.forEach(alternative => {
414 if (this._useRelated && this._htmlNode === alternative) {
415 this._createRelated(node);
416 } else {
417 this._createContentNode(node, alternative);
418 }
419 });
420
421 return node;
422 }
423
424 /**
425 * Builds multipart/related node. It should always contain html node with related attachments
426 *
427 * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
428 * @returns {Object} MimeNode node element
429 */
430 _createRelated(parentNode) {
431 let node;
432
433 if (!parentNode) {
434 node = new MimeNode('multipart/related; type="text/html"', {
435 baseBoundary: this.mail.baseBoundary,
436 textEncoding: this.mail.textEncoding,
437 boundaryPrefix: this.mail.boundaryPrefix,
438 disableUrlAccess: this.mail.disableUrlAccess,
439 disableFileAccess: this.mail.disableFileAccess,
440 normalizeHeaderKey: this.mail.normalizeHeaderKey,
441 newline: this.mail.newline
442 });
443 } else {
444 node = parentNode.createChild('multipart/related; type="text/html"', {
445 disableUrlAccess: this.mail.disableUrlAccess,
446 disableFileAccess: this.mail.disableFileAccess,
447 normalizeHeaderKey: this.mail.normalizeHeaderKey,
448 newline: this.mail.newline
449 });
450 }
451
452 this._createContentNode(node, this._htmlNode);
453
454 this._attachments.related.forEach(alternative => this._createContentNode(node, alternative));
455
456 return node;
457 }
458
459 /**
460 * Creates a regular node with contents
461 *
462 * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
463 * @param {Object} element Node data
464 * @returns {Object} MimeNode node element
465 */
466 _createContentNode(parentNode, element) {
467 element = element || {};
468 element.content = element.content || '';
469
470 let node;
471 let encoding = (element.encoding || 'utf8')
472 .toString()
473 .toLowerCase()
474 .replace(/[-_\s]/g, '');
475
476 if (!parentNode) {
477 node = new MimeNode(element.contentType, {
478 filename: element.filename,
479 baseBoundary: this.mail.baseBoundary,
480 textEncoding: this.mail.textEncoding,
481 boundaryPrefix: this.mail.boundaryPrefix,
482 disableUrlAccess: this.mail.disableUrlAccess,
483 disableFileAccess: this.mail.disableFileAccess,
484 normalizeHeaderKey: this.mail.normalizeHeaderKey,
485 newline: this.mail.newline
486 });
487 } else {
488 node = parentNode.createChild(element.contentType, {
489 filename: element.filename,
490 textEncoding: this.mail.textEncoding,
491 disableUrlAccess: this.mail.disableUrlAccess,
492 disableFileAccess: this.mail.disableFileAccess,
493 normalizeHeaderKey: this.mail.normalizeHeaderKey,
494 newline: this.mail.newline
495 });
496 }
497
498 // add custom headers
499 if (element.headers) {
500 node.addHeader(element.headers);
501 }
502
503 if (element.cid) {
504 node.setHeader('Content-Id', '<' + element.cid.replace(/[<>]/g, '') + '>');
505 }
506
507 if (element.contentTransferEncoding) {
508 node.setHeader('Content-Transfer-Encoding', element.contentTransferEncoding);
509 } else if (this.mail.encoding && /^text\//i.test(element.contentType)) {
510 node.setHeader('Content-Transfer-Encoding', this.mail.encoding);
511 }
512
513 if (!/^text\//i.test(element.contentType) || element.contentDisposition) {
514 node.setHeader(
515 'Content-Disposition',
516 element.contentDisposition || (element.cid && /^image\//i.test(element.contentType) ? 'inline' : 'attachment')
517 );
518 }
519
520 if (typeof element.content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
521 element.content = Buffer.from(element.content, encoding);
522 }
523
524 // prefer pregenerated raw content
525 if (element.raw) {
526 node.setRaw(element.raw);
527 } else {
528 node.setContent(element.content);
529 }
530
531 return node;
532 }
533
534 /**
535 * Parses data uri and converts it to a Buffer
536 *
537 * @param {Object} element Content element
538 * @return {Object} Parsed element
539 */
540 _processDataUrl(element) {
541 let parsedDataUri;
542 if ((element.path || element.href).match(/^data:/)) {
543 parsedDataUri = parseDataURI(element.path || element.href);
544 }
545
546 if (!parsedDataUri) {
547 return element;
548 }
549
550 element.content = parsedDataUri.data;
551 element.contentType = element.contentType || parsedDataUri.contentType;
552
553 if ('path' in element) {
554 element.path = false;
555 }
556
557 if ('href' in element) {
558 element.href = false;
559 }
560
561 return element;
562 }
563}
564
565module.exports = MailComposer;
Note: See TracBrowser for help on using the repository browser.