@postalsys/email-text-tools 2.4.26 → 2.6.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 (88) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +12 -0
  3. package/dist/cjs/index.d.ts +20 -0
  4. package/dist/cjs/index.js +31 -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-style.d.ts +2 -0
  16. package/dist/cjs/lib/inline-style.js +42 -0
  17. package/dist/cjs/lib/inline-text.d.ts +3 -0
  18. package/dist/cjs/lib/inline-text.js +111 -0
  19. package/dist/cjs/lib/juice-worker.d.ts +1 -0
  20. package/dist/cjs/lib/juice-worker.js +19 -0
  21. package/dist/cjs/lib/mime-html.d.ts +34 -0
  22. package/dist/cjs/lib/mime-html.js +496 -0
  23. package/dist/cjs/lib/text-to-html.d.ts +1 -0
  24. package/dist/cjs/lib/text-to-html.js +182 -0
  25. package/dist/cjs/lib/worker-pool.d.ts +61 -0
  26. package/dist/cjs/lib/worker-pool.js +229 -0
  27. package/dist/cjs/package.json +3 -0
  28. package/dist/esm/index.d.ts +20 -0
  29. package/dist/esm/index.js +13 -0
  30. package/dist/esm/lib/commonjs.cjs +17 -0
  31. package/dist/esm/lib/commonjs.d.cts +4 -0
  32. package/dist/esm/lib/format-date.d.ts +15 -0
  33. package/{lib → dist/esm/lib}/format-date.js +8 -16
  34. package/dist/esm/lib/html-guard.d.ts +3 -0
  35. package/{lib → dist/esm/lib}/html-guard.js +8 -24
  36. package/dist/esm/lib/html-to-text.d.ts +1 -0
  37. package/{lib → dist/esm/lib}/html-to-text.js +2 -14
  38. package/dist/esm/lib/inline-html.d.ts +19 -0
  39. package/{lib → dist/esm/lib}/inline-html.js +17 -55
  40. package/dist/esm/lib/inline-style.d.ts +2 -0
  41. package/dist/esm/lib/inline-style.js +39 -0
  42. package/dist/esm/lib/inline-text.d.ts +3 -0
  43. package/{lib → dist/esm/lib}/inline-text.js +8 -31
  44. package/dist/esm/lib/juice-worker.d.ts +1 -0
  45. package/{lib → dist/esm/lib}/juice-worker.js +7 -8
  46. package/dist/esm/lib/mime-html.d.ts +34 -0
  47. package/{lib → dist/esm/lib}/mime-html.js +80 -161
  48. package/dist/esm/lib/text-to-html.d.ts +1 -0
  49. package/{lib → dist/esm/lib}/text-to-html.js +35 -63
  50. package/dist/esm/lib/worker-pool.d.ts +61 -0
  51. package/{lib → dist/esm/lib}/worker-pool.js +18 -64
  52. package/dist/esm/package.json +3 -0
  53. package/package.json +67 -10
  54. package/.github/VULNERABILITY_REPORT.yml +0 -75
  55. package/.github/workflows/release.yaml +0 -38
  56. package/.github/workflows/test.yaml +0 -35
  57. package/.ncurc.js +0 -28
  58. package/.prettierignore +0 -14
  59. package/.prettierrc.js +0 -10
  60. package/.release-please-manifest.json +0 -3
  61. package/CLAUDE.md +0 -105
  62. package/benchmark/benchmark.js +0 -348
  63. package/benchmark/concurrency-benchmark.js +0 -176
  64. package/benchmark/quick-benchmark.js +0 -200
  65. package/benchmark/quick-concurrency.js +0 -64
  66. package/benchmark/real-world-benchmark.js +0 -257
  67. package/docs/PERFORMANCE.md +0 -262
  68. package/eslint.config.js +0 -71
  69. package/examples/failing.html +0 -617
  70. package/examples/fixtures/message-1.json +0 -74
  71. package/examples/inline-html.js +0 -24
  72. package/examples/inline-text.js +0 -24
  73. package/examples/mime-html.js +0 -284
  74. package/index.js +0 -22
  75. package/release-please-config.json +0 -10
  76. package/test/comprehensive.test.js +0 -358
  77. package/test/containment.test.js +0 -128
  78. package/test/edge-cases.test.js +0 -62
  79. package/test/hardening.test.js +0 -504
  80. package/test/html-to-text.test.js +0 -253
  81. package/test/inline-html.test.js +0 -230
  82. package/test/inline-text.test.js +0 -204
  83. package/test/mime-html-extra.test.js +0 -39
  84. package/test/mime-html.test.js +0 -302
  85. package/test/text-to-html-extra.test.js +0 -42
  86. package/test/worker-pool-extra.test.js +0 -132
  87. package/test/worker-pool-regression.test.js +0 -128
  88. package/test/worker-pool.test.js +0 -282
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.6.0](https://github.com/postalsys/email-text-tools/compare/v2.5.0...v2.6.0) (2026-10-21)
4
+
5
+
6
+ ### Features
7
+
8
+ * export INLINE_STYLE_BLOCK from the package entry ([36b8c30](https://github.com/postalsys/email-text-tools/commit/36b8c305b3841202ae6d7407e80e0c6189d556a6))
9
+
10
+ ## [2.5.0](https://github.com/postalsys/email-text-tools/compare/v2.4.26...v2.5.0) (2026-10-19)
11
+
12
+
13
+ ### Features
14
+
15
+ * migrate to TypeScript with ES module and CommonJS builds ([b709d60](https://github.com/postalsys/email-text-tools/commit/b709d60a6a43cd842ad60da554d9f6859350c9af))
16
+
3
17
  ## [2.4.26](https://github.com/postalsys/email-text-tools/compare/v2.4.25...v2.4.26) (2026-10-19)
4
18
 
5
19
 
package/README.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  Email text tools
4
4
 
5
+ Ships ES module and CommonJS builds with type declarations, Node.js 20 or newer:
6
+
7
+ ```js
8
+ import { mimeHtml, textToHtml } from '@postalsys/email-text-tools';
9
+ // or
10
+ const { mimeHtml, textToHtml } = require('@postalsys/email-text-tools');
11
+ ```
12
+
13
+ `require()` loads `mimeHtml` (and with it jsdom) only on first access; an ES module import loads it up front.
14
+
15
+ `INLINE_STYLE_BLOCK` is the `<style>` block `mimeHtml()` prepends to a message that only has a text body (font and the colours of nested quotes), for a caller that converts text with `textToHtml()` itself and wants the same styling. Reading it does not load jsdom.
16
+
5
17
  ## mimeHtml sanitizer policy
6
18
 
7
19
  `mimeHtml()` sanitizes with DOMPurify and additionally removes:
@@ -0,0 +1,20 @@
1
+ import textToHtml from './lib/text-to-html.js';
2
+ import htmlToText from './lib/html-to-text.js';
3
+ import inlineHtml from './lib/inline-html.js';
4
+ import inlineText from './lib/inline-text.js';
5
+ import mimeHtml from './lib/mime-html.js';
6
+ import { INLINE_STYLE_BLOCK } from './lib/inline-style.js';
7
+ export type { FormatDateOptions } from './lib/format-date.js';
8
+ export type { InlineAction, MessageAddress, QuotedMessage } from './lib/inline-html.js';
9
+ export type { MimeHtmlAsyncOptions, MimeHtmlMessage } from './lib/mime-html.js';
10
+ export type { WorkerPoolOptions, WorkerPoolStats } from './lib/worker-pool.js';
11
+ export { textToHtml, htmlToText, inlineHtml, inlineText, mimeHtml, INLINE_STYLE_BLOCK };
12
+ declare const textTools: {
13
+ textToHtml: typeof textToHtml;
14
+ htmlToText: typeof htmlToText;
15
+ inlineHtml: typeof inlineHtml;
16
+ inlineText: typeof inlineText;
17
+ mimeHtml: typeof mimeHtml;
18
+ INLINE_STYLE_BLOCK: string;
19
+ };
20
+ export default textTools;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ // The CommonJS entry point, compiled by scripts/build.js into dist/cjs/index.js. index.ts is the
3
+ // ES module entry and the source of the type declarations of both.
4
+ //
5
+ // require() returns the same object as before the TypeScript build: the five helpers as plain
6
+ // enumerable properties, mimeHtml through a getter. mimeHtml loads jsdom, which costs tens of
7
+ // megabytes of heap, so it is only loaded on first access and callers that only need the other
8
+ // helpers never pay for it. INLINE_STYLE_BLOCK was added after them and comes from a leaf module,
9
+ // so reading it does not load jsdom either.
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ const text_to_html_js_1 = __importDefault(require("./lib/text-to-html.js"));
15
+ const html_to_text_js_1 = __importDefault(require("./lib/html-to-text.js"));
16
+ const inline_html_js_1 = __importDefault(require("./lib/inline-html.js"));
17
+ const inline_text_js_1 = __importDefault(require("./lib/inline-text.js"));
18
+ const inline_style_js_1 = require("./lib/inline-style.js");
19
+ const textTools = { textToHtml: text_to_html_js_1.default, htmlToText: html_to_text_js_1.default, inlineHtml: inline_html_js_1.default, inlineText: inline_text_js_1.default };
20
+ Object.defineProperty(textTools, 'mimeHtml', {
21
+ enumerable: true,
22
+ get() {
23
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
24
+ return require('./lib/mime-html.js');
25
+ }
26
+ });
27
+ textTools.INLINE_STYLE_BLOCK = inline_style_js_1.INLINE_STYLE_BLOCK;
28
+ // What TypeScript and Babel generated `import textTools from '...'` code reads, as the
29
+ // declarations promise a default export
30
+ Object.defineProperty(textTools, 'default', { value: textTools, enumerable: false, writable: true, configurable: true });
31
+ module.exports = textTools;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ // What an ES module can not do the same way in both builds. This file compiles to
3
+ // commonjs.cjs in dist/cjs and in dist/esm alike, so __dirname is the directory of the
4
+ // build that loaded it and require() is available to both.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TLDS = exports.JUICE_WORKER_PATH = void 0;
7
+ // A plain require() keeps the call below in the literal path.join(__dirname, '...') form, the
8
+ // one form @yao-pkg/pkg recognizes when it collects the files a binary needs (a default import
9
+ // compiles to path_1.default.join, which it does not). Without it the worker script is left out
10
+ // of an EmailEngine binary
11
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
12
+ const path = require('node:path');
13
+ /** The worker script that runs juice off the main thread, the one of the same build */
14
+ exports.JUICE_WORKER_PATH = path.join(__dirname, 'juice-worker.js');
15
+ /** The top level domain list of the tlds package, which only ships it as index.json */
16
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
17
+ exports.TLDS = require('tlds');
@@ -0,0 +1,4 @@
1
+ /** The worker script that runs juice off the main thread, the one of the same build */
2
+ export declare const JUICE_WORKER_PATH: string;
3
+ /** The top level domain list of the tlds package, which only ships it as index.json */
4
+ export declare const TLDS: string[];
@@ -0,0 +1,15 @@
1
+ export interface FormatDateOptions {
2
+ /** BCP 47 language tag, defaults to 'en' */
3
+ locale?: string | undefined;
4
+ /** IANA time zone, defaults to the local time zone */
5
+ tz?: string | undefined;
6
+ }
7
+ /**
8
+ * Formats the date of a quoted message for the reply/forward header, for example
9
+ * "Sat, Jun 15, 2024, 2:30 PM" in English.
10
+ *
11
+ * @param value Date to format
12
+ * @param options Locale and time zone
13
+ * @returns Formatted date, or the input as text when it is not a valid date
14
+ */
15
+ export default function formatDate(value: Date | string | number, options?: FormatDateOptions | null): string;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = formatDate;
4
+ const DATE_FORMAT = {
5
+ weekday: 'short',
6
+ year: 'numeric',
7
+ month: 'short',
8
+ day: 'numeric',
9
+ hour: 'numeric',
10
+ minute: '2-digit'
11
+ };
12
+ // An unsupported locale would silently fall back to the host's default locale, and a Node build
13
+ // with only English ICU data (pkg binaries use small-icu) renders other locales as broken
14
+ // placeholders such as "M09 28, Mon", so anything not actually supported becomes 'en'.
15
+ function resolveLocale(locale) {
16
+ if (locale) {
17
+ try {
18
+ const supported = Intl.DateTimeFormat.supportedLocalesOf([locale]);
19
+ if (supported.length) {
20
+ return supported[0];
21
+ }
22
+ }
23
+ catch (_err) {
24
+ // structurally invalid language tag
25
+ }
26
+ }
27
+ return 'en';
28
+ }
29
+ /**
30
+ * Formats the date of a quoted message for the reply/forward header, for example
31
+ * "Sat, Jun 15, 2024, 2:30 PM" in English.
32
+ *
33
+ * @param value Date to format
34
+ * @param options Locale and time zone
35
+ * @returns Formatted date, or the input as text when it is not a valid date
36
+ */
37
+ function formatDate(value, options) {
38
+ options = options || {};
39
+ const date = value instanceof Date ? value : new Date(value);
40
+ if (isNaN(date.getTime())) {
41
+ return String(value);
42
+ }
43
+ const locale = resolveLocale(options.locale);
44
+ let formatter;
45
+ try {
46
+ formatter = new Intl.DateTimeFormat(locale, Object.assign({ timeZone: options.tz || undefined }, DATE_FORMAT));
47
+ }
48
+ catch (err) {
49
+ if (!(err instanceof RangeError)) {
50
+ throw err;
51
+ }
52
+ // an unknown time zone falls back to UTC rather than to whatever zone the host runs in
53
+ formatter = new Intl.DateTimeFormat(locale, Object.assign({ timeZone: 'UTC' }, DATE_FORMAT));
54
+ }
55
+ const formatted = formatter.format(date);
56
+ // ICU puts a narrow no-break space before "PM", which older mail clients show as a box
57
+ return formatted.replace(/[\u00a0\u202f]/g, ' ');
58
+ }
59
+ module.exports = exports.default;
60
+ Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
@@ -0,0 +1,3 @@
1
+ export declare const MAX_NESTING_DEPTH = 1000;
2
+ export declare function exceedsNestingDepth(html: string, maxDepth: number): boolean;
3
+ export declare function fallbackText(html: string): string;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MAX_NESTING_DEPTH = void 0;
7
+ exports.exceedsNestingDepth = exceedsNestingDepth;
8
+ exports.fallbackText = fallbackText;
9
+ const he_1 = __importDefault(require("he"));
10
+ // jsdom builds a tree in time quadratic to its depth (10,000 unclosed <b> tags take about 9 s) and
11
+ // its serializer overflows the stack a few thousand levels down; node-html-parser spends cubic time
12
+ // on the elements left open at the end of a document. Deeper input is not parsed at all. Real
13
+ // email nests a few dozen levels.
14
+ exports.MAX_NESTING_DEPTH = 1000;
15
+ // Elements that never stay open in HTML content, either void or closed implicitly by a sibling,
16
+ // so they do not add to the depth an HTML parser ends up with. Inside SVG and MathML they can nest,
17
+ // so there every start tag counts.
18
+ 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(','));
19
+ // A single linear pass estimating how deep the parsed tree would get. An end tag closes the
20
+ // nearest open element of its name together with everything opened inside it, and one that names
21
+ // no open element is ignored, as an HTML parser ignores it: counting every end tag down instead
22
+ // let "<b></i>" repeated nest as deep as the input allowed, and 10,000 copies took jsdom ten
23
+ // seconds. Otherwise the estimate only ever overestimates (tags inside comments, attribute values
24
+ // and raw text count too), which at worst means an extreme message is shown as plain text.
25
+ function exceedsNestingDepth(html, maxDepth) {
26
+ const tagName = /[a-z][a-z0-9:-]*/iy;
27
+ const stack = [];
28
+ const open = new Map();
29
+ let foreign = 0;
30
+ let pos = 0;
31
+ while (pos < html.length) {
32
+ const tagStart = html.indexOf('<', pos);
33
+ if (tagStart < 0) {
34
+ break;
35
+ }
36
+ const tagEnd = html.indexOf('>', tagStart + 1);
37
+ if (tagEnd < 0) {
38
+ break;
39
+ }
40
+ // resume right after the first ">", so no tag the parser would see is ever skipped
41
+ pos = tagEnd + 1;
42
+ const closing = html.charAt(tagStart + 1) === '/';
43
+ tagName.lastIndex = tagStart + (closing ? 2 : 1);
44
+ const nameMatch = tagName.exec(html);
45
+ if (!nameMatch) {
46
+ continue;
47
+ }
48
+ const name = nameMatch[0].toLowerCase();
49
+ const isForeignRoot = name === 'svg' || name === 'math';
50
+ if (closing) {
51
+ if (!open.get(name)) {
52
+ continue;
53
+ }
54
+ let popped;
55
+ do {
56
+ // the end tag names an open element, so the stack can not run out before it
57
+ popped = stack.pop();
58
+ open.set(popped, open.get(popped) - 1);
59
+ if (popped === 'svg' || popped === 'math') {
60
+ foreign--;
61
+ }
62
+ } while (popped !== name);
63
+ continue;
64
+ }
65
+ if (!foreign && NON_NESTING_TAGS.has(name)) {
66
+ continue;
67
+ }
68
+ // a self-closing flag is ignored on HTML elements, only SVG and MathML honor it
69
+ if ((foreign || isForeignRoot) && html.charAt(tagEnd - 1) === '/') {
70
+ continue;
71
+ }
72
+ if (isForeignRoot) {
73
+ foreign++;
74
+ }
75
+ stack.push(name);
76
+ open.set(name, (open.get(name) || 0) + 1);
77
+ if (stack.length > maxDepth) {
78
+ return true;
79
+ }
80
+ }
81
+ return false;
82
+ }
83
+ // The text of an HTML body for a fallback rendering. The fallback runs when the HTML could not be
84
+ // processed (hostile nesting, a parser or serializer overflow), so it cannot lean on a real HTML
85
+ // parser either: html-to-text gives up on a body nested past its own limit and returned nothing
86
+ // for exactly the input that lands here. One pass over the string: tags become a space, style and
87
+ // script blocks are dropped whole, entities are decoded once
88
+ function fallbackText(html) {
89
+ const lower = html.toLowerCase();
90
+ const parts = [];
91
+ let pos = 0;
92
+ while (pos < html.length) {
93
+ const lt = html.indexOf('<', pos);
94
+ if (lt < 0) {
95
+ parts.push(html.slice(pos));
96
+ break;
97
+ }
98
+ parts.push(html.slice(pos, lt));
99
+ const gt = html.indexOf('>', lt + 1);
100
+ if (gt < 0) {
101
+ // an unterminated tag runs to the end of the input
102
+ break;
103
+ }
104
+ const name = /^(style|script)(?:[\s/]|$)/.exec(lower.slice(lt + 1, gt));
105
+ if (name) {
106
+ const close = lower.indexOf('</' + name[1], gt + 1);
107
+ const closeEnd = close < 0 ? -1 : html.indexOf('>', close);
108
+ if (closeEnd < 0) {
109
+ break;
110
+ }
111
+ pos = closeEnd + 1;
112
+ }
113
+ else {
114
+ pos = gt + 1;
115
+ }
116
+ parts.push(' ');
117
+ }
118
+ return he_1.default.decode(parts.join(''));
119
+ }
@@ -0,0 +1 @@
1
+ export default function htmlToText(html: string): string;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = htmlToText;
4
+ const html_to_text_1 = require("html-to-text");
5
+ // Post-process function to clean up excessive newlines
6
+ function cleanupNewlines(text) {
7
+ // Replace 3 or more consecutive newlines with just 2
8
+ // This handles cases where HTML has many nested empty divs
9
+ return text.replace(/\n{3,}/g, '\n\n');
10
+ }
11
+ const htmlToTextCompiled = (0, html_to_text_1.compile)({
12
+ wordwrap: 160,
13
+ selectors: [
14
+ // Block elements that should preserve line breaks
15
+ { selector: 'p', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
16
+ // Divs get minimal breaks - many HTML emails wrap each line in a div
17
+ { selector: 'div', options: { leadingLineBreaks: 1, trailingLineBreaks: 0 } },
18
+ { selector: 'pre', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
19
+ { selector: 'blockquote', options: { leadingLineBreaks: 1, trailingLineBreaks: 1, trimEmptyLines: true } },
20
+ // Headers should have spacing
21
+ { selector: 'h1', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
22
+ { selector: 'h2', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
23
+ { selector: 'h3', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
24
+ { selector: 'h4', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
25
+ { selector: 'h5', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
26
+ { selector: 'h6', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
27
+ // Line breaks
28
+ { selector: 'br', options: { leadingLineBreaks: 1, trailingLineBreaks: 0 } },
29
+ { selector: 'hr', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
30
+ // Lists should preserve structure
31
+ { selector: 'ul', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
32
+ { selector: 'ol', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
33
+ // Tables
34
+ { selector: 'table', format: 'dataTable' },
35
+ // Ignore images
36
+ { selector: 'img', format: 'skip' }
37
+ ],
38
+ // Preserve whitespace better for content
39
+ preserveNewlines: true,
40
+ // The DOM walk is recursive, so hostile nesting would overflow the stack. Content below this
41
+ // depth is dropped; real email nests a few dozen levels.
42
+ limits: { maxDepth: 500, ellipsis: '' }
43
+ });
44
+ // Wrapper function that applies post-processing
45
+ function htmlToText(html) {
46
+ return cleanupNewlines(htmlToTextCompiled(html));
47
+ }
48
+ module.exports = exports.default;
49
+ Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
@@ -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;
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = inlineHtml;
7
+ const node_html_parser_1 = __importDefault(require("node-html-parser"));
8
+ const he_1 = __importDefault(require("he"));
9
+ const format_date_js_1 = __importDefault(require("./format-date.js"));
10
+ const text_to_html_js_1 = __importDefault(require("./text-to-html.js"));
11
+ const html_guard_js_1 = require("./html-guard.js");
12
+ // Escaped text in place of a body that is not worth parsing
13
+ function fallbackBody(html) {
14
+ return `<div style="white-space: pre-wrap;">${he_1.default.encode((0, html_guard_js_1.fallbackText)(html), { useNamedReferences: true })}</div>`;
15
+ }
16
+ function getHtmlBody(input) {
17
+ const html = ((input && input.toString()) || '').trim();
18
+ if (!html) {
19
+ return '';
20
+ }
21
+ // node-html-parser spends cubic time removing the elements left open at the end of a document
22
+ // (16 KB of unclosed <b> tags took ten seconds), so the depth check that guards mimeHtml()
23
+ // refuses the obvious shapes before parsing. The parse option leaves whatever is still open in
24
+ // place instead of reparenting it, so a shape the check underestimates (it follows the HTML
25
+ // parsing rules, node-html-parser ignores more end tags) costs linear time and at worst a stack
26
+ // overflow in the serializer, caught below
27
+ if ((0, html_guard_js_1.exceedsNestingDepth)(html, html_guard_js_1.MAX_NESTING_DEPTH)) {
28
+ return fallbackBody(html);
29
+ }
30
+ try {
31
+ const root = node_html_parser_1.default.parse(html, { parseNoneClosedTags: true });
32
+ const body = root.querySelector('body') || root.querySelector('html') || root;
33
+ return (body.innerHTML || '').trim();
34
+ }
35
+ catch (_err) {
36
+ return fallbackBody(html);
37
+ }
38
+ }
39
+ function formatAddressHtml(addr) {
40
+ if (!addr || (!addr.name && !addr.address)) {
41
+ return false;
42
+ }
43
+ let parts = [];
44
+ if (addr.name) {
45
+ parts.push(he_1.default.encode(addr.name, { useNamedReferences: true }));
46
+ }
47
+ if (addr.address) {
48
+ parts.push(`${he_1.default.encode('<', { useNamedReferences: true })}<a href="mailto:${he_1.default.encode(addr.address, { useNamedReferences: true })}">${he_1.default.encode(addr.address, { useNamedReferences: true })}</a>${he_1.default.encode('>', { useNamedReferences: true })}`);
49
+ }
50
+ return parts.join(' ');
51
+ }
52
+ function formatAddressesHtml(input) {
53
+ const addresses = [].concat(input || []);
54
+ if (!addresses || !addresses.length) {
55
+ return false;
56
+ }
57
+ let list = [];
58
+ for (let address of addresses) {
59
+ let entry = formatAddressHtml(address);
60
+ if (entry) {
61
+ list.push(entry);
62
+ }
63
+ }
64
+ if (!list.length) {
65
+ return false;
66
+ }
67
+ return list.join(', ');
68
+ }
69
+ function inlineHtml(action, messageContent, messageData, options) {
70
+ options = options || {};
71
+ messageContent = (messageContent || '').toString();
72
+ let originalBody;
73
+ if (messageData.text && !messageData.html) {
74
+ originalBody = (0, text_to_html_js_1.default)(messageData.text);
75
+ }
76
+ else {
77
+ originalBody = getHtmlBody(messageData.html);
78
+ }
79
+ if (!originalBody) {
80
+ return messageContent;
81
+ }
82
+ // remove html headers and stuff
83
+ messageContent = getHtmlBody(messageContent);
84
+ let headerLines = [];
85
+ if (messageData.from) {
86
+ let entry = {
87
+ title: 'From',
88
+ content: formatAddressesHtml(messageData.from) || he_1.default.encode('<>', { useNamedReferences: true })
89
+ };
90
+ headerLines.push(entry);
91
+ }
92
+ if (messageData.subject) {
93
+ let entry = {
94
+ title: 'Subject',
95
+ content: `<b>${he_1.default.encode(messageData.subject, { useNamedReferences: true })}</b>`
96
+ };
97
+ headerLines.push(entry);
98
+ }
99
+ let dateStr = '';
100
+ if (messageData.date) {
101
+ dateStr = (0, format_date_js_1.default)(messageData.date, options);
102
+ let entry = {
103
+ title: 'Date',
104
+ content: he_1.default.encode(dateStr, { useNamedReferences: true })
105
+ };
106
+ headerLines.push(entry);
107
+ }
108
+ for (let key of ['to', 'cc', 'bcc']) {
109
+ if (messageData[key]) {
110
+ let content = formatAddressesHtml(messageData[key]);
111
+ if (content) {
112
+ let entry = {
113
+ title: key.replace(/^./, c => c.toUpperCase()),
114
+ content
115
+ };
116
+ headerLines.push(entry);
117
+ }
118
+ }
119
+ }
120
+ let headers = [];
121
+ switch (action) {
122
+ case 'forward':
123
+ headers.push(`<div>Begin forwarded message:</div><br class="Apple-interchange-newline">`);
124
+ for (let line of headerLines) {
125
+ 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>`);
126
+ }
127
+ break;
128
+ case 'reply':
129
+ headers.push(`<div>On ${he_1.default.encode(dateStr, { useNamedReferences: true })}, ${formatAddressesHtml(messageData.from) || he_1.default.encode('<>', { useNamedReferences: true })} wrote:</div>`);
130
+ break;
131
+ }
132
+ return `<html>
133
+
134
+ <head>
135
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
136
+ </head>
137
+
138
+ <body style="word-wrap: break-word; -webkit-nbsp-mode: space; line-break: after-white-space;">${messageContent}<br>
139
+ <div><br>
140
+ <blockquote type="cite" class="ee-block-1">
141
+ ${headers.join('\n')}<br>
142
+ <div>${originalBody}</div>
143
+ </blockquote>
144
+ </div>
145
+ </body>
146
+
147
+ </html>`;
148
+ }
149
+ module.exports = exports.default;
150
+ Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
@@ -0,0 +1,2 @@
1
+ /** Base styles mimeHtml() applies to a message that only has a plain text body */
2
+ export declare const INLINE_STYLE_BLOCK = "<style>\n\nbody, td, th, p {\n font-family: sans-serif;\n font-size: 12px;\n}\n\nblockquote {\n border-left-width: 2px;\n border-left-style: solid;\n \n border-left-color: darkblue;\n color: darkblue;\n\n margin: 1rem 0;\n padding-left: 1rem;\n}\n\nblockquote blockquote{\n border-left-color: royalblue;\n color: royalblue;\n}\n\nblockquote blockquote blockquote{\n border-left-color: dodgerblue;\n color: dodgerblue;\n}\n\nblockquote blockquote blockquote blockquote{\n border-left-color: darkblue;\n color: darkblue;\n}\n\n</style>";
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ // The default <style> block mimeHtml() prepends when a message has only a text body, so quoted
3
+ // text converted by textToHtml() gets its quote colouring. A leaf module, so the package entry can
4
+ // export it without loading jsdom. Named export only: lib/ modules with a default export are
5
+ // rewritten to return it from require().
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.INLINE_STYLE_BLOCK = void 0;
8
+ /** Base styles mimeHtml() applies to a message that only has a plain text body */
9
+ exports.INLINE_STYLE_BLOCK = `<style>
10
+
11
+ body, td, th, p {
12
+ font-family: sans-serif;
13
+ font-size: 12px;
14
+ }
15
+
16
+ blockquote {
17
+ border-left-width: 2px;
18
+ border-left-style: solid;
19
+
20
+ border-left-color: darkblue;
21
+ color: darkblue;
22
+
23
+ margin: 1rem 0;
24
+ padding-left: 1rem;
25
+ }
26
+
27
+ blockquote blockquote{
28
+ border-left-color: royalblue;
29
+ color: royalblue;
30
+ }
31
+
32
+ blockquote blockquote blockquote{
33
+ border-left-color: dodgerblue;
34
+ color: dodgerblue;
35
+ }
36
+
37
+ blockquote blockquote blockquote blockquote{
38
+ border-left-color: darkblue;
39
+ color: darkblue;
40
+ }
41
+
42
+ </style>`;
@@ -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;