@postalsys/email-text-tools 2.1.15 → 2.2.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 +35 -30
- package/benchmark/benchmark.js +358 -0
- package/benchmark/concurrency-benchmark.js +176 -0
- package/benchmark/quick-benchmark.js +200 -0
- package/benchmark/quick-concurrency.js +64 -0
- package/benchmark/real-world-benchmark.js +267 -0
- package/docs/PERFORMANCE.md +262 -0
- package/eslint.config.js +53 -0
- package/examples/failing.html +617 -0
- package/examples/mime-html.js +262 -31
- package/lib/juice-worker.js +13 -0
- package/lib/mime-html.js +246 -22
- package/lib/text-to-html.js +1 -1
- package/lib/worker-pool.js +270 -0
- package/package.json +17 -8
- package/test/comprehensive.test.js +347 -0
- package/test/edge-cases.test.js +62 -0
- package/test/mime-html.test.js +327 -0
- package/test/worker-pool-regression.test.js +128 -0
- package/test/worker-pool.test.js +282 -0
- package/processed.txt +0 -699
package/examples/mime-html.js
CHANGED
|
@@ -6,48 +6,279 @@ const fs = require('fs');
|
|
|
6
6
|
const type = process.argv[3] || 'html';
|
|
7
7
|
const file = fs.readFileSync(process.argv[2]);
|
|
8
8
|
|
|
9
|
-
const HTML_TEMPLATE =
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
<
|
|
9
|
+
const HTML_TEMPLATE = `<!doctype html><html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" style="background-color: #fff;"><head>
|
|
10
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"><title>Neue Nachricht!</title>
|
|
11
|
+
<!--[if !mso]><!-->
|
|
12
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
13
|
+
<!--<![endif]-->
|
|
13
14
|
|
|
14
|
-
<
|
|
15
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
16
|
+
<style type="text/css">
|
|
17
|
+
#outlook a { padding:0; }
|
|
18
|
+
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
|
19
|
+
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
|
20
|
+
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
|
21
|
+
p { display:block;margin:13px 0; }
|
|
22
|
+
</style>
|
|
23
|
+
<!--[if mso]>
|
|
24
|
+
<noscript>
|
|
25
|
+
<xml>
|
|
26
|
+
<o:OfficeDocumentSettings>
|
|
27
|
+
<o:AllowPNG/>
|
|
28
|
+
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
29
|
+
</o:OfficeDocumentSettings>
|
|
30
|
+
</xml>
|
|
31
|
+
</noscript>
|
|
32
|
+
<![endif]-->
|
|
33
|
+
<!--[if lte mso 11]>
|
|
34
|
+
<style type="text/css">
|
|
35
|
+
.mj-outlook-group-fix { width:100% !important; }
|
|
36
|
+
</style>
|
|
37
|
+
<![endif]-->
|
|
15
38
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
39
|
+
<!--[if !mso]><!-->
|
|
40
|
+
<link href="https://fonts.googleapis.com/css?family=Inter:400,600" rel="stylesheet" type="text/css">
|
|
41
|
+
<style type="text/css">
|
|
42
|
+
@import url(https://fonts.googleapis.com/css?family=Inter:400,600);
|
|
43
|
+
</style>
|
|
44
|
+
<!--<![endif]-->
|
|
19
45
|
|
|
20
|
-
.body-container {
|
|
21
|
-
max-width: 840px;
|
|
22
|
-
margin: 30px auto;
|
|
23
|
-
padding: 20px;
|
|
24
|
-
background: #fafafa;
|
|
25
|
-
}
|
|
26
46
|
|
|
27
|
-
.email-container {
|
|
28
|
-
padding: 20px;
|
|
29
|
-
border: 1px solid #ccc;
|
|
30
|
-
background: white;
|
|
31
|
-
}
|
|
32
47
|
|
|
33
|
-
|
|
48
|
+
<style type="text/css">
|
|
49
|
+
@media only screen and (min-width:480px) {
|
|
50
|
+
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
|
51
|
+
}
|
|
52
|
+
</style>
|
|
53
|
+
<style media="screen and (min-width:480px)">
|
|
54
|
+
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
|
|
55
|
+
</style>
|
|
34
56
|
|
|
35
|
-
<head>
|
|
36
|
-
<body>
|
|
37
57
|
|
|
38
|
-
<div class="body-container">
|
|
39
58
|
|
|
40
|
-
<h1>Email message</h1>
|
|
41
59
|
|
|
42
|
-
<
|
|
43
|
-
HTML_BODY
|
|
44
|
-
</div>
|
|
60
|
+
<style type="text/css">
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
@media only screen and (max-width:479px) {
|
|
63
|
+
table.mj-full-width-mobile { width: 100% !important; }
|
|
64
|
+
td.mj-full-width-mobile { width: auto !important; }
|
|
65
|
+
}
|
|
47
66
|
|
|
48
|
-
</
|
|
67
|
+
</style>
|
|
49
68
|
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
<style type="text/css">
|
|
70
|
+
.wfix {
|
|
71
|
+
max-width: 600px !important;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@media(max-width: 480px) {
|
|
75
|
+
.body-image-mobile-width-14 td { width: 14px !important; }
|
|
76
|
+
.body-image td { max-width: 100% !important; }
|
|
77
|
+
.body-image img { box-sizing: border-box; }
|
|
78
|
+
.social-icon-mobile-spacer {
|
|
79
|
+
display: inline-block !important;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.mobile-only { display: none !important; } .desktop-only { display: block !important; }
|
|
84
|
+
@media(max-width: 480px) { .column-stack { width: 100% !important; max-width: 100% !important; display: block !important; } .column-stack td,.column-stack th { padding-left: 0 !important; padding-right: 0 !important; } .mobile-only { display: block !important; } .desktop-only { display: none !important; } }
|
|
85
|
+
</style>
|
|
86
|
+
<style>
|
|
87
|
+
.wrapper {
|
|
88
|
+
padding: 0 12px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Superhuman selector */
|
|
92
|
+
.ShadowHTML .wrapper {
|
|
93
|
+
padding: 0 !important;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
.ShadowHTML :is(.styleUnquotedContent .unquoted-content, .styleUnquotedContent .sh-unquoted-content, .styleQuotedContent .sh-quoted-content, .sh-signature, .Signature-settings) :is(td.column-container, div[class^="mj-column-per-"]){font-size:0!important}
|
|
100
|
+
html.sh-color :is(td.column-container, div[class^="mj-column-per-"], div.mj-column-per-100.mj-outlook-group-fix){font-size:0!important}
|
|
101
|
+
@media(max-width:480px){
|
|
102
|
+
html.sh-color .column-stack{width:100%!important;max-width:100%!important;display:block!important}
|
|
103
|
+
html.sh-color :is(.column-stack td,.column-stack th){padding-left:0!important;padding-right:0!important;width:100%!important;max-width:100%!important;display:block!important}
|
|
104
|
+
html.sh-color :is(.column-stack, .column-stack *){word-break:break-word!important;overflow-wrap:anywhere!important}
|
|
105
|
+
html.sh-color :is(.column-persist, .column-persist *){word-break:break-word!important;overflow-wrap:anywhere!important}
|
|
106
|
+
}
|
|
107
|
+
html.sh-color a[href^="tel:"]{color:inherit!important;text-decoration:none!important;font-size:inherit!important;line-height:inherit!important;letter-spacing:inherit!important;vertical-align:baseline!important}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
.isMarketing { background-color: #fff !important; }
|
|
111
|
+
.isMarketing .styled-primary-column { background-color: #fff !important; border-radius: 0px; }
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
[class~="x_social-icon-dark-mode"] { display: none !important; mso-hide:all; width: 0px !important; padding: 0px !important; }
|
|
115
|
+
|
|
116
|
+
/* Gmail selectors */
|
|
117
|
+
u + .body .wrapper {
|
|
118
|
+
padding: 0 !important;
|
|
119
|
+
}
|
|
120
|
+
div > u + .body .wrapper {
|
|
121
|
+
padding: 0 !important;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
a { text-decoration: none; color: #333; }
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
.link-unsubscribe { color: #9ca3af !important; }
|
|
130
|
+
</style>
|
|
131
|
+
<meta name="color-scheme" content="light">
|
|
132
|
+
<meta name="supported-color-schemes" content="light">
|
|
133
|
+
</head>
|
|
134
|
+
<body class="body" style="word-spacing:normal;background-color:#ffffff;">
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
<div class="email-body" style="background-color:#ffffff;" lang="und" dir="auto">
|
|
138
|
+
<!--[if mso]><style type="text/css"> a { text-decoration: underline !important; } .button a { text-decoration: none !important; } .outlook-list-fix { margin-left:4px !important; } .outlook-break-full-link { word-break: break-all !important; } .outlook-hide-social-icon { display: none !important; mso-hide:all; width: 0px !important; padding: 0px !important; } .outlook-hide-social-icon * { display: none !important; mso-hide:all; width: 0px !important; padding: 0px !important; } .outlook-hide-social-icon table { display: none !important; mso-hide:all; width: 0px !important; padding: 0px !important; } .outlook-hide-social-icon tbody { display: none !important; mso-hide:all; width: 0px !important; padding: 0px !important; } .outlook-hide-social-icon tr { display: none !important; mso-hide:all; width: 0px !important; padding: 0px !important; } .outlook-hide-social-icon td { display: none !important; mso-hide:all; width: 0px !important; padding: 0px !important; } .outlook-hide-social-icon img { display: none !important; mso-hide:all; width: 0px !important; padding: 0px !important; } .outlook-hide-social-icon a { display: none !important; mso-hide:all; width: 0px !important; padding: 0px !important; }
|
|
139
|
+
</style><table align="center" border="0" cellpadding="0" cellspacing="0" class="wfix-outlook gmail-no-padding-outlook wrapper-outlook" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
<div class="wfix gmail-no-padding wrapper" style="margin:0px auto;max-width:600px;">
|
|
143
|
+
|
|
144
|
+
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
|
145
|
+
<tbody>
|
|
146
|
+
<tr>
|
|
147
|
+
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-top:0;text-align:center;">
|
|
148
|
+
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="wfix-outlook" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="wfix-outlook" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
<div class="wfix" style="margin:0px auto;max-width:600px;">
|
|
152
|
+
|
|
153
|
+
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
|
154
|
+
<tbody>
|
|
155
|
+
<tr>
|
|
156
|
+
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:30px;padding-left:0px;padding-right:0px;padding-top:30px;text-align:center;">
|
|
157
|
+
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td align="center" class="wfix-outlook rounded-corners-outlook styled-primary-column-outlook" style="vertical-align:top;width:600px;" ><![endif]-->
|
|
158
|
+
|
|
159
|
+
<div class="mj-column-per-100 mj-outlook-group-fix wfix rounded-corners styled-primary-column" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
160
|
+
|
|
161
|
+
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="border-collapse: separate;">
|
|
162
|
+
<tbody>
|
|
163
|
+
<tr>
|
|
164
|
+
<td style="background-color:#ffffff;border-radius:0px;vertical-align:top;padding-top:0px;padding-right:15px;padding-bottom:0px;padding-left:15px;">
|
|
165
|
+
|
|
166
|
+
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%">
|
|
167
|
+
<tbody>
|
|
168
|
+
|
|
169
|
+
<tr>
|
|
170
|
+
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;word-break:break-word;">
|
|
171
|
+
|
|
172
|
+
<div style="font-family:Inter, ui-sans-serif, sans-serif;font-size:14px;letter-spacing:0px;line-height:21px !important;text-align:left;color:#666666;"><span> </span></div>
|
|
173
|
+
|
|
174
|
+
</td>
|
|
175
|
+
</tr>
|
|
176
|
+
|
|
177
|
+
<tr>
|
|
178
|
+
<td align="left" class="body-image body-image-mobile-width-14" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;word-break:break-word;">
|
|
179
|
+
|
|
180
|
+
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
|
181
|
+
<tbody>
|
|
182
|
+
<tr>
|
|
183
|
+
<td style="width:14px;">
|
|
184
|
+
|
|
185
|
+
<a href="https://framer.com" target="_blank" style="text-decoration: none; color: #333;">
|
|
186
|
+
|
|
187
|
+
<img alt="Framer Logo" src="https://d3b9kr64nievew.cloudfront.net/cm915sac71jhgzjjtr8rmq42q/cmb54yn83cy1vzx0ib1pj5jrq.png" style="border:0;border-radius:0px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="14" height="auto">
|
|
188
|
+
|
|
189
|
+
</a>
|
|
190
|
+
|
|
191
|
+
</td>
|
|
192
|
+
</tr>
|
|
193
|
+
</tbody>
|
|
194
|
+
</table>
|
|
195
|
+
|
|
196
|
+
</td>
|
|
197
|
+
</tr>
|
|
198
|
+
|
|
199
|
+
<tr>
|
|
200
|
+
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;word-break:break-word;">
|
|
201
|
+
|
|
202
|
+
<div style="font-family:Inter, ui-sans-serif, sans-serif;font-size:14px;letter-spacing:0px;line-height:21px !important;text-align:left;color:#666666;"><span> </span></div>
|
|
203
|
+
|
|
204
|
+
</td>
|
|
205
|
+
</tr>
|
|
206
|
+
|
|
207
|
+
<tr>
|
|
208
|
+
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;word-break:break-word;">
|
|
209
|
+
|
|
210
|
+
<div style="font-family:Inter, ui-sans-serif, sans-serif;font-size:14px;letter-spacing:0px;line-height:21px !important;text-align:left;color:#666666;"><span style="line-height: 21px !important; font-size: 14px; font-size: 14px !important; white-space: pre-wrap;">Sie haben eine neue Nachricht über das Kontaktformular auf der Webseite erhalten!</span></div>
|
|
211
|
+
|
|
212
|
+
</td>
|
|
213
|
+
</tr>
|
|
214
|
+
|
|
215
|
+
<tr>
|
|
216
|
+
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;word-break:break-word;">
|
|
217
|
+
|
|
218
|
+
<div style="font-family:Inter, ui-sans-serif, sans-serif;font-size:14px;letter-spacing:0px;line-height:22px !important;text-align:left;color:#666666;"><span> </span></div>
|
|
219
|
+
|
|
220
|
+
</td>
|
|
221
|
+
</tr>
|
|
222
|
+
|
|
223
|
+
<tr>
|
|
224
|
+
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;word-break:break-word;">
|
|
225
|
+
|
|
226
|
+
<div style="font-family:Inter, ui-sans-serif, sans-serif;font-size:14px;letter-spacing:0px;line-height:21px !important;text-align:left;color:#666666;"><span style="line-height: 21px !important; font-size: 14px; font-size: 14px !important; white-space: pre-wrap;"><style type="text/css">a:link {text-decoration: none !important; }</style><div style="line-height:19px;padding-bottom:5px"><span style="color:#999999">Location: </span>Haus kaufen</div><div style="line-height:19px;padding-bottom:5px"><span style="color:#999999">Name: </span>Hayati Kaya</div><div style="line-height:19px;padding-bottom:5px"><span style="color:#999999">Phone Number: </span>+41796336385</div><div style="line-height:19px;padding-bottom:5px"><span style="color:#999999">Email Address: </span>kayahayati@windowslive.com</div><div style="line-height:19px;padding-bottom:5px"><span style="color:#999999">Message: </span>Ich bin an diesem Objekt in Remetchwil interessiert, für das Frau Rudhani zuständig ist, und würde es gerne besichtigen.</div></span></div>
|
|
227
|
+
|
|
228
|
+
</td>
|
|
229
|
+
</tr>
|
|
230
|
+
|
|
231
|
+
<tr>
|
|
232
|
+
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;word-break:break-word;">
|
|
233
|
+
|
|
234
|
+
<div style="font-family:Inter, ui-sans-serif, sans-serif;font-size:14px;letter-spacing:0px;line-height:21px !important;text-align:left;color:#666666;"><span> </span></div>
|
|
235
|
+
|
|
236
|
+
</td>
|
|
237
|
+
</tr>
|
|
238
|
+
|
|
239
|
+
<tr>
|
|
240
|
+
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;word-break:break-word;">
|
|
241
|
+
|
|
242
|
+
<div style="font-family:Inter, ui-sans-serif, sans-serif;font-size:14px;letter-spacing:0px;line-height:21px !important;text-align:left;color:#666666;"><span> </span></div>
|
|
243
|
+
|
|
244
|
+
</td>
|
|
245
|
+
</tr>
|
|
246
|
+
|
|
247
|
+
</tbody>
|
|
248
|
+
</table>
|
|
249
|
+
|
|
250
|
+
</td>
|
|
251
|
+
</tr>
|
|
252
|
+
</tbody>
|
|
253
|
+
</table>
|
|
254
|
+
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
258
|
+
</td>
|
|
259
|
+
</tr>
|
|
260
|
+
</tbody>
|
|
261
|
+
</table>
|
|
262
|
+
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
|
267
|
+
</td>
|
|
268
|
+
</tr>
|
|
269
|
+
</tbody>
|
|
270
|
+
</table>
|
|
271
|
+
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
</body>
|
|
281
|
+
</html>
|
|
282
|
+
`;
|
|
52
283
|
|
|
53
284
|
console.log(HTML_TEMPLATE.replace(/HTML_BODY/g, mimeHtml({ [type]: file })));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parentPort } = require('worker_threads');
|
|
4
|
+
const juice = require('juice');
|
|
5
|
+
|
|
6
|
+
parentPort.on('message', html => {
|
|
7
|
+
try {
|
|
8
|
+
const result = juice(html);
|
|
9
|
+
parentPort.postMessage({ success: true, result });
|
|
10
|
+
} catch (error) {
|
|
11
|
+
parentPort.postMessage({ success: false, error: error.message });
|
|
12
|
+
}
|
|
13
|
+
});
|
package/lib/mime-html.js
CHANGED
|
@@ -4,6 +4,9 @@ const createDOMPurify = require('dompurify');
|
|
|
4
4
|
const { JSDOM } = require('jsdom');
|
|
5
5
|
const juice = require('juice');
|
|
6
6
|
const he = require('he');
|
|
7
|
+
const { Worker } = require('worker_threads');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const WorkerPool = require('./worker-pool');
|
|
7
10
|
|
|
8
11
|
const textToHtml = require('./text-to-html');
|
|
9
12
|
|
|
@@ -60,7 +63,72 @@ const FORBID_STYLES = [
|
|
|
60
63
|
// keep the style at first
|
|
61
64
|
const FORBID_TAGS_ALL = ['style'].concat(FORBID_TAGS);
|
|
62
65
|
|
|
66
|
+
// Global worker pool instance (lazy initialization)
|
|
67
|
+
let globalWorkerPool = null;
|
|
68
|
+
|
|
69
|
+
function getWorkerPool(options = {}) {
|
|
70
|
+
if (!globalWorkerPool) {
|
|
71
|
+
globalWorkerPool = new WorkerPool({
|
|
72
|
+
minWorkers: options.minWorkers || 2,
|
|
73
|
+
maxWorkers: options.maxWorkers || 4,
|
|
74
|
+
workerTimeout: options.timeout || 5000,
|
|
75
|
+
idleTimeout: options.idleTimeout || 30000
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return globalWorkerPool;
|
|
79
|
+
}
|
|
80
|
+
|
|
63
81
|
// define styles for plaintext emails
|
|
82
|
+
// Wrapper for juice that runs in a worker thread with timeout protection
|
|
83
|
+
async function juiceWithTimeout(html, options = {}) {
|
|
84
|
+
const timeout = options.timeout || 5000;
|
|
85
|
+
const usePool = options.useWorkerPool !== false; // Default to true
|
|
86
|
+
|
|
87
|
+
if (usePool) {
|
|
88
|
+
try {
|
|
89
|
+
const pool = getWorkerPool(options);
|
|
90
|
+
return await pool.process(html, { timeout });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Fall back to single worker if pool fails
|
|
93
|
+
if (options.fallbackOnError === false) {
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Single worker implementation (fallback or when pool disabled)
|
|
100
|
+
const timeoutMs = timeout;
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const worker = new Worker(path.join(__dirname, 'juice-worker.js'));
|
|
103
|
+
let timedOut = false;
|
|
104
|
+
|
|
105
|
+
const timeout = setTimeout(() => {
|
|
106
|
+
timedOut = true;
|
|
107
|
+
worker.terminate();
|
|
108
|
+
reject(new Error('Juice processing timed out - likely due to unsupported CSS selectors'));
|
|
109
|
+
}, timeoutMs);
|
|
110
|
+
|
|
111
|
+
worker.on('message', msg => {
|
|
112
|
+
clearTimeout(timeout);
|
|
113
|
+
worker.terminate();
|
|
114
|
+
if (msg.success) {
|
|
115
|
+
resolve(msg.result);
|
|
116
|
+
} else {
|
|
117
|
+
reject(new Error(msg.error));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
worker.on('error', err => {
|
|
122
|
+
clearTimeout(timeout);
|
|
123
|
+
if (!timedOut) {
|
|
124
|
+
reject(err);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
worker.postMessage(html);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
64
132
|
const INLINE_STYLE_BLOCK = `<style>
|
|
65
133
|
|
|
66
134
|
body, td, th, p {
|
|
@@ -96,7 +164,7 @@ blockquote blockquote blockquote blockquote{
|
|
|
96
164
|
|
|
97
165
|
</style>`;
|
|
98
166
|
|
|
99
|
-
function
|
|
167
|
+
function mimeHtmlSync(message) {
|
|
100
168
|
let html = message.html;
|
|
101
169
|
if (!html && message.text) {
|
|
102
170
|
html = textToHtml(message.text) || '';
|
|
@@ -105,32 +173,46 @@ function mimeHtml(message) {
|
|
|
105
173
|
}
|
|
106
174
|
}
|
|
107
175
|
|
|
108
|
-
html = html.toString().replace(/^"data:[^"]+"/g, '');
|
|
109
|
-
console.log('HTML', html);
|
|
110
|
-
try {
|
|
111
|
-
//inline styles
|
|
112
|
-
//html = juice(html);
|
|
113
|
-
} catch (err) {
|
|
114
|
-
// should log?
|
|
115
|
-
}
|
|
176
|
+
html = (html || '').toString().replace(/^"data:[^"]+"/g, '');
|
|
116
177
|
|
|
117
|
-
console.log(1, Date.now());
|
|
118
178
|
const window = new JSDOM('').window;
|
|
119
|
-
|
|
179
|
+
|
|
120
180
|
const DOMPurify = createDOMPurify(window);
|
|
121
|
-
|
|
181
|
+
|
|
122
182
|
// first pass, outputs a fixed HTML page
|
|
123
183
|
html = DOMPurify.sanitize(html, { WHOLE_DOCUMENT: true, FORCE_BODY: false, FORBID_TAGS });
|
|
124
|
-
|
|
184
|
+
|
|
125
185
|
try {
|
|
126
|
-
//
|
|
186
|
+
// Pre-process HTML to remove known problematic CSS that causes juice to hang
|
|
187
|
+
// This is a fallback for the sync version - the async version with timeout is preferred
|
|
188
|
+
const tempDoc = new JSDOM(html).window.document;
|
|
189
|
+
|
|
190
|
+
// Remove any style tags from within body content
|
|
191
|
+
const bodyStyleTags = tempDoc.body ? tempDoc.body.querySelectorAll('style') : [];
|
|
192
|
+
bodyStyleTags.forEach(tag => tag.remove());
|
|
193
|
+
|
|
194
|
+
// Process remaining style tags to remove known problematic selectors
|
|
195
|
+
const styleTags = tempDoc.querySelectorAll('style');
|
|
196
|
+
styleTags.forEach(styleTag => {
|
|
197
|
+
let cssContent = styleTag.textContent;
|
|
198
|
+
|
|
199
|
+
// Remove rules with known problematic pseudo-classes
|
|
200
|
+
const problematicSelectors = [':is\\(', ':where\\(', ':has\\(', ':not\\(.*:.*\\)'];
|
|
201
|
+
problematicSelectors.forEach(selector => {
|
|
202
|
+
const regex = new RegExp(`[^{}]*${selector}[^{}]*\\{[^}]*\\}`, 'gi');
|
|
203
|
+
cssContent = cssContent.replace(regex, '');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
styleTag.textContent = cssContent;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
html = tempDoc.documentElement.outerHTML;
|
|
210
|
+
|
|
211
|
+
// Run juice synchronously - risky but backwards compatible
|
|
127
212
|
html = juice(html);
|
|
128
|
-
} catch (
|
|
129
|
-
console.log(err);
|
|
213
|
+
} catch (_err) {
|
|
130
214
|
// not so important, so we'll ignore if style inlining fails
|
|
131
|
-
// should log?
|
|
132
215
|
}
|
|
133
|
-
console.log(5, Date.now());
|
|
134
216
|
|
|
135
217
|
// second pass, outputs a BODY DOM element
|
|
136
218
|
const dom = DOMPurify.sanitize(html, {
|
|
@@ -139,7 +221,6 @@ function mimeHtml(message) {
|
|
|
139
221
|
FORCE_BODY: true,
|
|
140
222
|
FORBID_TAGS: FORBID_TAGS_ALL
|
|
141
223
|
});
|
|
142
|
-
console.log(6, Date.now());
|
|
143
224
|
|
|
144
225
|
// mark all links to be opened in a new window
|
|
145
226
|
for (let elm of dom.querySelectorAll('a')) {
|
|
@@ -157,7 +238,6 @@ function mimeHtml(message) {
|
|
|
157
238
|
dom.style.removeProperty('min-height');
|
|
158
239
|
dom.style.removeProperty('max-height');
|
|
159
240
|
|
|
160
|
-
console.log(7, Date.now());
|
|
161
241
|
// remove specific style properties for all DOM nodes
|
|
162
242
|
let walkDom = node => {
|
|
163
243
|
if (node.style) {
|
|
@@ -167,18 +247,162 @@ function mimeHtml(message) {
|
|
|
167
247
|
}
|
|
168
248
|
|
|
169
249
|
if (node.childNodes) {
|
|
170
|
-
|
|
250
|
+
// Convert NodeList to array to avoid infinite loops if DOM changes during iteration
|
|
251
|
+
const children = Array.from(node.childNodes);
|
|
252
|
+
for (let childNode of children) {
|
|
171
253
|
walkDom(childNode);
|
|
172
254
|
}
|
|
173
255
|
}
|
|
174
256
|
};
|
|
175
257
|
walkDom(dom);
|
|
176
258
|
|
|
177
|
-
console.log(8, Date.now());
|
|
178
259
|
let bodyStyles = (dom.getAttribute('style') || '').toString().trim();
|
|
179
260
|
|
|
180
261
|
// embed into a styled container
|
|
181
262
|
return `<div style="${he.encode(bodyStyles)}">${dom.innerHTML.trim()}</div>`;
|
|
182
263
|
}
|
|
183
264
|
|
|
265
|
+
async function mimeHtmlAsync(message, options = {}) {
|
|
266
|
+
let html = message.html;
|
|
267
|
+
if (!html && message.text) {
|
|
268
|
+
html = textToHtml(message.text) || '';
|
|
269
|
+
if (html) {
|
|
270
|
+
html = INLINE_STYLE_BLOCK + html;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
html = (html || '').toString().replace(/^"data:[^"]+"/g, '');
|
|
275
|
+
|
|
276
|
+
const window = new JSDOM('').window;
|
|
277
|
+
const DOMPurify = createDOMPurify(window);
|
|
278
|
+
|
|
279
|
+
// first pass, outputs a fixed HTML page
|
|
280
|
+
html = DOMPurify.sanitize(html, { WHOLE_DOCUMENT: true, FORCE_BODY: false, FORBID_TAGS });
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
// Pre-process HTML to remove known problematic CSS that causes juice to hang
|
|
284
|
+
// This is the same filtering as the sync version to avoid unnecessary timeouts
|
|
285
|
+
const tempDoc = new JSDOM(html).window.document;
|
|
286
|
+
|
|
287
|
+
// Remove any style tags from within body content
|
|
288
|
+
const bodyStyleTags = tempDoc.body ? tempDoc.body.querySelectorAll('style') : [];
|
|
289
|
+
bodyStyleTags.forEach(tag => tag.remove());
|
|
290
|
+
|
|
291
|
+
// Process remaining style tags to remove known problematic selectors
|
|
292
|
+
const styleTags = tempDoc.querySelectorAll('style');
|
|
293
|
+
styleTags.forEach(styleTag => {
|
|
294
|
+
let cssContent = styleTag.textContent;
|
|
295
|
+
|
|
296
|
+
// Remove rules with known problematic pseudo-classes
|
|
297
|
+
const problematicSelectors = [':is\\(', ':where\\(', ':has\\(', ':not\\(.*:.*\\)'];
|
|
298
|
+
problematicSelectors.forEach(selector => {
|
|
299
|
+
const regex = new RegExp(`[^{}]*${selector}[^{}]*\\{[^}]*\\}`, 'gi');
|
|
300
|
+
cssContent = cssContent.replace(regex, '');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
styleTag.textContent = cssContent;
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
html = tempDoc.documentElement.outerHTML;
|
|
307
|
+
|
|
308
|
+
// Use worker thread with timeout for juice - provides protection against ANY
|
|
309
|
+
// unexpected CSS that might cause hangs (beyond our known problematic selectors)
|
|
310
|
+
html = await juiceWithTimeout(html, options);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
// If juice fails or times out, continue without inlining styles
|
|
313
|
+
// The HTML is still usable, just without inlined CSS
|
|
314
|
+
// Note: logErrors option can be used to control error visibility
|
|
315
|
+
|
|
316
|
+
if (options.fallbackOnError === false) {
|
|
317
|
+
throw err;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// second pass, outputs a BODY DOM element
|
|
322
|
+
const dom = DOMPurify.sanitize(html, {
|
|
323
|
+
RETURN_DOM: true,
|
|
324
|
+
WHOLE_DOCUMENT: false,
|
|
325
|
+
FORCE_BODY: true,
|
|
326
|
+
FORBID_TAGS: FORBID_TAGS_ALL
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// mark all links to be opened in a new window
|
|
330
|
+
for (let elm of dom.querySelectorAll('a')) {
|
|
331
|
+
elm.setAttribute('target', '_blank');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
dom.style.overflow = 'auto';
|
|
335
|
+
|
|
336
|
+
// return <BODY> HTML
|
|
337
|
+
dom.style.removeProperty('width');
|
|
338
|
+
dom.style.removeProperty('min-width');
|
|
339
|
+
dom.style.removeProperty('max-width');
|
|
340
|
+
dom.style.removeProperty('height');
|
|
341
|
+
dom.style.removeProperty('min-height');
|
|
342
|
+
dom.style.removeProperty('max-height');
|
|
343
|
+
|
|
344
|
+
// remove specific style properties for all DOM nodes
|
|
345
|
+
let walkDom = node => {
|
|
346
|
+
if (node.style) {
|
|
347
|
+
for (let disallowedStyle of FORBID_STYLES) {
|
|
348
|
+
node.style.removeProperty(disallowedStyle);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (node.childNodes) {
|
|
353
|
+
// Convert NodeList to array to avoid infinite loops if DOM changes during iteration
|
|
354
|
+
const children = Array.from(node.childNodes);
|
|
355
|
+
for (let childNode of children) {
|
|
356
|
+
walkDom(childNode);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
walkDom(dom);
|
|
361
|
+
|
|
362
|
+
let bodyStyles = (dom.getAttribute('style') || '').toString().trim();
|
|
363
|
+
|
|
364
|
+
// embed into a styled container
|
|
365
|
+
return `<div style="${he.encode(bodyStyles)}">${dom.innerHTML.trim()}</div>`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Main export - maintains backward compatibility with sync API
|
|
369
|
+
// Architecture:
|
|
370
|
+
// - Both sync and async versions pre-filter known problematic CSS selectors
|
|
371
|
+
// - Sync version: Pre-filters selectors, then runs juice synchronously (can still hang on unknown issues)
|
|
372
|
+
// - Async version: Pre-filters selectors, then runs juice in worker with timeout (fully protected)
|
|
373
|
+
// The timeout in async is a safety net for unknown edge cases, not the primary defense
|
|
374
|
+
function mimeHtml(message) {
|
|
375
|
+
// Use the sync version for backward compatibility
|
|
376
|
+
return mimeHtmlSync(message);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Export both versions
|
|
380
|
+
mimeHtml.async = mimeHtmlAsync;
|
|
381
|
+
mimeHtml.sync = mimeHtmlSync;
|
|
382
|
+
|
|
383
|
+
// Close worker pool when process exits
|
|
384
|
+
if (globalWorkerPool) {
|
|
385
|
+
process.on('exit', () => {
|
|
386
|
+
if (globalWorkerPool) {
|
|
387
|
+
globalWorkerPool.close().catch(() => {});
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Export function to manually close worker pool
|
|
393
|
+
mimeHtml.closeWorkerPool = async () => {
|
|
394
|
+
if (globalWorkerPool) {
|
|
395
|
+
await globalWorkerPool.close();
|
|
396
|
+
globalWorkerPool = null;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// Export function to get worker pool stats
|
|
401
|
+
mimeHtml.getWorkerPoolStats = () => {
|
|
402
|
+
if (globalWorkerPool) {
|
|
403
|
+
return globalWorkerPool.getStats();
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
};
|
|
407
|
+
|
|
184
408
|
module.exports = mimeHtml;
|