@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.0](https://github.com/postalsys/email-text-tools/compare/v2.4.26...v2.5.0) (2026-10-19)
4
+
5
+
6
+ ### Features
7
+
8
+ * migrate to TypeScript with ES module and CommonJS builds ([b709d60](https://github.com/postalsys/email-text-tools/commit/b709d60a6a43cd842ad60da554d9f6859350c9af))
9
+
10
+ ## [2.4.26](https://github.com/postalsys/email-text-tools/compare/v2.4.25...v2.4.26) (2026-10-19)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * bump email-content to 0.1.17 and node-html-parser to 9.0.5 ([e95e4a8](https://github.com/postalsys/email-text-tools/commit/e95e4a840a5d343004c022ccaeaf4caec1d4e142))
16
+
3
17
  ## [2.4.25](https://github.com/postalsys/email-text-tools/compare/v2.4.24...v2.4.25) (2026-10-18)
4
18
 
5
19
 
package/README.md CHANGED
@@ -2,6 +2,16 @@
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
+
5
15
  ## mimeHtml sanitizer policy
6
16
 
7
17
  `mimeHtml()` sanitizes with DOMPurify and additionally removes:
@@ -0,0 +1,18 @@
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
+ export type { FormatDateOptions } from './lib/format-date.js';
7
+ export type { InlineAction, MessageAddress, QuotedMessage } from './lib/inline-html.js';
8
+ export type { MimeHtmlAsyncOptions, MimeHtmlMessage } from './lib/mime-html.js';
9
+ export type { WorkerPoolOptions, WorkerPoolStats } from './lib/worker-pool.js';
10
+ export { textToHtml, htmlToText, inlineHtml, inlineText, mimeHtml };
11
+ declare const textTools: {
12
+ textToHtml: typeof textToHtml;
13
+ htmlToText: typeof htmlToText;
14
+ inlineHtml: typeof inlineHtml;
15
+ inlineText: typeof inlineText;
16
+ mimeHtml: typeof mimeHtml;
17
+ };
18
+ export default textTools;
@@ -0,0 +1,28 @@
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.
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const text_to_html_js_1 = __importDefault(require("./lib/text-to-html.js"));
14
+ const html_to_text_js_1 = __importDefault(require("./lib/html-to-text.js"));
15
+ const inline_html_js_1 = __importDefault(require("./lib/inline-html.js"));
16
+ const inline_text_js_1 = __importDefault(require("./lib/inline-text.js"));
17
+ 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 };
18
+ Object.defineProperty(textTools, 'mimeHtml', {
19
+ enumerable: true,
20
+ get() {
21
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
22
+ return require('./lib/mime-html.js');
23
+ }
24
+ });
25
+ // What TypeScript and Babel generated `import textTools from '...'` code reads, as the
26
+ // declarations promise a default export
27
+ Object.defineProperty(textTools, 'default', { value: textTools, enumerable: false, writable: true, configurable: true });
28
+ 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,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;
@@ -0,0 +1,111 @@
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 = inlineText;
7
+ const format_date_js_1 = __importDefault(require("./format-date.js"));
8
+ const html_to_text_js_1 = __importDefault(require("./html-to-text.js"));
9
+ function formatAddressText(addr) {
10
+ if (!addr || (!addr.name && !addr.address)) {
11
+ return false;
12
+ }
13
+ let parts = [];
14
+ if (addr.name) {
15
+ parts.push(addr.name);
16
+ }
17
+ if (addr.address) {
18
+ parts.push(`<${addr.address}>`);
19
+ }
20
+ return parts.join(' ');
21
+ }
22
+ function formatAddressesText(input) {
23
+ const addresses = [].concat(input || []);
24
+ if (!addresses || !addresses.length) {
25
+ return false;
26
+ }
27
+ let list = [];
28
+ for (let address of addresses) {
29
+ let entry = formatAddressText(address);
30
+ if (entry) {
31
+ list.push(entry);
32
+ }
33
+ }
34
+ if (!list.length) {
35
+ return false;
36
+ }
37
+ return list.join(', ');
38
+ }
39
+ function escapeText(text) {
40
+ return '> ' + text.replace(/\r?\n/g, '\n> ');
41
+ }
42
+ function inlineText(type, messageContent, messageData, options) {
43
+ options = options || {};
44
+ messageContent = (messageContent || '').toString();
45
+ let originalBody;
46
+ if (!messageData.text && messageData.html) {
47
+ // passed on as given, a Buffer included
48
+ originalBody = (0, html_to_text_js_1.default)(messageData.html);
49
+ }
50
+ else {
51
+ originalBody = messageData.text;
52
+ }
53
+ if (!originalBody) {
54
+ return messageContent;
55
+ }
56
+ let headerLines = [];
57
+ if (messageData.from) {
58
+ let entry = {
59
+ title: 'From',
60
+ content: formatAddressesText(messageData.from) || '<>'
61
+ };
62
+ headerLines.push(entry);
63
+ }
64
+ if (messageData.subject) {
65
+ let entry = {
66
+ title: 'Subject',
67
+ content: messageData.subject
68
+ };
69
+ headerLines.push(entry);
70
+ }
71
+ let dateStr = '';
72
+ if (messageData.date) {
73
+ dateStr = (0, format_date_js_1.default)(messageData.date, options);
74
+ let entry = {
75
+ title: 'Date',
76
+ content: dateStr
77
+ };
78
+ headerLines.push(entry);
79
+ }
80
+ for (let key of ['to', 'cc', 'bcc']) {
81
+ if (messageData[key]) {
82
+ let content = formatAddressesText(messageData[key]);
83
+ if (content) {
84
+ let entry = {
85
+ title: key.replace(/^./, c => c.toUpperCase()),
86
+ content
87
+ };
88
+ headerLines.push(entry);
89
+ }
90
+ }
91
+ }
92
+ let headers = [];
93
+ switch (type) {
94
+ case 'forward':
95
+ headers.push('Begin forwarded message:\n');
96
+ for (let line of headerLines) {
97
+ headers.push(`${line.title}: ${line.content}`);
98
+ }
99
+ break;
100
+ case 'reply':
101
+ headers.push(`On ${dateStr}, ${formatAddressesText(messageData.from) || '<>'} wrote:\n`);
102
+ break;
103
+ }
104
+ return `${messageContent}
105
+
106
+ ${escapeText(headers.join('\n'))}
107
+ >
108
+ ${escapeText(originalBody)}`;
109
+ }
110
+ module.exports = exports.default;
111
+ Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
@@ -0,0 +1 @@
1
+ export {};