@postalsys/email-text-tools 2.1.16 → 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.
@@ -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 = `<html>
10
- <head>
11
- <meta charset="utf-8">
12
- <title>test email</title>
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
- <style>
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
- body {
17
- background: white;
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
- </style>
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
- <div class="email-container">
43
- HTML_BODY
44
- </div>
60
+ <style type="text/css">
45
61
 
46
- <p>Footer text</p>
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
- </div>
67
+ </style>
49
68
 
50
- </body>
51
- </html>`;
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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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 mimeHtml(message) {
167
+ function mimeHtmlSync(message) {
100
168
  let html = message.html;
101
169
  if (!html && message.text) {
102
170
  html = textToHtml(message.text) || '';
@@ -105,14 +173,7 @@ function mimeHtml(message) {
105
173
  }
106
174
  }
107
175
 
108
- html = html.toString().replace(/^"data:[^"]+"/g, '');
109
-
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
178
  const window = new JSDOM('').window;
118
179
 
@@ -122,11 +183,35 @@ function mimeHtml(message) {
122
183
  html = DOMPurify.sanitize(html, { WHOLE_DOCUMENT: true, FORCE_BODY: false, FORBID_TAGS });
123
184
 
124
185
  try {
125
- //inline styles
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
126
212
  html = juice(html);
127
- } catch (err) {
213
+ } catch (_err) {
128
214
  // not so important, so we'll ignore if style inlining fails
129
- // should log?
130
215
  }
131
216
 
132
217
  // second pass, outputs a BODY DOM element
@@ -162,7 +247,9 @@ function mimeHtml(message) {
162
247
  }
163
248
 
164
249
  if (node.childNodes) {
165
- for (let childNode of node.childNodes) {
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) {
166
253
  walkDom(childNode);
167
254
  }
168
255
  }
@@ -175,4 +262,147 @@ function mimeHtml(message) {
175
262
  return `<div style="${he.encode(bodyStyles)}">${dom.innerHTML.trim()}</div>`;
176
263
  }
177
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
+
178
408
  module.exports = mimeHtml;
@@ -114,7 +114,7 @@ function linkifyAndHtmlEscape(text) {
114
114
  })
115
115
  .join('');
116
116
  }
117
- } catch (err) {
117
+ } catch (_err) {
118
118
  // ignore?
119
119
  }
120
120