index.js 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225
  1. /* eslint no-undefined: 0, prefer-spread: 0 */
  2. 'use strict';
  3. const crypto = require('crypto');
  4. const os = require('os');
  5. const fs = require('fs');
  6. const punycode = require('punycode');
  7. const PassThrough = require('stream').PassThrough;
  8. const mimeFuncs = require('../mime-funcs');
  9. const qp = require('../qp');
  10. const base64 = require('../base64');
  11. const addressparser = require('../addressparser');
  12. const fetch = require('../fetch');
  13. const LastNewline = require('./last-newline');
  14. /**
  15. * Creates a new mime tree node. Assumes 'multipart/*' as the content type
  16. * if it is a branch, anything else counts as leaf. If rootNode is missing from
  17. * the options, assumes this is the root.
  18. *
  19. * @param {String} contentType Define the content type for the node. Can be left blank for attachments (derived from filename)
  20. * @param {Object} [options] optional options
  21. * @param {Object} [options.rootNode] root node for this tree
  22. * @param {Object} [options.parentNode] immediate parent for this node
  23. * @param {Object} [options.filename] filename for an attachment node
  24. * @param {String} [options.baseBoundary] shared part of the unique multipart boundary
  25. * @param {Boolean} [options.keepBcc] If true, do not exclude Bcc from the generated headers
  26. * @param {String} [options.textEncoding] either 'Q' (the default) or 'B'
  27. */
  28. class MimeNode {
  29. constructor(contentType, options) {
  30. this.nodeCounter = 0;
  31. options = options || {};
  32. /**
  33. * shared part of the unique multipart boundary
  34. */
  35. this.baseBoundary = options.baseBoundary || crypto.randomBytes(8).toString('hex');
  36. this.boundaryPrefix = options.boundaryPrefix || '--_NmP';
  37. this.disableFileAccess = !!options.disableFileAccess;
  38. this.disableUrlAccess = !!options.disableUrlAccess;
  39. /**
  40. * If date headers is missing and current node is the root, this value is used instead
  41. */
  42. this.date = new Date();
  43. /**
  44. * Root node for current mime tree
  45. */
  46. this.rootNode = options.rootNode || this;
  47. /**
  48. * If true include Bcc in generated headers (if available)
  49. */
  50. this.keepBcc = !!options.keepBcc;
  51. /**
  52. * If filename is specified but contentType is not (probably an attachment)
  53. * detect the content type from filename extension
  54. */
  55. if (options.filename) {
  56. /**
  57. * Filename for this node. Useful with attachments
  58. */
  59. this.filename = options.filename;
  60. if (!contentType) {
  61. contentType = mimeFuncs.detectMimeType(this.filename.split('.').pop());
  62. }
  63. }
  64. /**
  65. * Indicates which encoding should be used for header strings: "Q" or "B"
  66. */
  67. this.textEncoding = (options.textEncoding || '')
  68. .toString()
  69. .trim()
  70. .charAt(0)
  71. .toUpperCase();
  72. /**
  73. * Immediate parent for this node (or undefined if not set)
  74. */
  75. this.parentNode = options.parentNode;
  76. /**
  77. * Hostname for default message-id values
  78. */
  79. this.hostname = options.hostname;
  80. /**
  81. * An array for possible child nodes
  82. */
  83. this.childNodes = [];
  84. /**
  85. * Used for generating unique boundaries (prepended to the shared base)
  86. */
  87. this._nodeId = ++this.rootNode.nodeCounter;
  88. /**
  89. * A list of header values for this node in the form of [{key:'', value:''}]
  90. */
  91. this._headers = [];
  92. /**
  93. * True if the content only uses ASCII printable characters
  94. * @type {Boolean}
  95. */
  96. this._isPlainText = false;
  97. /**
  98. * True if the content is plain text but has longer lines than allowed
  99. * @type {Boolean}
  100. */
  101. this._hasLongLines = false;
  102. /**
  103. * If set, use instead this value for envelopes instead of generating one
  104. * @type {Boolean}
  105. */
  106. this._envelope = false;
  107. /**
  108. * If set then use this value as the stream content instead of building it
  109. * @type {String|Buffer|Stream}
  110. */
  111. this._raw = false;
  112. /**
  113. * Additional transform streams that the message will be piped before
  114. * exposing by createReadStream
  115. * @type {Array}
  116. */
  117. this._transforms = [];
  118. /**
  119. * Additional process functions that the message will be piped through before
  120. * exposing by createReadStream. These functions are run after transforms
  121. * @type {Array}
  122. */
  123. this._processFuncs = [];
  124. /**
  125. * If content type is set (or derived from the filename) add it to headers
  126. */
  127. if (contentType) {
  128. this.setHeader('Content-Type', contentType);
  129. }
  130. }
  131. /////// PUBLIC METHODS
  132. /**
  133. * Creates and appends a child node.Arguments provided are passed to MimeNode constructor
  134. *
  135. * @param {String} [contentType] Optional content type
  136. * @param {Object} [options] Optional options object
  137. * @return {Object} Created node object
  138. */
  139. createChild(contentType, options) {
  140. if (!options && typeof contentType === 'object') {
  141. options = contentType;
  142. contentType = undefined;
  143. }
  144. let node = new MimeNode(contentType, options);
  145. this.appendChild(node);
  146. return node;
  147. }
  148. /**
  149. * Appends an existing node to the mime tree. Removes the node from an existing
  150. * tree if needed
  151. *
  152. * @param {Object} childNode node to be appended
  153. * @return {Object} Appended node object
  154. */
  155. appendChild(childNode) {
  156. if (childNode.rootNode !== this.rootNode) {
  157. childNode.rootNode = this.rootNode;
  158. childNode._nodeId = ++this.rootNode.nodeCounter;
  159. }
  160. childNode.parentNode = this;
  161. this.childNodes.push(childNode);
  162. return childNode;
  163. }
  164. /**
  165. * Replaces current node with another node
  166. *
  167. * @param {Object} node Replacement node
  168. * @return {Object} Replacement node
  169. */
  170. replace(node) {
  171. if (node === this) {
  172. return this;
  173. }
  174. this.parentNode.childNodes.forEach((childNode, i) => {
  175. if (childNode === this) {
  176. node.rootNode = this.rootNode;
  177. node.parentNode = this.parentNode;
  178. node._nodeId = this._nodeId;
  179. this.rootNode = this;
  180. this.parentNode = undefined;
  181. node.parentNode.childNodes[i] = node;
  182. }
  183. });
  184. return node;
  185. }
  186. /**
  187. * Removes current node from the mime tree
  188. *
  189. * @return {Object} removed node
  190. */
  191. remove() {
  192. if (!this.parentNode) {
  193. return this;
  194. }
  195. for (let i = this.parentNode.childNodes.length - 1; i >= 0; i--) {
  196. if (this.parentNode.childNodes[i] === this) {
  197. this.parentNode.childNodes.splice(i, 1);
  198. this.parentNode = undefined;
  199. this.rootNode = this;
  200. return this;
  201. }
  202. }
  203. }
  204. /**
  205. * Sets a header value. If the value for selected key exists, it is overwritten.
  206. * You can set multiple values as well by using [{key:'', value:''}] or
  207. * {key: 'value'} as the first argument.
  208. *
  209. * @param {String|Array|Object} key Header key or a list of key value pairs
  210. * @param {String} value Header value
  211. * @return {Object} current node
  212. */
  213. setHeader(key, value) {
  214. let added = false,
  215. headerValue;
  216. // Allow setting multiple headers at once
  217. if (!value && key && typeof key === 'object') {
  218. // allow {key:'content-type', value: 'text/plain'}
  219. if (key.key && 'value' in key) {
  220. this.setHeader(key.key, key.value);
  221. } else if (Array.isArray(key)) {
  222. // allow [{key:'content-type', value: 'text/plain'}]
  223. key.forEach(i => {
  224. this.setHeader(i.key, i.value);
  225. });
  226. } else {
  227. // allow {'content-type': 'text/plain'}
  228. Object.keys(key).forEach(i => {
  229. this.setHeader(i, key[i]);
  230. });
  231. }
  232. return this;
  233. }
  234. key = this._normalizeHeaderKey(key);
  235. headerValue = {
  236. key,
  237. value
  238. };
  239. // Check if the value exists and overwrite
  240. for (let i = 0, len = this._headers.length; i < len; i++) {
  241. if (this._headers[i].key === key) {
  242. if (!added) {
  243. // replace the first match
  244. this._headers[i] = headerValue;
  245. added = true;
  246. } else {
  247. // remove following matches
  248. this._headers.splice(i, 1);
  249. i--;
  250. len--;
  251. }
  252. }
  253. }
  254. // match not found, append the value
  255. if (!added) {
  256. this._headers.push(headerValue);
  257. }
  258. return this;
  259. }
  260. /**
  261. * Adds a header value. If the value for selected key exists, the value is appended
  262. * as a new field and old one is not touched.
  263. * You can set multiple values as well by using [{key:'', value:''}] or
  264. * {key: 'value'} as the first argument.
  265. *
  266. * @param {String|Array|Object} key Header key or a list of key value pairs
  267. * @param {String} value Header value
  268. * @return {Object} current node
  269. */
  270. addHeader(key, value) {
  271. // Allow setting multiple headers at once
  272. if (!value && key && typeof key === 'object') {
  273. // allow {key:'content-type', value: 'text/plain'}
  274. if (key.key && key.value) {
  275. this.addHeader(key.key, key.value);
  276. } else if (Array.isArray(key)) {
  277. // allow [{key:'content-type', value: 'text/plain'}]
  278. key.forEach(i => {
  279. this.addHeader(i.key, i.value);
  280. });
  281. } else {
  282. // allow {'content-type': 'text/plain'}
  283. Object.keys(key).forEach(i => {
  284. this.addHeader(i, key[i]);
  285. });
  286. }
  287. return this;
  288. } else if (Array.isArray(value)) {
  289. value.forEach(val => {
  290. this.addHeader(key, val);
  291. });
  292. return this;
  293. }
  294. this._headers.push({
  295. key: this._normalizeHeaderKey(key),
  296. value
  297. });
  298. return this;
  299. }
  300. /**
  301. * Retrieves the first mathcing value of a selected key
  302. *
  303. * @param {String} key Key to search for
  304. * @retun {String} Value for the key
  305. */
  306. getHeader(key) {
  307. key = this._normalizeHeaderKey(key);
  308. for (let i = 0, len = this._headers.length; i < len; i++) {
  309. if (this._headers[i].key === key) {
  310. return this._headers[i].value;
  311. }
  312. }
  313. }
  314. /**
  315. * Sets body content for current node. If the value is a string, charset is added automatically
  316. * to Content-Type (if it is text/*). If the value is a Buffer, you need to specify
  317. * the charset yourself
  318. *
  319. * @param (String|Buffer) content Body content
  320. * @return {Object} current node
  321. */
  322. setContent(content) {
  323. this.content = content;
  324. if (typeof this.content.pipe === 'function') {
  325. // pre-stream handler. might be triggered if a stream is set as content
  326. // and 'error' fires before anything is done with this stream
  327. this._contentErrorHandler = err => {
  328. this.content.removeListener('error', this._contentErrorHandler);
  329. this.content = err;
  330. };
  331. this.content.once('error', this._contentErrorHandler);
  332. } else if (typeof this.content === 'string') {
  333. this._isPlainText = mimeFuncs.isPlainText(this.content);
  334. if (this._isPlainText && mimeFuncs.hasLongerLines(this.content, 76)) {
  335. // If there are lines longer than 76 symbols/bytes do not use 7bit
  336. this._hasLongLines = true;
  337. }
  338. }
  339. return this;
  340. }
  341. build(callback) {
  342. let stream = this.createReadStream();
  343. let buf = [];
  344. let buflen = 0;
  345. let returned = false;
  346. stream.on('readable', () => {
  347. let chunk;
  348. while ((chunk = stream.read()) !== null) {
  349. buf.push(chunk);
  350. buflen += chunk.length;
  351. }
  352. });
  353. stream.once('error', err => {
  354. if (returned) {
  355. return;
  356. }
  357. returned = true;
  358. return callback(err);
  359. });
  360. stream.once('end', chunk => {
  361. if (returned) {
  362. return;
  363. }
  364. returned = true;
  365. if (chunk && chunk.length) {
  366. buf.push(chunk);
  367. buflen += chunk.length;
  368. }
  369. return callback(null, Buffer.concat(buf, buflen));
  370. });
  371. }
  372. getTransferEncoding() {
  373. let transferEncoding = false;
  374. let contentType = (this.getHeader('Content-Type') || '')
  375. .toString()
  376. .toLowerCase()
  377. .trim();
  378. if (this.content) {
  379. transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '')
  380. .toString()
  381. .toLowerCase()
  382. .trim();
  383. if (!transferEncoding || !['base64', 'quoted-printable'].includes(transferEncoding)) {
  384. if (/^text\//i.test(contentType)) {
  385. // If there are no special symbols, no need to modify the text
  386. if (this._isPlainText && !this._hasLongLines) {
  387. transferEncoding = '7bit';
  388. } else if (typeof this.content === 'string' || this.content instanceof Buffer) {
  389. // detect preferred encoding for string value
  390. transferEncoding = this._getTextEncoding(this.content) === 'Q' ? 'quoted-printable' : 'base64';
  391. } else {
  392. // we can not check content for a stream, so either use preferred encoding or fallback to QP
  393. transferEncoding = this.transferEncoding === 'B' ? 'base64' : 'quoted-printable';
  394. }
  395. } else if (!/^(multipart|message)\//i.test(contentType)) {
  396. transferEncoding = transferEncoding || 'base64';
  397. }
  398. }
  399. }
  400. return transferEncoding;
  401. }
  402. /**
  403. * Builds the header block for the mime node. Append \r\n\r\n before writing the content
  404. *
  405. * @returns {String} Headers
  406. */
  407. buildHeaders() {
  408. let transferEncoding = this.getTransferEncoding();
  409. let headers = [];
  410. if (transferEncoding) {
  411. this.setHeader('Content-Transfer-Encoding', transferEncoding);
  412. }
  413. if (this.filename && !this.getHeader('Content-Disposition')) {
  414. this.setHeader('Content-Disposition', 'attachment');
  415. }
  416. // Ensure mandatory header fields
  417. if (this.rootNode === this) {
  418. if (!this.getHeader('Date')) {
  419. this.setHeader('Date', this.date.toUTCString().replace(/GMT/, '+0000'));
  420. }
  421. // ensure that Message-Id is present
  422. this.messageId();
  423. if (!this.getHeader('MIME-Version')) {
  424. this.setHeader('MIME-Version', '1.0');
  425. }
  426. }
  427. this._headers.forEach(header => {
  428. let key = header.key;
  429. let value = header.value;
  430. let structured;
  431. let param;
  432. let options = {};
  433. let formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
  434. if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
  435. Object.keys(value).forEach(key => {
  436. if (key !== 'value') {
  437. options[key] = value[key];
  438. }
  439. });
  440. value = (value.value || '').toString();
  441. if (!value.trim()) {
  442. return;
  443. }
  444. }
  445. if (options.prepared) {
  446. // header value is
  447. headers.push(key + ': ' + value);
  448. return;
  449. }
  450. switch (header.key) {
  451. case 'Content-Disposition':
  452. structured = mimeFuncs.parseHeaderValue(value);
  453. if (this.filename) {
  454. structured.params.filename = this.filename;
  455. }
  456. value = mimeFuncs.buildHeaderValue(structured);
  457. break;
  458. case 'Content-Type':
  459. structured = mimeFuncs.parseHeaderValue(value);
  460. this._handleContentType(structured);
  461. if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
  462. structured.params.charset = 'utf-8';
  463. }
  464. value = mimeFuncs.buildHeaderValue(structured);
  465. if (this.filename) {
  466. // add support for non-compliant clients like QQ webmail
  467. // we can't build the value with buildHeaderValue as the value is non standard and
  468. // would be converted to parameter continuation encoding that we do not want
  469. param = this._encodeWords(this.filename);
  470. if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
  471. // include value in quotes if needed
  472. param = '"' + param + '"';
  473. }
  474. value += '; name=' + param;
  475. }
  476. break;
  477. case 'Bcc':
  478. if (!this.keepBcc) {
  479. // skip BCC values
  480. return;
  481. }
  482. break;
  483. }
  484. value = this._encodeHeaderValue(key, value);
  485. // skip empty lines
  486. if (!(value || '').toString().trim()) {
  487. return;
  488. }
  489. headers.push(mimeFuncs.foldLines(key + ': ' + value, 76));
  490. });
  491. return headers.join('\r\n');
  492. }
  493. /**
  494. * Streams the rfc2822 message from the current node. If this is a root node,
  495. * mandatory header fields are set if missing (Date, Message-Id, MIME-Version)
  496. *
  497. * @return {String} Compiled message
  498. */
  499. createReadStream(options) {
  500. options = options || {};
  501. let stream = new PassThrough(options);
  502. let outputStream = stream;
  503. let transform;
  504. this.stream(stream, options, err => {
  505. if (err) {
  506. outputStream.emit('error', err);
  507. return;
  508. }
  509. stream.end();
  510. });
  511. for (let i = 0, len = this._transforms.length; i < len; i++) {
  512. transform = typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
  513. outputStream.once('error', err => {
  514. transform.emit('error', err);
  515. });
  516. outputStream = outputStream.pipe(transform);
  517. }
  518. // ensure terminating newline after possible user transforms
  519. transform = new LastNewline();
  520. outputStream.once('error', err => {
  521. transform.emit('error', err);
  522. });
  523. outputStream = outputStream.pipe(transform);
  524. // dkim and stuff
  525. for (let i = 0, len = this._processFuncs.length; i < len; i++) {
  526. transform = this._processFuncs[i];
  527. outputStream = transform(outputStream);
  528. }
  529. return outputStream;
  530. }
  531. /**
  532. * Appends a transform stream object to the transforms list. Final output
  533. * is passed through this stream before exposing
  534. *
  535. * @param {Object} transform Read-Write stream
  536. */
  537. transform(transform) {
  538. this._transforms.push(transform);
  539. }
  540. /**
  541. * Appends a post process function. The functon is run after transforms and
  542. * uses the following syntax
  543. *
  544. * processFunc(input) -> outputStream
  545. *
  546. * @param {Object} processFunc Read-Write stream
  547. */
  548. processFunc(processFunc) {
  549. this._processFuncs.push(processFunc);
  550. }
  551. stream(outputStream, options, done) {
  552. let transferEncoding = this.getTransferEncoding();
  553. let contentStream;
  554. let localStream;
  555. // protect actual callback against multiple triggering
  556. let returned = false;
  557. let callback = err => {
  558. if (returned) {
  559. return;
  560. }
  561. returned = true;
  562. done(err);
  563. };
  564. // for multipart nodes, push child nodes
  565. // for content nodes end the stream
  566. let finalize = () => {
  567. let childId = 0;
  568. let processChildNode = () => {
  569. if (childId >= this.childNodes.length) {
  570. outputStream.write('\r\n--' + this.boundary + '--\r\n');
  571. return callback();
  572. }
  573. let child = this.childNodes[childId++];
  574. outputStream.write((childId > 1 ? '\r\n' : '') + '--' + this.boundary + '\r\n');
  575. child.stream(outputStream, options, err => {
  576. if (err) {
  577. return callback(err);
  578. }
  579. setImmediate(processChildNode);
  580. });
  581. };
  582. if (this.multipart) {
  583. setImmediate(processChildNode);
  584. } else {
  585. return callback();
  586. }
  587. };
  588. // pushes node content
  589. let sendContent = () => {
  590. if (this.content) {
  591. if (Object.prototype.toString.call(this.content) === '[object Error]') {
  592. // content is already errored
  593. return callback(this.content);
  594. }
  595. if (typeof this.content.pipe === 'function') {
  596. this.content.removeListener('error', this._contentErrorHandler);
  597. this._contentErrorHandler = err => callback(err);
  598. this.content.once('error', this._contentErrorHandler);
  599. }
  600. let createStream = () => {
  601. if (['quoted-printable', 'base64'].includes(transferEncoding)) {
  602. contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
  603. contentStream.pipe(outputStream, {
  604. end: false
  605. });
  606. contentStream.once('end', finalize);
  607. contentStream.once('error', err => callback(err));
  608. localStream = this._getStream(this.content);
  609. localStream.pipe(contentStream);
  610. } else {
  611. // anything that is not QP or Base54 passes as-is
  612. localStream = this._getStream(this.content);
  613. localStream.pipe(outputStream, {
  614. end: false
  615. });
  616. localStream.once('end', finalize);
  617. }
  618. localStream.once('error', err => callback(err));
  619. };
  620. if (this.content._resolve) {
  621. let chunks = [];
  622. let chunklen = 0;
  623. let returned = false;
  624. let sourceStream = this._getStream(this.content);
  625. sourceStream.on('error', err => {
  626. if (returned) {
  627. return;
  628. }
  629. returned = true;
  630. callback(err);
  631. });
  632. sourceStream.on('readable', () => {
  633. let chunk;
  634. while ((chunk = sourceStream.read()) !== null) {
  635. chunks.push(chunk);
  636. chunklen += chunk.length;
  637. }
  638. });
  639. sourceStream.on('end', () => {
  640. if (returned) {
  641. return;
  642. }
  643. returned = true;
  644. this.content._resolve = false;
  645. this.content._resolvedValue = Buffer.concat(chunks, chunklen);
  646. setImmediate(createStream);
  647. });
  648. } else {
  649. setImmediate(createStream);
  650. }
  651. return;
  652. } else {
  653. return setImmediate(finalize);
  654. }
  655. };
  656. if (this._raw) {
  657. setImmediate(() => {
  658. if (Object.prototype.toString.call(this._raw) === '[object Error]') {
  659. // content is already errored
  660. return callback(this._raw);
  661. }
  662. // remove default error handler (if set)
  663. if (typeof this._raw.pipe === 'function') {
  664. this._raw.removeListener('error', this._contentErrorHandler);
  665. }
  666. let raw = this._getStream(this._raw);
  667. raw.pipe(outputStream, {
  668. end: false
  669. });
  670. raw.on('error', err => outputStream.emit('error', err));
  671. raw.on('end', finalize);
  672. });
  673. } else {
  674. outputStream.write(this.buildHeaders() + '\r\n\r\n');
  675. setImmediate(sendContent);
  676. }
  677. }
  678. /**
  679. * Sets envelope to be used instead of the generated one
  680. *
  681. * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
  682. */
  683. setEnvelope(envelope) {
  684. let list;
  685. this._envelope = {
  686. from: false,
  687. to: []
  688. };
  689. if (envelope.from) {
  690. list = [];
  691. this._convertAddresses(this._parseAddresses(envelope.from), list);
  692. list = list.filter(address => address && address.address);
  693. if (list.length && list[0]) {
  694. this._envelope.from = list[0].address;
  695. }
  696. }
  697. ['to', 'cc', 'bcc'].forEach(key => {
  698. if (envelope[key]) {
  699. this._convertAddresses(this._parseAddresses(envelope[key]), this._envelope.to);
  700. }
  701. });
  702. this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address);
  703. let standardFields = ['to', 'cc', 'bcc', 'from'];
  704. Object.keys(envelope).forEach(key => {
  705. if (!standardFields.includes(key)) {
  706. this._envelope[key] = envelope[key];
  707. }
  708. });
  709. return this;
  710. }
  711. /**
  712. * Generates and returns an object with parsed address fields
  713. *
  714. * @return {Object} Address object
  715. */
  716. getAddresses() {
  717. let addresses = {};
  718. this._headers.forEach(header => {
  719. let key = header.key.toLowerCase();
  720. if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
  721. if (!Array.isArray(addresses[key])) {
  722. addresses[key] = [];
  723. }
  724. this._convertAddresses(this._parseAddresses(header.value), addresses[key]);
  725. }
  726. });
  727. return addresses;
  728. }
  729. /**
  730. * Generates and returns SMTP envelope with the sender address and a list of recipients addresses
  731. *
  732. * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
  733. */
  734. getEnvelope() {
  735. if (this._envelope) {
  736. return this._envelope;
  737. }
  738. let envelope = {
  739. from: false,
  740. to: []
  741. };
  742. this._headers.forEach(header => {
  743. let list = [];
  744. if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
  745. this._convertAddresses(this._parseAddresses(header.value), list);
  746. if (list.length && list[0]) {
  747. envelope.from = list[0].address;
  748. }
  749. } else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
  750. this._convertAddresses(this._parseAddresses(header.value), envelope.to);
  751. }
  752. });
  753. envelope.to = envelope.to.map(to => to.address);
  754. return envelope;
  755. }
  756. /**
  757. * Returns Message-Id value. If it does not exist, then creates one
  758. *
  759. * @return {String} Message-Id value
  760. */
  761. messageId() {
  762. let messageId = this.getHeader('Message-ID');
  763. // You really should define your own Message-Id field!
  764. if (!messageId) {
  765. messageId = this._generateMessageId();
  766. this.setHeader('Message-ID', messageId);
  767. }
  768. return messageId;
  769. }
  770. /**
  771. * Sets pregenerated content that will be used as the output of this node
  772. *
  773. * @param {String|Buffer|Stream} Raw MIME contents
  774. */
  775. setRaw(raw) {
  776. this._raw = raw;
  777. if (this._raw && typeof this._raw.pipe === 'function') {
  778. // pre-stream handler. might be triggered if a stream is set as content
  779. // and 'error' fires before anything is done with this stream
  780. this._contentErrorHandler = err => {
  781. this._raw.removeListener('error', this._contentErrorHandler);
  782. this._raw = err;
  783. };
  784. this._raw.once('error', this._contentErrorHandler);
  785. }
  786. return this;
  787. }
  788. /////// PRIVATE METHODS
  789. /**
  790. * Detects and returns handle to a stream related with the content.
  791. *
  792. * @param {Mixed} content Node content
  793. * @returns {Object} Stream object
  794. */
  795. _getStream(content) {
  796. let contentStream;
  797. if (content._resolvedValue) {
  798. // pass string or buffer content as a stream
  799. contentStream = new PassThrough();
  800. setImmediate(() => contentStream.end(content._resolvedValue));
  801. return contentStream;
  802. } else if (typeof content.pipe === 'function') {
  803. // assume as stream
  804. return content;
  805. } else if (content && typeof content.path === 'string' && !content.href) {
  806. if (this.disableFileAccess) {
  807. contentStream = new PassThrough();
  808. setImmediate(() => contentStream.emit('error', new Error('File access rejected for ' + content.path)));
  809. return contentStream;
  810. }
  811. // read file
  812. return fs.createReadStream(content.path);
  813. } else if (content && typeof content.href === 'string') {
  814. if (this.disableUrlAccess) {
  815. contentStream = new PassThrough();
  816. setImmediate(() => contentStream.emit('error', new Error('Url access rejected for ' + content.href)));
  817. return contentStream;
  818. }
  819. // fetch URL
  820. return fetch(content.href);
  821. } else {
  822. // pass string or buffer content as a stream
  823. contentStream = new PassThrough();
  824. setImmediate(() => contentStream.end(content || ''));
  825. return contentStream;
  826. }
  827. }
  828. /**
  829. * Parses addresses. Takes in a single address or an array or an
  830. * array of address arrays (eg. To: [[first group], [second group],...])
  831. *
  832. * @param {Mixed} addresses Addresses to be parsed
  833. * @return {Array} An array of address objects
  834. */
  835. _parseAddresses(addresses) {
  836. return [].concat.apply(
  837. [],
  838. [].concat(addresses).map(address => {
  839. // eslint-disable-line prefer-spread
  840. if (address && address.address) {
  841. address.address = this._normalizeAddress(address.address);
  842. address.name = address.name || '';
  843. return [address];
  844. }
  845. return addressparser(address);
  846. })
  847. );
  848. }
  849. /**
  850. * Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
  851. *
  852. * @param {String} key Key to be normalized
  853. * @return {String} key in Camel-Case form
  854. */
  855. _normalizeHeaderKey(key) {
  856. return (
  857. (key || '')
  858. .toString()
  859. // no newlines in keys
  860. .replace(/\r?\n|\r/g, ' ')
  861. .trim()
  862. .toLowerCase()
  863. // use uppercase words, except MIME
  864. .replace(/^X-SMTPAPI$|^(MIME|DKIM)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
  865. // special case
  866. .replace(/^Content-Features$/i, 'Content-features')
  867. );
  868. }
  869. /**
  870. * Checks if the content type is multipart and defines boundary if needed.
  871. * Doesn't return anything, modifies object argument instead.
  872. *
  873. * @param {Object} structured Parsed header value for 'Content-Type' key
  874. */
  875. _handleContentType(structured) {
  876. this.contentType = structured.value.trim().toLowerCase();
  877. this.multipart = this.contentType.split('/').reduce((prev, value) => (prev === 'multipart' ? value : false));
  878. if (this.multipart) {
  879. this.boundary = structured.params.boundary = structured.params.boundary || this.boundary || this._generateBoundary();
  880. } else {
  881. this.boundary = false;
  882. }
  883. }
  884. /**
  885. * Generates a multipart boundary value
  886. *
  887. * @return {String} boundary value
  888. */
  889. _generateBoundary() {
  890. return this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary + '-Part_' + this._nodeId;
  891. }
  892. /**
  893. * Encodes a header value for use in the generated rfc2822 email.
  894. *
  895. * @param {String} key Header key
  896. * @param {String} value Header value
  897. */
  898. _encodeHeaderValue(key, value) {
  899. key = this._normalizeHeaderKey(key);
  900. switch (key) {
  901. // Structured headers
  902. case 'From':
  903. case 'Sender':
  904. case 'To':
  905. case 'Cc':
  906. case 'Bcc':
  907. case 'Reply-To':
  908. return this._convertAddresses(this._parseAddresses(value));
  909. // values enclosed in <>
  910. case 'Message-ID':
  911. case 'In-Reply-To':
  912. case 'Content-Id':
  913. value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
  914. if (value.charAt(0) !== '<') {
  915. value = '<' + value;
  916. }
  917. if (value.charAt(value.length - 1) !== '>') {
  918. value = value + '>';
  919. }
  920. return value;
  921. // space separated list of values enclosed in <>
  922. case 'References':
  923. value = [].concat
  924. .apply(
  925. [],
  926. [].concat(value || '').map(elm => {
  927. // eslint-disable-line prefer-spread
  928. elm = (elm || '')
  929. .toString()
  930. .replace(/\r?\n|\r/g, ' ')
  931. .trim();
  932. return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
  933. })
  934. )
  935. .map(elm => {
  936. if (elm.charAt(0) !== '<') {
  937. elm = '<' + elm;
  938. }
  939. if (elm.charAt(elm.length - 1) !== '>') {
  940. elm = elm + '>';
  941. }
  942. return elm;
  943. });
  944. return value.join(' ').trim();
  945. case 'Date':
  946. if (Object.prototype.toString.call(value) === '[object Date]') {
  947. return value.toUTCString().replace(/GMT/, '+0000');
  948. }
  949. value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
  950. return this._encodeWords(value);
  951. default:
  952. value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
  953. // encodeWords only encodes if needed, otherwise the original string is returned
  954. return this._encodeWords(value);
  955. }
  956. }
  957. /**
  958. * Rebuilds address object using punycode and other adjustments
  959. *
  960. * @param {Array} addresses An array of address objects
  961. * @param {Array} [uniqueList] An array to be populated with addresses
  962. * @return {String} address string
  963. */
  964. _convertAddresses(addresses, uniqueList) {
  965. let values = [];
  966. uniqueList = uniqueList || [];
  967. [].concat(addresses || []).forEach(address => {
  968. if (address.address) {
  969. address.address = this._normalizeAddress(address.address);
  970. if (!address.name) {
  971. values.push(address.address);
  972. } else if (address.name) {
  973. values.push(this._encodeAddressName(address.name) + ' <' + address.address + '>');
  974. }
  975. if (address.address) {
  976. if (!uniqueList.filter(a => a.address === address.address).length) {
  977. uniqueList.push(address);
  978. }
  979. }
  980. } else if (address.group) {
  981. values.push(
  982. this._encodeAddressName(address.name) + ':' + (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim() + ';'
  983. );
  984. }
  985. });
  986. return values.join(', ');
  987. }
  988. /**
  989. * Normalizes an email address
  990. *
  991. * @param {Array} address An array of address objects
  992. * @return {String} address string
  993. */
  994. _normalizeAddress(address) {
  995. address = (address || '').toString().trim();
  996. let lastAt = address.lastIndexOf('@');
  997. let user = address.substr(0, lastAt);
  998. let domain = address.substr(lastAt + 1);
  999. // Usernames are not touched and are kept as is even if these include unicode
  1000. // Domains are punycoded by default
  1001. // 'jõgeva.ee' will be converted to 'xn--jgeva-dua.ee'
  1002. // non-unicode domains are left as is
  1003. return user + '@' + punycode.toASCII(domain.toLowerCase());
  1004. }
  1005. /**
  1006. * If needed, mime encodes the name part
  1007. *
  1008. * @param {String} name Name part of an address
  1009. * @returns {String} Mime word encoded string if needed
  1010. */
  1011. _encodeAddressName(name) {
  1012. if (!/^[\w ']*$/.test(name)) {
  1013. if (/^[\x20-\x7e]*$/.test(name)) {
  1014. return '"' + name.replace(/([\\"])/g, '\\$1') + '"';
  1015. } else {
  1016. return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
  1017. }
  1018. }
  1019. return name;
  1020. }
  1021. /**
  1022. * If needed, mime encodes the name part
  1023. *
  1024. * @param {String} name Name part of an address
  1025. * @returns {String} Mime word encoded string if needed
  1026. */
  1027. _encodeWords(value) {
  1028. return mimeFuncs.encodeWords(value, this._getTextEncoding(value), 52);
  1029. }
  1030. /**
  1031. * Detects best mime encoding for a text value
  1032. *
  1033. * @param {String} value Value to check for
  1034. * @return {String} either 'Q' or 'B'
  1035. */
  1036. _getTextEncoding(value) {
  1037. value = (value || '').toString();
  1038. let encoding = this.textEncoding;
  1039. let latinLen;
  1040. let nonLatinLen;
  1041. if (!encoding) {
  1042. // count latin alphabet symbols and 8-bit range symbols + control symbols
  1043. // if there are more latin characters, then use quoted-printable
  1044. // encoding, otherwise use base64
  1045. nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length; // eslint-disable-line no-control-regex
  1046. latinLen = (value.match(/[a-z]/gi) || []).length;
  1047. // if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
  1048. encoding = nonLatinLen < latinLen ? 'Q' : 'B';
  1049. }
  1050. return encoding;
  1051. }
  1052. /**
  1053. * Generates a message id
  1054. *
  1055. * @return {String} Random Message-ID value
  1056. */
  1057. _generateMessageId() {
  1058. return (
  1059. '<' +
  1060. [2, 2, 2, 6].reduce(
  1061. // crux to generate UUID-like random strings
  1062. (prev, len) => prev + '-' + crypto.randomBytes(len).toString('hex'),
  1063. crypto.randomBytes(4).toString('hex')
  1064. ) +
  1065. '@' +
  1066. // try to use the domain of the FROM address or fallback to server hostname
  1067. (this.getEnvelope().from || this.hostname || os.hostname() || 'localhost').split('@').pop() +
  1068. '>'
  1069. );
  1070. }
  1071. }
  1072. module.exports = MimeNode;