@postalsys/email-text-tools 2.4.25 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +10 -0
  3. package/dist/cjs/index.d.ts +18 -0
  4. package/dist/cjs/index.js +28 -0
  5. package/dist/cjs/lib/commonjs.cjs +17 -0
  6. package/dist/cjs/lib/commonjs.d.cts +4 -0
  7. package/dist/cjs/lib/format-date.d.ts +15 -0
  8. package/dist/cjs/lib/format-date.js +60 -0
  9. package/dist/cjs/lib/html-guard.d.ts +3 -0
  10. package/dist/cjs/lib/html-guard.js +119 -0
  11. package/dist/cjs/lib/html-to-text.d.ts +1 -0
  12. package/dist/cjs/lib/html-to-text.js +49 -0
  13. package/dist/cjs/lib/inline-html.d.ts +19 -0
  14. package/dist/cjs/lib/inline-html.js +150 -0
  15. package/dist/cjs/lib/inline-text.d.ts +3 -0
  16. package/dist/cjs/lib/inline-text.js +111 -0
  17. package/dist/cjs/lib/juice-worker.d.ts +1 -0
  18. package/dist/cjs/lib/juice-worker.js +19 -0
  19. package/dist/cjs/lib/mime-html.d.ts +34 -0
  20. package/dist/cjs/lib/mime-html.js +529 -0
  21. package/dist/cjs/lib/text-to-html.d.ts +1 -0
  22. package/dist/cjs/lib/text-to-html.js +182 -0
  23. package/dist/cjs/lib/worker-pool.d.ts +61 -0
  24. package/dist/cjs/lib/worker-pool.js +229 -0
  25. package/dist/cjs/package.json +3 -0
  26. package/dist/esm/index.d.ts +18 -0
  27. package/dist/esm/index.js +12 -0
  28. package/dist/esm/lib/commonjs.cjs +17 -0
  29. package/dist/esm/lib/commonjs.d.cts +4 -0
  30. package/dist/esm/lib/format-date.d.ts +15 -0
  31. package/{lib → dist/esm/lib}/format-date.js +8 -16
  32. package/dist/esm/lib/html-guard.d.ts +3 -0
  33. package/{lib → dist/esm/lib}/html-guard.js +8 -24
  34. package/dist/esm/lib/html-to-text.d.ts +1 -0
  35. package/{lib → dist/esm/lib}/html-to-text.js +2 -14
  36. package/dist/esm/lib/inline-html.d.ts +19 -0
  37. package/{lib → dist/esm/lib}/inline-html.js +17 -55
  38. package/dist/esm/lib/inline-text.d.ts +3 -0
  39. package/{lib → dist/esm/lib}/inline-text.js +8 -31
  40. package/dist/esm/lib/juice-worker.d.ts +1 -0
  41. package/{lib → dist/esm/lib}/juice-worker.js +7 -8
  42. package/dist/esm/lib/mime-html.d.ts +34 -0
  43. package/{lib → dist/esm/lib}/mime-html.js +79 -127
  44. package/dist/esm/lib/text-to-html.d.ts +1 -0
  45. package/{lib → dist/esm/lib}/text-to-html.js +35 -63
  46. package/dist/esm/lib/worker-pool.d.ts +61 -0
  47. package/{lib → dist/esm/lib}/worker-pool.js +18 -64
  48. package/dist/esm/package.json +3 -0
  49. package/package.json +72 -10
  50. package/.eslintrc +0 -15
  51. package/.github/VULNERABILITY_REPORT.yml +0 -75
  52. package/.github/workflows/release.yaml +0 -37
  53. package/.github/workflows/test.yaml +0 -31
  54. package/.ncurc.js +0 -22
  55. package/.prettierignore +0 -14
  56. package/.prettierrc.js +0 -8
  57. package/.release-please-manifest.json +0 -3
  58. package/benchmark/benchmark.js +0 -358
  59. package/benchmark/concurrency-benchmark.js +0 -176
  60. package/benchmark/quick-benchmark.js +0 -200
  61. package/benchmark/quick-concurrency.js +0 -64
  62. package/benchmark/real-world-benchmark.js +0 -267
  63. package/docs/PERFORMANCE.md +0 -262
  64. package/eslint.config.js +0 -52
  65. package/examples/failing.html +0 -617
  66. package/examples/fixtures/message-1.json +0 -74
  67. package/examples/inline-html.js +0 -24
  68. package/examples/inline-text.js +0 -24
  69. package/examples/mime-html.js +0 -284
  70. package/index.js +0 -22
  71. package/release-please-config.json +0 -10
  72. package/test/comprehensive.test.js +0 -358
  73. package/test/containment.test.js +0 -128
  74. package/test/edge-cases.test.js +0 -62
  75. package/test/hardening.test.js +0 -504
  76. package/test/html-to-text.test.js +0 -253
  77. package/test/inline-html.test.js +0 -230
  78. package/test/inline-text.test.js +0 -204
  79. package/test/mime-html-extra.test.js +0 -39
  80. package/test/mime-html.test.js +0 -302
  81. package/test/text-to-html-extra.test.js +0 -42
  82. package/test/worker-pool-extra.test.js +0 -132
  83. package/test/worker-pool-regression.test.js +0 -128
  84. package/test/worker-pool.test.js +0 -282
