@postalsys/email-text-tools 2.4.26 → 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.
- package/CHANGELOG.md +7 -0
- package/README.md +10 -0
- package/dist/cjs/index.d.ts +18 -0
- package/dist/cjs/index.js +28 -0
- package/dist/cjs/lib/commonjs.cjs +17 -0
- package/dist/cjs/lib/commonjs.d.cts +4 -0
- package/dist/cjs/lib/format-date.d.ts +15 -0
- package/dist/cjs/lib/format-date.js +60 -0
- package/dist/cjs/lib/html-guard.d.ts +3 -0
- package/dist/cjs/lib/html-guard.js +119 -0
- package/dist/cjs/lib/html-to-text.d.ts +1 -0
- package/dist/cjs/lib/html-to-text.js +49 -0
- package/dist/cjs/lib/inline-html.d.ts +19 -0
- package/dist/cjs/lib/inline-html.js +150 -0
- package/dist/cjs/lib/inline-text.d.ts +3 -0
- package/dist/cjs/lib/inline-text.js +111 -0
- package/dist/cjs/lib/juice-worker.d.ts +1 -0
- package/dist/cjs/lib/juice-worker.js +19 -0
- package/dist/cjs/lib/mime-html.d.ts +34 -0
- package/dist/cjs/lib/mime-html.js +529 -0
- package/dist/cjs/lib/text-to-html.d.ts +1 -0
- package/dist/cjs/lib/text-to-html.js +182 -0
- package/dist/cjs/lib/worker-pool.d.ts +61 -0
- package/dist/cjs/lib/worker-pool.js +229 -0
- package/dist/cjs/package.json +3 -0
- package/dist/esm/index.d.ts +18 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/lib/commonjs.cjs +17 -0
- package/dist/esm/lib/commonjs.d.cts +4 -0
- package/dist/esm/lib/format-date.d.ts +15 -0
- package/{lib → dist/esm/lib}/format-date.js +8 -16
- package/dist/esm/lib/html-guard.d.ts +3 -0
- package/{lib → dist/esm/lib}/html-guard.js +8 -24
- package/dist/esm/lib/html-to-text.d.ts +1 -0
- package/{lib → dist/esm/lib}/html-to-text.js +2 -14
- package/dist/esm/lib/inline-html.d.ts +19 -0
- package/{lib → dist/esm/lib}/inline-html.js +17 -55
- package/dist/esm/lib/inline-text.d.ts +3 -0
- package/{lib → dist/esm/lib}/inline-text.js +8 -31
- package/dist/esm/lib/juice-worker.d.ts +1 -0
- package/{lib → dist/esm/lib}/juice-worker.js +7 -8
- package/dist/esm/lib/mime-html.d.ts +34 -0
- package/{lib → dist/esm/lib}/mime-html.js +79 -127
- package/dist/esm/lib/text-to-html.d.ts +1 -0
- package/{lib → dist/esm/lib}/text-to-html.js +35 -63
- package/dist/esm/lib/worker-pool.d.ts +61 -0
- package/{lib → dist/esm/lib}/worker-pool.js +18 -64
- package/dist/esm/package.json +3 -0
- package/package.json +67 -10
- package/.github/VULNERABILITY_REPORT.yml +0 -75
- package/.github/workflows/release.yaml +0 -38
- package/.github/workflows/test.yaml +0 -35
- package/.ncurc.js +0 -28
- package/.prettierignore +0 -14
- package/.prettierrc.js +0 -10
- package/.release-please-manifest.json +0 -3
- package/CLAUDE.md +0 -105
- package/benchmark/benchmark.js +0 -348
- package/benchmark/concurrency-benchmark.js +0 -176
- package/benchmark/quick-benchmark.js +0 -200
- package/benchmark/quick-concurrency.js +0 -64
- package/benchmark/real-world-benchmark.js +0 -257
- package/docs/PERFORMANCE.md +0 -262
- package/eslint.config.js +0 -71
- package/examples/failing.html +0 -617
- package/examples/fixtures/message-1.json +0 -74
- package/examples/inline-html.js +0 -24
- package/examples/inline-text.js +0 -24
- package/examples/mime-html.js +0 -284
- package/index.js +0 -22
- package/release-please-config.json +0 -10
- package/test/comprehensive.test.js +0 -358
- package/test/containment.test.js +0 -128
- package/test/edge-cases.test.js +0 -62
- package/test/hardening.test.js +0 -504
- package/test/html-to-text.test.js +0 -253
- package/test/inline-html.test.js +0 -230
- package/test/inline-text.test.js +0 -204
- package/test/mime-html-extra.test.js +0 -39
- package/test/mime-html.test.js +0 -302
- package/test/text-to-html-extra.test.js +0 -42
- package/test/worker-pool-extra.test.js +0 -132
- package/test/worker-pool-regression.test.js +0 -128
- package/test/worker-pool.test.js +0 -282
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
}
|
|
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;
|