@postalsys/email-text-tools 1.0.0 → 1.1.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/examples/mime-html.js +53 -0
- package/index.js +2 -1
- package/lib/mime-html.js +100 -0
- package/lib/text-to-html.js +14 -3
- package/package.json +9 -6
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const mimeHtml = require('../lib/mime-html');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
const type = process.argv[3] || 'html';
|
|
7
|
+
const file = fs.readFileSync(process.argv[2]);
|
|
8
|
+
|
|
9
|
+
const HTML_TEMPLATE = `<html>
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="utf-8">
|
|
12
|
+
<title>test email</title>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
background: white;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.body-container {
|
|
21
|
+
max-width: 840px;
|
|
22
|
+
margin: 30px auto;
|
|
23
|
+
padding: 20px;
|
|
24
|
+
background: #fafafa;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.email-container {
|
|
28
|
+
padding: 20px;
|
|
29
|
+
border: 1px solid #ccc;
|
|
30
|
+
background: white;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
</style>
|
|
34
|
+
|
|
35
|
+
<head>
|
|
36
|
+
<body>
|
|
37
|
+
|
|
38
|
+
<div class="body-container">
|
|
39
|
+
|
|
40
|
+
<h1>Email message</h1>
|
|
41
|
+
|
|
42
|
+
<div class="email-container">
|
|
43
|
+
HTML_BODY
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<p>Footer text</p>
|
|
47
|
+
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
</body>
|
|
51
|
+
</html>`;
|
|
52
|
+
|
|
53
|
+
console.log(HTML_TEMPLATE.replace(/HTML_BODY/g, mimeHtml({ [type]: file })));
|
package/index.js
CHANGED
|
@@ -4,5 +4,6 @@ const textToHtml = require('./lib/text-to-html');
|
|
|
4
4
|
const htmlToText = require('./lib/html-to-text');
|
|
5
5
|
const inlineHtml = require('./lib/inline-html');
|
|
6
6
|
const inlineText = require('./lib/inline-text');
|
|
7
|
+
const mimeHtml = require('./lib/mime-html');
|
|
7
8
|
|
|
8
|
-
module.exports = { textToHtml, htmlToText, inlineHtml, inlineText };
|
|
9
|
+
module.exports = { textToHtml, htmlToText, inlineHtml, inlineText, mimeHtml };
|
package/lib/mime-html.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const createDOMPurify = require('dompurify');
|
|
4
|
+
const { JSDOM } = require('jsdom');
|
|
5
|
+
const juice = require('juice');
|
|
6
|
+
const he = require('he');
|
|
7
|
+
|
|
8
|
+
const textToHtml = require('./text-to-html');
|
|
9
|
+
|
|
10
|
+
// define styles for plaintext emails
|
|
11
|
+
const INLINE_STYLE_BLOCK = `<style>
|
|
12
|
+
|
|
13
|
+
body, td, th, p {
|
|
14
|
+
font-family: sans-serif;
|
|
15
|
+
font-size: 12px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
blockquote {
|
|
19
|
+
border-left-width: 2px;
|
|
20
|
+
border-left-style: solid;
|
|
21
|
+
|
|
22
|
+
border-left-color: darkblue;
|
|
23
|
+
color: darkblue;
|
|
24
|
+
|
|
25
|
+
margin: 1rem 0;
|
|
26
|
+
padding-left: 1rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
blockquote blockquote{
|
|
30
|
+
border-left-color: royalblue;
|
|
31
|
+
color: royalblue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
blockquote blockquote blockquote{
|
|
35
|
+
border-left-color: dodgerblue;
|
|
36
|
+
color: dodgerblue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
blockquote blockquote blockquote blockquote{
|
|
40
|
+
border-left-color: darkblue;
|
|
41
|
+
color: darkblue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
</style>`;
|
|
45
|
+
|
|
46
|
+
function mimeHtml(message) {
|
|
47
|
+
let html = message.html;
|
|
48
|
+
if (!html && message.text) {
|
|
49
|
+
html = textToHtml(message.text) || '';
|
|
50
|
+
if (html) {
|
|
51
|
+
html = INLINE_STYLE_BLOCK + html;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
//inline styles
|
|
57
|
+
//html = juice(html);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// should log?
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const window = new JSDOM('').window;
|
|
63
|
+
const DOMPurify = createDOMPurify(window);
|
|
64
|
+
|
|
65
|
+
// first pass, outputs a fixed HTML page
|
|
66
|
+
html = DOMPurify.sanitize(html, { WHOLE_DOCUMENT: true, FORCE_BODY: false, FORBID_TAGS: ['title', 'link', 'meta'] });
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
//inline styles
|
|
70
|
+
html = juice(html);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// not so important, so we'll ignore if style inlining fails
|
|
73
|
+
// should log?
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// second pass, outputs a BODY DOM element
|
|
77
|
+
const dom = DOMPurify.sanitize(html, { RETURN_DOM: true, WHOLE_DOCUMENT: false, FORCE_BODY: true, FORBID_TAGS: ['style', 'title', 'link', 'meta'] });
|
|
78
|
+
|
|
79
|
+
// mark all links to be opened in a new window
|
|
80
|
+
for (let elm of dom.querySelectorAll('a')) {
|
|
81
|
+
elm.setAttribute('target', '_blank');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
dom.style.overflow = 'auto';
|
|
85
|
+
|
|
86
|
+
// return <BODY> HTML
|
|
87
|
+
|
|
88
|
+
dom.style.removeProperty('width');
|
|
89
|
+
dom.style.removeProperty('min-width');
|
|
90
|
+
dom.style.removeProperty('max-width');
|
|
91
|
+
dom.style.removeProperty('height');
|
|
92
|
+
dom.style.removeProperty('min-height');
|
|
93
|
+
dom.style.removeProperty('max-height');
|
|
94
|
+
let bodyStyles = (dom.getAttribute('style') || '').toString().trim();
|
|
95
|
+
|
|
96
|
+
// embed into a styled container
|
|
97
|
+
return `<div style="${he.encode(bodyStyles)}">${dom.innerHTML.trim()}</div>`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = mimeHtml;
|
package/lib/text-to-html.js
CHANGED
|
@@ -101,9 +101,10 @@ function linkifyAndHtmlEscape(text) {
|
|
|
101
101
|
return parts
|
|
102
102
|
.map(part => {
|
|
103
103
|
switch (part.type) {
|
|
104
|
-
case 'text':
|
|
104
|
+
case 'text': {
|
|
105
105
|
// normal text, escape HTML
|
|
106
106
|
return he.encode(part.content, { useNamedReferences: true });
|
|
107
|
+
}
|
|
107
108
|
case 'link':
|
|
108
109
|
// URL with html escaped text content and URL
|
|
109
110
|
return `<a href="${he.encode(part.url, { useNamedReferences: true })}">${he.encode(part.text, {
|
|
@@ -132,9 +133,18 @@ function textLinesToHtml(lines) {
|
|
|
132
133
|
textparts.push('</p><p>');
|
|
133
134
|
}
|
|
134
135
|
} else if (i < lines.length - 1 && lines[i + 1].trim()) {
|
|
135
|
-
|
|
136
|
+
let escaped = linkifyAndHtmlEscape(line);
|
|
137
|
+
if (/^[ \t]*[-_]+[ \t]*$/.test(escaped)) {
|
|
138
|
+
textparts.push(`<hr />`);
|
|
139
|
+
} else {
|
|
140
|
+
textparts.push(`${escaped}<br />`);
|
|
141
|
+
}
|
|
136
142
|
} else {
|
|
137
|
-
|
|
143
|
+
let escaped = linkifyAndHtmlEscape(line);
|
|
144
|
+
if (/^[ \t]*[-_]+[ \t]*$/.test(escaped)) {
|
|
145
|
+
escaped = `<hr />`;
|
|
146
|
+
}
|
|
147
|
+
textparts.push(escaped);
|
|
138
148
|
}
|
|
139
149
|
}
|
|
140
150
|
|
|
@@ -142,6 +152,7 @@ function textLinesToHtml(lines) {
|
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
function textToHtml(text) {
|
|
155
|
+
text = (text || '').toString();
|
|
145
156
|
let tree = generateQuotedTree(text);
|
|
146
157
|
|
|
147
158
|
let blockQuoteCounter = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postalsys/email-text-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Email text tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,13 +22,16 @@
|
|
|
22
22
|
"eslint-config-prettier": "8.5.0"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"dompurify": "3.0.1",
|
|
25
26
|
"handlebars": "4.7.7",
|
|
26
27
|
"he": "1.2.0",
|
|
27
|
-
"html-to-text": "8.2.
|
|
28
|
-
"js-beautify": "1.14.
|
|
28
|
+
"html-to-text": "8.2.1",
|
|
29
|
+
"js-beautify": "1.14.7",
|
|
30
|
+
"jsdom": "21.1.0",
|
|
31
|
+
"juice": "8.1.0",
|
|
29
32
|
"linkify-it": "4.0.1",
|
|
30
|
-
"moment-timezone": "0.5.
|
|
31
|
-
"node-html-parser": "
|
|
32
|
-
"tlds": "1.
|
|
33
|
+
"moment-timezone": "0.5.39",
|
|
34
|
+
"node-html-parser": "6.1.4",
|
|
35
|
+
"tlds": "1.233.0"
|
|
33
36
|
}
|
|
34
37
|
}
|