index.js 42 KB

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