@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
@@ -1,75 +0,0 @@
1
- name: '@postalsys/email-text-tools security vulnerability report'
2
- description: 'Report an exploitable security vulnerability in @postalsys/email-text-tools'
3
-
4
- body:
5
- - type: markdown
6
- attributes:
7
- value: |
8
- Before reporting, read our
9
- [Security Policy](https://github.com/postalsys/email-text-tools/security/policy).
10
-
11
- Reports must demonstrate a credible security impact.
12
- Technically valid bugs without a realistic attack
13
- scenario belong in the regular issue tracker.
14
-
15
- - type: textarea
16
- id: summary
17
- attributes:
18
- label: Vulnerability summary
19
- description: Describe the vulnerability and affected versions.
20
- validations:
21
- required: true
22
-
23
- - type: textarea
24
- id: attacker
25
- attributes:
26
- label: Attacker capabilities
27
- description: |
28
- What can an attacker control without already having
29
- privileged access to the application or its configuration?
30
- validations:
31
- required: true
32
- min_length: 100
33
-
34
- - type: textarea
35
- id: exploit
36
- attributes:
37
- label: Realistic exploitation scenario
38
- description: |
39
- Describe how this can be exploited in a real application.
40
- Explain the prerequisites and security boundary crossed.
41
- validations:
42
- required: true
43
- min_length: 150
44
-
45
- - type: textarea
46
- id: poc
47
- attributes:
48
- label: Proof of concept
49
- description: Provide reproducible steps or working code.
50
- validations:
51
- required: true
52
-
53
- - type: textarea
54
- id: impact
55
- attributes:
56
- label: Security impact
57
- description: |
58
- What can the attacker achieve? Who is affected?
59
- Explain why this is a security vulnerability rather
60
- than an ordinary bug.
61
- validations:
62
- required: true
63
- min_length: 100
64
-
65
- - type: checkboxes
66
- id: confirmation
67
- attributes:
68
- label: Reporting requirements
69
- options:
70
- - label: I have read the Security Policy.
71
- required: true
72
- - label: I have verified the finding against a supported version.
73
- required: true
74
- - label: I have described a credible attacker-controlled scenario.
75
- required: true
@@ -1,37 +0,0 @@
1
- on:
2
- push:
3
- branches:
4
- - main
5
-
6
- permissions:
7
- contents: write
8
- pull-requests: write
9
- issues: write
10
- id-token: write
11
-
12
- name: release
13
- jobs:
14
- release-please:
15
- runs-on: ubuntu-latest
16
- outputs:
17
- release_created: ${{ steps.release.outputs.release_created }}
18
- steps:
19
- - uses: googleapis/release-please-action@v5
20
- id: release
21
-
22
- publish:
23
- needs: release-please
24
- if: ${{ needs.release-please.outputs.release_created }}
25
- runs-on: ubuntu-latest
26
- environment: npm-publish
27
- permissions:
28
- contents: read
29
- id-token: write
30
- steps:
31
- - uses: actions/checkout@v6
32
- - uses: actions/setup-node@v6
33
- with:
34
- node-version: 24
35
- registry-url: 'https://registry.npmjs.org'
36
- - run: npm ci
37
- - run: npm publish --provenance --access public
@@ -1,31 +0,0 @@
1
- name: Run tests
2
-
3
- on:
4
- push:
5
- pull_request:
6
-
7
- permissions:
8
- contents: read
9
-
10
- concurrency:
11
- group: ${{ github.workflow }}-${{ github.ref }}
12
- cancel-in-progress: true
13
-
14
- jobs:
15
- test:
16
- strategy:
17
- matrix:
18
- node: [22.x, 24.x]
19
- os: [ubuntu-latest]
20
- runs-on: ${{ matrix.os }}
21
- timeout-minutes: 10
22
- steps:
23
- - uses: actions/checkout@v6
24
- - name: Use Node.js ${{ matrix.node }}
25
- uses: actions/setup-node@v6
26
- with:
27
- node-version: ${{ matrix.node }}
28
- cache: npm
29
- - run: npm install
30
- - run: npm run lint
31
- - run: npm test
package/.ncurc.js DELETED
@@ -1,22 +0,0 @@
1
- module.exports = {
2
- upgrade: true,
3
- reject: [
4
- // Block package upgrades that moved to ESM
5
- 'nanoid',
6
- 'jsdom', // parse5 dependency in 27.0.1 is ESM only
7
- 'juice', // 12+ is pure ESM (requires Node >=22.12) and breaks pkg builds
8
- // node-html-parser was rejected here on the grounds that 8+ depends on the pure-ESM entities@8.
9
- // That is not how it ships: node-html-parser 9 bundles entities inline into dist/index.cjs and
10
- // never require()s the package, so the ESM copy is installed but never loaded. Unpinned 2026-08,
11
- // which also drops a duplicate parser from EmailEngine's tree (@postalsys/email-content already
12
- // pulls 9.x). Keep it unpinned unless a release starts require()ing entities for real.
13
- // 6.0.0 changed the CommonJS export shape (named bindings instead of the constructor) and,
14
- // more importantly, stopped linkifying bare domains such as 'example.com' or 'www.example.org'
15
- // by default, which silently breaks autolinking in plain text emails. Stay on 5.x.
16
- 'linkify-it',
17
- // 2.0.0 is ESM-only (exports only src/he.mjs) and requires Node >=22, so require('he') throws
18
- // and pkg cannot bundle it. 1.2.0 is the last CommonJS release; EmailEngine and mailparser
19
- // hold it there too, and a different copy here would be a second he in their trees
20
- 'he'
21
- ]
22
- };
package/.prettierignore DELETED
@@ -1,14 +0,0 @@
1
- # Auto-generated files
2
- CHANGELOG.md
3
-
4
- # Dependencies
5
- node_modules/
6
- package-lock.json
7
-
8
- # Build output
9
- dist/
10
- build/
11
- coverage/
12
-
13
- # Logs
14
- *.log
package/.prettierrc.js DELETED
@@ -1,8 +0,0 @@
1
- module.exports = {
2
- printWidth: 160,
3
- tabWidth: 4,
4
- singleQuote: true,
5
- endOfLine: 'lf',
6
- trailingComma: 'none',
7
- arrowParens: 'avoid'
8
- };
@@ -1,3 +0,0 @@
1
- {
2
- ".": "2.4.25"
3
- }
@@ -1,358 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const mimeHtml = require('../lib/mime-html');
7
-
8
- // Test data sets
9
- const testCases = {
10
- simple: {
11
- name: 'Simple HTML',
12
- html: `
13
- <!DOCTYPE html>
14
- <html>
15
- <head>
16
- <style>
17
- body { font-family: Arial; }
18
- p { color: blue; }
19
- </style>
20
- </head>
21
- <body>
22
- <h1>Hello World</h1>
23
- <p>This is a simple test email.</p>
24
- </body>
25
- </html>
26
- `
27
- },
28
-
29
- moderate: {
30
- name: 'Moderate HTML with inline styles',
31
- html: `
32
- <!DOCTYPE html>
33
- <html>
34
- <head>
35
- <style>
36
- body { font-family: Arial, sans-serif; background: #f0f0f0; }
37
- .container { max-width: 600px; margin: 0 auto; padding: 20px; }
38
- h1 { color: #333; border-bottom: 2px solid #007bff; }
39
- p { line-height: 1.6; color: #666; }
40
- .button { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; }
41
- .footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; }
42
- </style>
43
- </head>
44
- <body>
45
- <div class="container">
46
- <h1>Newsletter Title</h1>
47
- <p>This is a moderately complex email template with various styles.</p>
48
- <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
49
- <a href="#" class="button">Call to Action</a>
50
- <div class="footer">
51
- <p>Footer content here</p>
52
- </div>
53
- </div>
54
- </body>
55
- </html>
56
- `
57
- },
58
-
59
- complex: {
60
- name: 'Complex HTML with media queries',
61
- html: `
62
- <!DOCTYPE html>
63
- <html>
64
- <head>
65
- <style>
66
- * { margin: 0; padding: 0; box-sizing: border-box; }
67
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; }
68
- .wrapper { max-width: 600px; margin: 0 auto; }
69
- .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 20px; }
70
- .header h1 { color: white; font-size: 32px; }
71
- .content { padding: 40px 20px; background: white; }
72
- .card { border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
73
- .card h2 { color: #333; margin-bottom: 10px; }
74
- .grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
75
- @media only screen and (max-width: 600px) {
76
- .grid { grid-template-columns: 1fr; }
77
- .header h1 { font-size: 24px; }
78
- }
79
- table { width: 100%; border-collapse: collapse; }
80
- th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
81
- .button { display: inline-block; padding: 12px 24px; background: #007bff; color: white; text-decoration: none; border-radius: 4px; }
82
- .footer { background: #f8f9fa; padding: 30px 20px; text-align: center; color: #6c757d; }
83
- </style>
84
- </head>
85
- <body>
86
- <div class="wrapper">
87
- <div class="header">
88
- <h1>Complex Email Template</h1>
89
- </div>
90
- <div class="content">
91
- <div class="card">
92
- <h2>Feature One</h2>
93
- <p>Description of the first feature with detailed information.</p>
94
- </div>
95
- <div class="grid">
96
- <div class="card">
97
- <h2>Item 1</h2>
98
- <p>Grid item content</p>
99
- </div>
100
- <div class="card">
101
- <h2>Item 2</h2>
102
- <p>Grid item content</p>
103
- </div>
104
- </div>
105
- <table>
106
- <tr><th>Product</th><th>Price</th><th>Quantity</th></tr>
107
- <tr><td>Item A</td><td>$10</td><td>2</td></tr>
108
- <tr><td>Item B</td><td>$20</td><td>1</td></tr>
109
- </table>
110
- <a href="#" class="button">Shop Now</a>
111
- </div>
112
- <div class="footer">
113
- <p>Copyright 2024. All rights reserved.</p>
114
- </div>
115
- </div>
116
- </body>
117
- </html>
118
- `
119
- },
120
-
121
- problematic: {
122
- name: 'HTML with problematic CSS (modern selectors)',
123
- html: `
124
- <!DOCTYPE html>
125
- <html>
126
- <head>
127
- <style>
128
- body { font-family: system-ui, sans-serif; }
129
- /* Modern CSS that might cause issues */
130
- .container:is(.active, .highlighted) { background: yellow; }
131
- .item:where(.primary) { color: blue; }
132
- .parent:has(> .child) { border: 1px solid red; }
133
- /* Standard CSS */
134
- h1 { color: #333; }
135
- p { line-height: 1.6; }
136
- .box:not(.special) { padding: 10px; }
137
- </style>
138
- </head>
139
- <body>
140
- <div class="container active">
141
- <h1>Test with Modern CSS</h1>
142
- <div class="parent">
143
- <div class="child">Child element</div>
144
- </div>
145
- <p class="item primary">This uses modern selectors</p>
146
- <div class="box">Regular box</div>
147
- <div class="box special">Special box</div>
148
- </div>
149
- </body>
150
- </html>
151
- `
152
- }
153
- };
154
-
155
- // Load the failing.html if it exists
156
- try {
157
- const failingHtml = fs.readFileSync(path.join(__dirname, '../examples/failing.html'), 'utf8');
158
- testCases.realWorld = {
159
- name: 'Real-world problematic HTML',
160
- html: failingHtml
161
- };
162
- } catch (_err) {
163
- // File doesn't exist, skip it
164
- }
165
-
166
- // Benchmark runner
167
- class Benchmark {
168
- constructor() {
169
- this.results = [];
170
- }
171
-
172
- async runTest(name, fn, iterations = 100) {
173
- const times = [];
174
- const errors = [];
175
-
176
- // Warmup
177
- for (let i = 0; i < 5; i++) {
178
- try {
179
- await fn();
180
- } catch (_err) {
181
- // Ignore warmup errors
182
- }
183
- }
184
-
185
- // Actual test
186
- for (let i = 0; i < iterations; i++) {
187
- const start = process.hrtime.bigint();
188
- try {
189
- await fn();
190
- const end = process.hrtime.bigint();
191
- times.push(Number(end - start) / 1000000); // Convert to milliseconds
192
- } catch (err) {
193
- errors.push(err.message);
194
- }
195
- }
196
-
197
- if (times.length > 0) {
198
- times.sort((a, b) => a - b);
199
- const avg = times.reduce((a, b) => a + b, 0) / times.length;
200
- const median = times[Math.floor(times.length / 2)];
201
- const min = times[0];
202
- const max = times[times.length - 1];
203
- const p95 = times[Math.floor(times.length * 0.95)];
204
- const p99 = times[Math.floor(times.length * 0.99)];
205
-
206
- return {
207
- name,
208
- iterations,
209
- successful: times.length,
210
- failed: errors.length,
211
- avg: avg.toFixed(2),
212
- median: median.toFixed(2),
213
- min: min.toFixed(2),
214
- max: max.toFixed(2),
215
- p95: p95.toFixed(2),
216
- p99: p99.toFixed(2),
217
- errors: errors.length > 0 ? errors[0] : null
218
- };
219
- } else {
220
- return {
221
- name,
222
- iterations,
223
- successful: 0,
224
- failed: errors.length,
225
- errors: errors[0] || 'All iterations failed'
226
- };
227
- }
228
- }
229
-
230
- formatResults(results) {
231
- console.log('\n' + '='.repeat(80));
232
- console.log(`Test Case: ${results.testCase}`);
233
- console.log('='.repeat(80));
234
-
235
- const table = [];
236
- results.tests.forEach(test => {
237
- if (test.successful > 0) {
238
- table.push({
239
- Method: test.name,
240
- 'Success Rate': `${test.successful}/${test.iterations}`,
241
- 'Avg (ms)': test.avg,
242
- 'Median (ms)': test.median,
243
- 'Min (ms)': test.min,
244
- 'Max (ms)': test.max,
245
- 'P95 (ms)': test.p95,
246
- 'P99 (ms)': test.p99
247
- });
248
- } else {
249
- table.push({
250
- Method: test.name,
251
- 'Success Rate': `${test.successful}/${test.iterations}`,
252
- Error: test.errors
253
- });
254
- }
255
- });
256
-
257
- console.table(table);
258
-
259
- // Calculate speedup if both succeeded
260
- const syncTest = results.tests.find(t => t.name === 'Sync');
261
- const asyncTest = results.tests.find(t => t.name === 'Async (with pool)');
262
-
263
- if (syncTest && asyncTest && syncTest.successful > 0 && asyncTest.successful > 0) {
264
- const speedup = (parseFloat(syncTest.avg) / parseFloat(asyncTest.avg)).toFixed(2);
265
- if (speedup > 1) {
266
- console.log(`\nAsync is ${speedup}x faster than Sync`);
267
- } else {
268
- console.log(`\nSync is ${(1 / speedup).toFixed(2)}x faster than Async`);
269
- }
270
- }
271
- }
272
-
273
- async runBenchmarks() {
274
- console.log('Starting Mime HTML Benchmarks...');
275
- console.log('================================\n');
276
-
277
- for (const [_key, testCase] of Object.entries(testCases)) {
278
- const results = {
279
- testCase: testCase.name,
280
- tests: []
281
- };
282
-
283
- // Test sync version
284
- results.tests.push(
285
- await this.runTest(
286
- 'Sync',
287
- () => {
288
- return Promise.resolve(mimeHtml.sync({ html: testCase.html }));
289
- },
290
- 100
291
- )
292
- );
293
-
294
- // Test async version with pool
295
- results.tests.push(
296
- await this.runTest(
297
- 'Async (with pool)',
298
- async () => {
299
- return await mimeHtml.async(
300
- { html: testCase.html },
301
- {
302
- useWorkerPool: true,
303
- timeout: 5000,
304
- minWorkers: 2,
305
- maxWorkers: 4
306
- }
307
- );
308
- },
309
- 100
310
- )
311
- );
312
-
313
- // Test async version without pool
314
- results.tests.push(
315
- await this.runTest(
316
- 'Async (no pool)',
317
- async () => {
318
- return await mimeHtml.async(
319
- { html: testCase.html },
320
- {
321
- useWorkerPool: false,
322
- timeout: 5000
323
- }
324
- );
325
- },
326
- 100
327
- )
328
- );
329
-
330
- this.formatResults(results);
331
- }
332
-
333
- // Show worker pool stats
334
- const stats = mimeHtml.getWorkerPoolStats();
335
- if (stats) {
336
- console.log('\n' + '='.repeat(80));
337
- console.log('Worker Pool Stats:');
338
- console.log('='.repeat(80));
339
- console.table([stats]);
340
- }
341
-
342
- // Clean up
343
- await mimeHtml.closeWorkerPool();
344
- }
345
- }
346
-
347
- // Run benchmarks
348
- const benchmark = new Benchmark();
349
- benchmark
350
- .runBenchmarks()
351
- .then(() => {
352
- console.log('\nBenchmark complete!');
353
- process.exit(0);
354
- })
355
- .catch(err => {
356
- console.error('Benchmark failed:', err);
357
- process.exit(1);
358
- });