@@ -1,29 +1,20 @@
1
- 'use strict';
2
-
3
- const he = require('he');
4
-
1
+ import he from 'he';
5
2
  // jsdom builds a tree in time quadratic to its depth (10,000 unclosed <b> tags take about 9 s) and
6
3
  // its serializer overflows the stack a few thousand levels down; node-html-parser spends cubic time
7
4
  // on the elements left open at the end of a document. Deeper input is not parsed at all. Real
8
5
  // email nests a few dozen levels.
9
- const MAX_NESTING_DEPTH = 1000;
10
-
6
+ export const MAX_NESTING_DEPTH = 1000;
11
7
  // Elements that never stay open in HTML content, either void or closed implicitly by a sibling,
12
8
  // so they do not add to the depth an HTML parser ends up with. Inside SVG and MathML they can nest,
13
9
  // so there every start tag counts.
14
- const NON_NESTING_TAGS = new Set(
15
- 'area,base,br,col,embed,hr,img,input,keygen,link,meta,param,source,track,wbr,p,li,dt,dd,td,th,tr,tbody,thead,tfoot,colgroup,caption,option,optgroup,rt,rp'.split(
16
- ','
17
- )
18
- );
19
-
10
+ const NON_NESTING_TAGS = new Set('area,base,br,col,embed,hr,img,input,keygen,link,meta,param,source,track,wbr,p,li,dt,dd,td,th,tr,tbody,thead,tfoot,colgroup,caption,option,optgroup,rt,rp'.split(','));
20
11
  // A single linear pass estimating how deep the parsed tree would get. An end tag closes the
21
12
  // nearest open element of its name together with everything opened inside it, and one that names
22
13
  // no open element is ignored, as an HTML parser ignores it: counting every end tag down instead
23
14
  // let "<b></i>" repeated nest as deep as the input allowed, and 10,000 copies took jsdom ten
24
15
  // seconds. Otherwise the estimate only ever overestimates (tags inside comments, attribute values
25
16
  // and raw text count too), which at worst means an extreme message is shown as plain text.
26
- function exceedsNestingDepth(html, maxDepth) {
17
+ export function exceedsNestingDepth(html, maxDepth) {
27
18
  const tagName = /[a-z][a-z0-9:-]*/iy;
28
19
  const stack = [];
29
20
  const open = new Map();
@@ -40,7 +31,6 @@ function exceedsNestingDepth(html, maxDepth) {
40
31
  }
41
32
  // resume right after the first ">", so no tag the parser would see is ever skipped
42
33
  pos = tagEnd + 1;
43
-
44
34
  const closing = html.charAt(tagStart + 1) === '/';
45
35
  tagName.lastIndex = tagStart + (closing ? 2 : 1);
46
36
  const nameMatch = tagName.exec(html);
@@ -49,13 +39,13 @@ function exceedsNestingDepth(html, maxDepth) {
49
39
  }
50
40
  const name = nameMatch[0].toLowerCase();
51
41
  const isForeignRoot = name === 'svg' || name === 'math';
52
-
53
42
  if (closing) {
54
43
  if (!open.get(name)) {
55
44
  continue;
56
45
  }
57
46
  let popped;
58
47
  do {
48
+ // the end tag names an open element, so the stack can not run out before it
59
49
  popped = stack.pop();
60
50
  open.set(popped, open.get(popped) - 1);
61
51
  if (popped === 'svg' || popped === 'math') {
@@ -64,20 +54,16 @@ function exceedsNestingDepth(html, maxDepth) {
64
54
  } while (popped !== name);
65
55
  continue;
66
56
  }
67
-
68
57
  if (!foreign && NON_NESTING_TAGS.has(name)) {
69
58
  continue;
70
59
  }
71
-
72
60
  // a self-closing flag is ignored on HTML elements, only SVG and MathML honor it
73
61
  if ((foreign || isForeignRoot) && html.charAt(tagEnd - 1) === '/') {
74
62
  continue;
75
63
  }
76
-
77
64
  if (isForeignRoot) {
78
65
  foreign++;
79
66
  }
80
-
81
67
  stack.push(name);
82
68
  open.set(name, (open.get(name) || 0) + 1);
83
69
  if (stack.length > maxDepth) {
@@ -86,13 +72,12 @@ function exceedsNestingDepth(html, maxDepth) {
86
72
  }
87
73
  return false;
88
74
  }
89
-
90
75
  // The text of an HTML body for a fallback rendering. The fallback runs when the HTML could not be
91
76
  // processed (hostile nesting, a parser or serializer overflow), so it cannot lean on a real HTML
92
77
  // parser either: html-to-text gives up on a body nested past its own limit and returned nothing
93
78
  // for exactly the input that lands here. One pass over the string: tags become a space, style and
94
79
  // script blocks are dropped whole, entities are decoded once
95
- function fallbackText(html) {
80
+ export function fallbackText(html) {
96
81
  const lower = html.toLowerCase();
97
82
  const parts = [];
98
83
  let pos = 0;
@@ -116,12 +101,11 @@ function fallbackText(html) {
116
101
  break;
117
102
  }
118
103
  pos = closeEnd + 1;
119
- } else {
104
+ }
105
+ else {
120
106
  pos = gt + 1;
121
107
  }
122
108
  parts.push(' ');
123
109
  }
124
110
  return he.decode(parts.join(''));
125
111
  }
126
-
127
- module.exports = { MAX_NESTING_DEPTH, exceedsNestingDepth, fallbackText };
@@ -0,0 +1 @@
1
+ export default function htmlToText(html: string): string;
@@ -1,13 +1,10 @@
1
- 'use strict';
2
- const { compile } = require('html-to-text');
3
-
1
+ import { compile } from 'html-to-text';
4
2
  // Post-process function to clean up excessive newlines
5
3
  function cleanupNewlines(text) {
6
4
  // Replace 3 or more consecutive newlines with just 2
7
5
  // This handles cases where HTML has many nested empty divs
8
6
  return text.replace(/\n{3,}/g, '\n\n');
9
7
  }
10
-
11
8
  const htmlToTextCompiled = compile({
12
9
  wordwrap: 160,
13
10
  selectors: [
@@ -17,7 +14,6 @@ const htmlToTextCompiled = compile({
17
14
  { selector: 'div', options: { leadingLineBreaks: 1, trailingLineBreaks: 0 } },
18
15
  { selector: 'pre', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
19
16
  { selector: 'blockquote', options: { leadingLineBreaks: 1, trailingLineBreaks: 1, trimEmptyLines: true } },
20
-
21
17
  // Headers should have spacing
22
18
  { selector: 'h1', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
23
19
  { selector: 'h2', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
@@ -25,32 +21,24 @@ const htmlToTextCompiled = compile({
25
21
  { selector: 'h4', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
26
22
  { selector: 'h5', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
27
23
  { selector: 'h6', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
28
-
29
24
  // Line breaks
30
25
  { selector: 'br', options: { leadingLineBreaks: 1, trailingLineBreaks: 0 } },
31
26
  { selector: 'hr', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
32
-
33
27
  // Lists should preserve structure
34
28
  { selector: 'ul', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
35
29
  { selector: 'ol', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
36
-
37
30
  // Tables
38
31
  { selector: 'table', format: 'dataTable' },
39
-
40
32
  // Ignore images
41
33
  { selector: 'img', format: 'skip' }
42
34
  ],
43
- hideLinkHrefIfSameAsText: true,
44
35
  // Preserve whitespace better for content
45
36
  preserveNewlines: true,
46
37
  // The DOM walk is recursive, so hostile nesting would overflow the stack. Content below this
47
38
  // depth is dropped; real email nests a few dozen levels.
48
39
  limits: { maxDepth: 500, ellipsis: '' }
49
40
  });
50
-
51
41
  // Wrapper function that applies post-processing
52
- function htmlToText(html) {
42
+ export default function htmlToText(html) {
53
43
  return cleanupNewlines(htmlToTextCompiled(html));
54
44
  }
55
-
56
- module.exports = htmlToText;
@@ -0,0 +1,19 @@
1
+ import type { FormatDateOptions } from './format-date.js';
2
+ /** An address of the quoted message */
3
+ export interface MessageAddress {
4
+ name?: string | undefined;
5
+ address?: string | undefined;
6
+ }
7
+ /** The message being replied to or forwarded */
8
+ export interface QuotedMessage {
9
+ from?: MessageAddress | MessageAddress[] | null | undefined;
10
+ to?: MessageAddress | MessageAddress[] | null | undefined;
11
+ cc?: MessageAddress | MessageAddress[] | null | undefined;
12
+ bcc?: MessageAddress | MessageAddress[] | null | undefined;
13
+ subject?: string | null | undefined;
14
+ date?: Date | string | number | null | undefined;
15
+ html?: string | Buffer | null | undefined;
16
+ text?: string | null | undefined;
17
+ }
18
+ export type InlineAction = 'reply' | 'forward';
19
+ export default function inlineHtml(action: InlineAction | string, messageContent: string | Buffer | null | undefined, messageData: QuotedMessage, options?: FormatDateOptions | null): string;
@@ -1,22 +1,17 @@
1
- 'use strict';
2
-
3
- const HTMLParser = require('node-html-parser');
4
- const he = require('he');
5
- const formatDate = require('./format-date');
6
- const textToHtml = require('./text-to-html');
7
- const { MAX_NESTING_DEPTH, exceedsNestingDepth, fallbackText } = require('./html-guard');
8
-
1
+ import HTMLParser from 'node-html-parser';
2
+ import he from 'he';
3
+ import formatDate from './format-date.js';
4
+ import textToHtml from './text-to-html.js';
5
+ import { MAX_NESTING_DEPTH, exceedsNestingDepth, fallbackText } from './html-guard.js';
9
6
  // Escaped text in place of a body that is not worth parsing
10
7
  function fallbackBody(html) {
11
8
  return `<div style="white-space: pre-wrap;">${he.encode(fallbackText(html), { useNamedReferences: true })}</div>`;
12
9
  }
13
-
14
- function getHtmlBody(html) {
15
- html = ((html && html.toString()) || '').trim();
10
+ function getHtmlBody(input) {
11
+ const html = ((input && input.toString()) || '').trim();
16
12
  if (!html) {
17
13
  return '';
18
14
  }
19
-
20
15
  // node-html-parser spends cubic time removing the elements left open at the end of a document
21
16
  // (16 KB of unclosed <b> tags took ten seconds), so the depth check that guards mimeHtml()
22
17
  // refuses the obvious shapes before parsing. The parse option leaves whatever is still open in
@@ -30,38 +25,29 @@ function getHtmlBody(html) {
30
25
  const root = HTMLParser.parse(html, { parseNoneClosedTags: true });
31
26
  const body = root.querySelector('body') || root.querySelector('html') || root;
32
27
  return (body.innerHTML || '').trim();
33
- } catch (_err) {
28
+ }
29
+ catch (_err) {
34
30
  return fallbackBody(html);
35
31
  }
36
32
  }
37
-
38
33
  function formatAddressHtml(addr) {
39
34
  if (!addr || (!addr.name && !addr.address)) {
40
35
  return false;
41
36
  }
42
-
43
37
  let parts = [];
44
38
  if (addr.name) {
45
39
  parts.push(he.encode(addr.name, { useNamedReferences: true }));
46
40
  }
47
41
  if (addr.address) {
48
- parts.push(
49
- `${he.encode('<', { useNamedReferences: true })}<a href="mailto:${he.encode(addr.address, { useNamedReferences: true })}">${he.encode(
50
- addr.address,
51
- { useNamedReferences: true }
52
- )}</a>${he.encode('>', { useNamedReferences: true })}`
53
- );
42
+ parts.push(`${he.encode('<', { useNamedReferences: true })}<a href="mailto:${he.encode(addr.address, { useNamedReferences: true })}">${he.encode(addr.address, { useNamedReferences: true })}</a>${he.encode('>', { useNamedReferences: true })}`);
54
43
  }
55
-
56
44
  return parts.join(' ');
57
45
  }
58
-
59
- function formatAddressesHtml(addresses) {
60
- addresses = [].concat(addresses || []);
46
+ function formatAddressesHtml(input) {
47
+ const addresses = [].concat(input || []);
61
48
  if (!addresses || !addresses.length) {
62
49
  return false;
63
50
  }
64
-
65
51
  let list = [];
66
52
  for (let address of addresses) {
67
53
  let entry = formatAddressHtml(address);
@@ -74,29 +60,22 @@ function formatAddressesHtml(addresses) {
74
60
  }
75
61
  return list.join(', ');
76
62
  }
77
-
78
- function inlineHtml(action, messageContent, messageData, options) {
63
+ export default function inlineHtml(action, messageContent, messageData, options) {
79
64
  options = options || {};
80
-
81
65
  messageContent = (messageContent || '').toString();
82
-
83
66
  let originalBody;
84
-
85
67
  if (messageData.text && !messageData.html) {
86
68
  originalBody = textToHtml(messageData.text);
87
- } else {
69
+ }
70
+ else {
88
71
  originalBody = getHtmlBody(messageData.html);
89
72
  }
90
-
91
73
  if (!originalBody) {
92
74
  return messageContent;
93
75
  }
94
-
95
76
  // remove html headers and stuff
96
77
  messageContent = getHtmlBody(messageContent);
97
-
98
78
  let headerLines = [];
99
-
100
79
  if (messageData.from) {
101
80
  let entry = {
102
81
  title: 'From',
@@ -104,7 +83,6 @@ function inlineHtml(action, messageContent, messageData, options) {
104
83
  };
105
84
  headerLines.push(entry);
106
85
  }
107
-
108
86
  if (messageData.subject) {
109
87
  let entry = {
110
88
  title: 'Subject',
@@ -112,19 +90,15 @@ function inlineHtml(action, messageContent, messageData, options) {
112
90
  };
113
91
  headerLines.push(entry);
114
92
  }
115
-
116
93
  let dateStr = '';
117
-
118
94
  if (messageData.date) {
119
95
  dateStr = formatDate(messageData.date, options);
120
-
121
96
  let entry = {
122
97
  title: 'Date',
123
98
  content: he.encode(dateStr, { useNamedReferences: true })
124
99
  };
125
100
  headerLines.push(entry);
126
101
  }
127
-
128
102
  for (let key of ['to', 'cc', 'bcc']) {
129
103
  if (messageData[key]) {
130
104
  let content = formatAddressesHtml(messageData[key]);
@@ -137,28 +111,18 @@ function inlineHtml(action, messageContent, messageData, options) {
137
111
  }
138
112
  }
139
113
  }
140
-
141
114
  let headers = [];
142
-
143
115
  switch (action) {
144
116
  case 'forward':
145
117
  headers.push(`<div>Begin forwarded message:</div><br class="Apple-interchange-newline">`);
146
-
147
118
  for (let line of headerLines) {
148
- headers.push(
149
- `<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px;"><span style="font-family: -webkit-system-font, Helvetica Neue, Helvetica, sans-serif; color:rgba(0, 0, 0, 1.0);"><b>${line.title}: </b></span><span style="font-family: -webkit-system-font, Helvetica Neue, Helvetica, sans-serif;">${line.content}<br></span></div>`
150
- );
119
+ headers.push(`<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px;"><span style="font-family: -webkit-system-font, Helvetica Neue, Helvetica, sans-serif; color:rgba(0, 0, 0, 1.0);"><b>${line.title}: </b></span><span style="font-family: -webkit-system-font, Helvetica Neue, Helvetica, sans-serif;">${line.content}<br></span></div>`);
151
120
  }
152
121
  break;
153
122
  case 'reply':
154
- headers.push(
155
- `<div>On ${he.encode(dateStr, { useNamedReferences: true })}, ${
156
- formatAddressesHtml(messageData.from) || he.encode('<>', { useNamedReferences: true })
157
- } wrote:</div>`
158
- );
123
+ headers.push(`<div>On ${he.encode(dateStr, { useNamedReferences: true })}, ${formatAddressesHtml(messageData.from) || he.encode('<>', { useNamedReferences: true })} wrote:</div>`);
159
124
  break;
160
125
  }
161
-
162
126
  return `<html>
163
127
 
164
128
  <head>
@@ -176,5 +140,3 @@ ${headers.join('\n')}<br>
176
140
 
177
141
  </html>`;
178
142
  }
179
-
180
- module.exports = inlineHtml;
@@ -0,0 +1,3 @@
1
+ import type { FormatDateOptions } from './format-date.js';
2
+ import type { InlineAction, QuotedMessage } from './inline-html.js';
3
+ export default function inlineText(type: InlineAction | string, messageContent: string | Buffer | null | undefined, messageData: QuotedMessage, options?: FormatDateOptions | null): string;
@@ -1,27 +1,20 @@
1
- 'use strict';
2
-
3
- const formatDate = require('./format-date');
4
- const htmlToText = require('./html-to-text');
5
-
1
+ import formatDate from './format-date.js';
2
+ import htmlToText from './html-to-text.js';
6
3
  function formatAddressText(addr) {
7
4
  if (!addr || (!addr.name && !addr.address)) {
8
5
  return false;
9
6
  }
10
-
11
7
  let parts = [];
12
8
  if (addr.name) {
13
9
  parts.push(addr.name);
14
10
  }
15
-
16
11
  if (addr.address) {
17
12
  parts.push(`<${addr.address}>`);
18
13
  }
19
-
20
14
  return parts.join(' ');
21
15
  }
22
-
23
- function formatAddressesText(addresses) {
24
- addresses = [].concat(addresses || []);
16
+ function formatAddressesText(input) {
17
+ const addresses = [].concat(input || []);
25
18
  if (!addresses || !addresses.length) {
26
19
  return false;
27
20
  }
@@ -37,29 +30,24 @@ function formatAddressesText(addresses) {
37
30
  }
38
31
  return list.join(', ');
39
32
  }
40
-
41
33
  function escapeText(text) {
42
34
  return '> ' + text.replace(/\r?\n/g, '\n> ');
43
35
  }
44
-
45
- function inlineText(type, messageContent, messageData, options) {
36
+ export default function inlineText(type, messageContent, messageData, options) {
46
37
  options = options || {};
47
-
48
38
  messageContent = (messageContent || '').toString();
49
-
50
39
  let originalBody;
51
40
  if (!messageData.text && messageData.html) {
41
+ // passed on as given, a Buffer included
52
42
  originalBody = htmlToText(messageData.html);
53
- } else {
43
+ }
44
+ else {
54
45
  originalBody = messageData.text;
55
46
  }
56
-
57
47
  if (!originalBody) {
58
48
  return messageContent;
59
49
  }
60
-
61
50
  let headerLines = [];
62
-
63
51
  if (messageData.from) {
64
52
  let entry = {
65
53
  title: 'From',
@@ -67,7 +55,6 @@ function inlineText(type, messageContent, messageData, options) {
67
55
  };
68
56
  headerLines.push(entry);
69
57
  }
70
-
71
58
  if (messageData.subject) {
72
59
  let entry = {
73
60
  title: 'Subject',
@@ -75,19 +62,15 @@ function inlineText(type, messageContent, messageData, options) {
75
62
  };
76
63
  headerLines.push(entry);
77
64
  }
78
-
79
65
  let dateStr = '';
80
-
81
66
  if (messageData.date) {
82
67
  dateStr = formatDate(messageData.date, options);
83
-
84
68
  let entry = {
85
69
  title: 'Date',
86
70
  content: dateStr
87
71
  };
88
72
  headerLines.push(entry);
89
73
  }
90
-
91
74
  for (let key of ['to', 'cc', 'bcc']) {
92
75
  if (messageData[key]) {
93
76
  let content = formatAddressesText(messageData[key]);
@@ -100,13 +83,10 @@ function inlineText(type, messageContent, messageData, options) {
100
83
  }
101
84
  }
102
85
  }
103
-
104
86
  let headers = [];
105
-
106
87
  switch (type) {
107
88
  case 'forward':
108
89
  headers.push('Begin forwarded message:\n');
109
-
110
90
  for (let line of headerLines) {
111
91
  headers.push(`${line.title}: ${line.content}`);
112
92
  }
@@ -115,12 +95,9 @@ function inlineText(type, messageContent, messageData, options) {
115
95
  headers.push(`On ${dateStr}, ${formatAddressesText(messageData.from) || '<>'} wrote:\n`);
116
96
  break;
117
97
  }
118
-
119
98
  return `${messageContent}
120
99
 
121
100
  ${escapeText(headers.join('\n'))}
122
101
  >
123
102
  ${escapeText(originalBody)}`;
124
103
  }
125
-
126
- module.exports = inlineText;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,15 +1,14 @@
1
- 'use strict';
2
-
3
- const { parentPort } = require('worker_threads');
1
+ import workerThreads from 'node:worker_threads';
4
2
  // Pinned to juice 11.x: juice 12+ is pure ESM (requires Node >=22.12) and breaks pkg builds.
5
- // The `.default` fallback keeps this working if juice ever ships a CJS-compatible build again.
6
- const juice = require('juice').default || require('juice');
7
-
8
- parentPort.on('message', html => {
3
+ import juice from 'juice';
4
+ // Runs as a worker thread (see worker-pool.ts), the parent port is always there
5
+ const parentPort = workerThreads.parentPort;
6
+ parentPort.on('message', (html) => {
9
7
  try {
10
8
  const result = juice(html);
11
9
  parentPort.postMessage({ success: true, result });
12
- } catch (error) {
10
+ }
11
+ catch (error) {
13
12
  parentPort.postMessage({ success: false, error: error.message });
14
13
  }
15
14
  });
@@ -0,0 +1,34 @@
1
+ import type { WorkerPoolStats } from './worker-pool.js';
2
+ /** The message to render */
3
+ export interface MimeHtmlMessage {
4
+ html?: string | Buffer | null | undefined;
5
+ text?: string | Buffer | null | undefined;
6
+ }
7
+ export interface MimeHtmlAsyncOptions {
8
+ /** How long juice may run in the worker, in milliseconds (default 5000) */
9
+ timeout?: number | undefined;
10
+ /** Run juice in the shared worker pool (default) or in a worker of its own */
11
+ useWorkerPool?: boolean | undefined;
12
+ /** false rethrows a juice failure or a pipeline error instead of falling back */
13
+ fallbackOnError?: boolean | undefined;
14
+ /** Size of the shared worker pool, used when the pool is created */
15
+ minWorkers?: number | undefined;
16
+ maxWorkers?: number | undefined;
17
+ idleTimeout?: number | undefined;
18
+ }
19
+ declare function hasNotWithPseudo(selector: string): boolean;
20
+ declare function filterCss(css: string): string;
21
+ declare function mimeHtmlSync(message: MimeHtmlMessage): string;
22
+ declare function mimeHtmlAsync(message: MimeHtmlMessage, options?: MimeHtmlAsyncOptions): Promise<string>;
23
+ declare function mimeHtml(message: MimeHtmlMessage): string;
24
+ declare namespace mimeHtml {
25
+ var async: typeof mimeHtmlAsync;
26
+ var sync: typeof mimeHtmlSync;
27
+ var _internal: {
28
+ filterCss: typeof filterCss;
29
+ hasNotWithPseudo: typeof hasNotWithPseudo;
30
+ };
31
+ var closeWorkerPool: () => Promise<void>;
32
+ var getWorkerPoolStats: () => WorkerPoolStats | null;
33
+ }
34
+ export default mimeHtml;