@postalsys/email-text-tools 1.0.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/.eslintrc ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "rules": {
3
+ "indent": 0,
4
+ "no-await-in-loop": 0,
5
+ "require-atomic-updates": 0
6
+ },
7
+ "globals": {
8
+ "BigInt": true
9
+ },
10
+ "extends": ["nodemailer", "prettier"],
11
+ "parserOptions": {
12
+ "ecmaVersion": 2018,
13
+ "sourceType": "script"
14
+ }
15
+ }
package/.prettierrc.js ADDED
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ printWidth: 160,
3
+ tabWidth: 4,
4
+ singleQuote: true,
5
+ endOfLine: 'lf',
6
+ trailingComma: 'none',
7
+ arrowParens: 'avoid'
8
+ };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Postal Systems
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # email-text-tools
2
+
3
+ Email text tools
@@ -0,0 +1,74 @@
1
+ {
2
+ "id": "AAAADQAABlw",
3
+ "uid": 1628,
4
+ "path": "INBOX",
5
+ "date": "2022-07-06T14:12:30.000Z",
6
+ "flags": ["\\Seen"],
7
+ "size": 4400,
8
+ "subject": "Re: proovikiri",
9
+ "from": {
10
+ "name": "Andris Reinman",
11
+ "address": "andris.reinman@gmail.com"
12
+ },
13
+ "replyTo": [
14
+ {
15
+ "name": "Andris Reinman",
16
+ "address": "andris.reinman@gmail.com"
17
+ }
18
+ ],
19
+ "sender": {
20
+ "name": "Andris Reinman",
21
+ "address": "andris.reinman@gmail.com"
22
+ },
23
+ "to": [
24
+ {
25
+ "name": "andris",
26
+ "address": "andris@ekiri.ee"
27
+ }
28
+ ],
29
+ "messageId": "<A768E075-1C9F-4483-A04A-943FA920BFC5@gmail.com>",
30
+ "inReplyTo": "<CAPFU5tRb4fM+4d74kkb5bjGuFr7n3kFF_TNXK=67ki93CLzGFw@mail.gmail.com>",
31
+ "headers": {
32
+ "return-path": ["<andris.reinman@gmail.com>"],
33
+ "delivered-to": ["andris@ekiri.ee"],
34
+ "received": [
35
+ "from mail-lj1-f179.google.com (mail-lj1-f179.google.com [209.85.208.179]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by uvn-67-33.tll01.zonevs.eu (Postfix) with ESMTPS id 44342821FF for <andris@ekiri.ee>; Wed, 6 Jul 2022 17:12:33 +0300 (EEST)",
36
+ "by mail-lj1-f179.google.com with SMTP id w2so2284874ljj.7 for <andris@ekiri.ee>; Wed, 06 Jul 2022 07:12:33 -0700 (PDT)",
37
+ "from smtpclient.apple (39-152-191-90.dyn.estpak.ee. [90.191.152.39]) by smtp.gmail.com with ESMTPSA id z8-20020a05651c022800b0025d37955bc7sm947119ljn.74.2022.07.06.07.12.30 for <andris@ekiri.ee> (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Wed, 06 Jul 2022 07:12:31 -0700 (PDT)"
38
+ ],
39
+ "authentication-results": [
40
+ "uvn-67-33.tll01.zonevs.eu; dkim=pass reason=\"2048-bit key; insecure key\" header.d=gmail.com header.i=@gmail.com header.b=E9nmrfaB; dkim-adsp=pass; dkim-atps=neutral"
41
+ ],
42
+ "dkim-signature": [
43
+ "v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:mime-version:subject:date:references:to:in-reply-to:message-id; bh=us7LzUXeWkaVy96MNsqMB7vDjhaZ9hRrGPgxXc9icYc=; b=E9nmrfaBGUMEisRJCJy674YfEQyu2oXIF/1E7AcyGAit26CPU+vfpL3XGXmMzwWKTd S+EjMUwCc4xn4wkmxYl3nxcd3EQr1XpIpp42fK4wEfaAt7Nb3Wp+uV1pBej6Slh2TgBf j7NWyea3gyh/m09OqKAijYesJYw9YGmo+6Y7lgiNFFuIeuJRUrg5htvc6eNzoSQfnIIl QiUptLnrFFfgMRsaBkIsd4ie1/1HnLoa7ngopIRVARKRrrCNRhTqkiM93BUf8nBUDjb7 QPh8PBzzGrVvA9nDLjlSsF8t0s8Nwk9f7HApzFKAra3dzv8VnFY6zQxBm26yCjnrCxbH 8dyA=="
44
+ ],
45
+ "x-google-dkim-signature": [
46
+ "v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:mime-version:subject:date:references:to :in-reply-to:message-id; bh=us7LzUXeWkaVy96MNsqMB7vDjhaZ9hRrGPgxXc9icYc=; b=0hw1EbIluZO7ZAuzBIRNVqHtNAHviTJ0RXDtefUWxyfjaaTnKkmnwPum49KIf5gGu+ J7r9tw4rGvQKLjmM1runJiZFZHq7afV3MmR4wGjjvLRA62MAdIcpAT5u8ElxjHf0nhhM OO7Yf+mYwqsB6KDeZQ5JF9LCNBvpe+TQpSXMlHANyWvh5Ofp6ugH+gLEx+8QIJHIcGBi 6cXu+pPb1GsE+M3bWY7DEiYeTDy6QQfxRZSdi9vSHFb4h/R8K6Z1/heJN+RlEGmUU2M7 fQbEuCcHfObhWMe5AmLfDXIlrx+wKxVyVxpafkLSiMQnRSvqhxVSWcUKjzr20TlHT/qH ca9g=="
47
+ ],
48
+ "x-gm-message-state": ["AJIora/2p0hYvgtQ3eBu7h9wBVVfY6UdVx4BO6QA32p0fTru+yWtItsi GdXLS5baocCqlr3JhbHd4ORKCa46iqRbZA=="],
49
+ "x-google-smtp-source": ["AGRyM1s3m/T3Exsg6pXn6DGV9dmrrt5GV7nTjah/wAyWp+fel/88cNszkKiPBCE/I3u1VEiSO1zJMw=="],
50
+ "x-received": [
51
+ "by 2002:a05:651c:1032:b0:25a:780b:7140 with SMTP id w18-20020a05651c103200b0025a780b7140mr23860776ljm.319.1657116752521; Wed, 06 Jul 2022 07:12:32 -0700 (PDT)"
52
+ ],
53
+ "from": ["Andris Reinman <andris.reinman@gmail.com>"],
54
+ "content-type": ["multipart/alternative; boundary=\"Apple-Mail=_650C0B0C-FF20-4BA0-B542-9025E8199E24\""],
55
+ "mime-version": ["1.0 (Mac OS X Mail 16.0 \\(3696.100.31\\))"],
56
+ "subject": ["Re: proovikiri"],
57
+ "date": ["Wed, 6 Jul 2022 17:12:30 +0300"],
58
+ "references": ["<CAPFU5tRb4fM+4d74kkb5bjGuFr7n3kFF_TNXK=67ki93CLzGFw@mail.gmail.com>"],
59
+ "to": ["andris <andris@ekiri.ee>"],
60
+ "in-reply-to": ["<CAPFU5tRb4fM+4d74kkb5bjGuFr7n3kFF_TNXK=67ki93CLzGFw@mail.gmail.com>"],
61
+ "message-id": ["<A768E075-1C9F-4483-A04A-943FA920BFC5@gmail.com>"],
62
+ "x-mailer": ["Apple Mail (2.3696.100.31)"]
63
+ },
64
+ "text": {
65
+ "id": "AAAADQAABlyTkaExkaEykA",
66
+ "encodedSize": {
67
+ "plain": 123,
68
+ "html": 590
69
+ },
70
+ "plain": "multi pultiu 3\n\n> On 6. Jul 2022, at 16:34, Andris Reinman <andris@postalsys.com> wrote:\n> \n> See on lihtsalt proov\n\n",
71
+ "html": "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\"></head><body style=\"word-wrap: break-word; -webkit-nbsp-mode: space; line-break: after-white-space;\" class=\"\">multi <b class=\"\">pultiu 3</b><br class=\"\"><div><br class=\"\"><blockquote type=\"cite\" class=\"\"><div class=\"\">On 6. Jul 2022, at 16:34, Andris Reinman &lt;<a href=\"mailto:andris@postalsys.com\" class=\"\">andris@postalsys.com</a>&gt; wrote:</div><br class=\"Apple-interchange-newline\"><div class=\"\"><div dir=\"ltr\" class=\"\">See on lihtsalt proov</div>\n</div></blockquote></div><br class=\"\"></body></html>",
72
+ "hasMore": false
73
+ }
74
+ }
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const sourceMessage = require('./fixtures/message-1.json');
4
+
5
+ const { inlineHtml } = require('..');
6
+
7
+ const messageContent = `<h1>Hello world!</h1>
8
+
9
+ <p>Sent from my iPhone</p>`;
10
+
11
+ let messageData = {
12
+ text: sourceMessage.text && sourceMessage.text.plain,
13
+ html: sourceMessage.text && sourceMessage.text.html
14
+ };
15
+ for (let key of ['from', 'to', 'cc', 'bcc', 'date', 'subject']) {
16
+ messageData[key] = sourceMessage[key];
17
+ }
18
+
19
+ let inlinedHtml = inlineHtml('forward', messageContent, messageData, {
20
+ locale: 'et',
21
+ tz: 'Europe/Tallinn'
22
+ });
23
+
24
+ console.log(inlinedHtml);
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const sourceMessage = require('./fixtures/message-1.json');
4
+
5
+ const { inlineText } = require('..');
6
+
7
+ const messageContent = `Hello world!
8
+
9
+ Sent from my iPhone`;
10
+
11
+ let messageData = {
12
+ //text: sourceMessage.text && sourceMessage.text.plain,
13
+ html: sourceMessage.text && sourceMessage.text.html
14
+ };
15
+ for (let key of ['from', 'to', 'cc', 'bcc', 'date', 'subject']) {
16
+ messageData[key] = sourceMessage[key];
17
+ }
18
+
19
+ let inlinedHtml = inlineText('forward', messageContent, messageData, {
20
+ locale: 'et',
21
+ tz: 'Europe/Tallinn'
22
+ });
23
+
24
+ console.log(inlinedHtml);
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ const textToHtml = require('./lib/text-to-html');
4
+ const htmlToText = require('./lib/html-to-text');
5
+ const inlineHtml = require('./lib/inline-html');
6
+ const inlineText = require('./lib/inline-text');
7
+
8
+ module.exports = { textToHtml, htmlToText, inlineHtml, inlineText };
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+ const { compile } = require('html-to-text');
3
+
4
+ const htmlToText = compile({
5
+ wordwrap: 160,
6
+ selectors: [
7
+ { selector: 'p', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
8
+ { selector: 'pre', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
9
+ { selector: 'table.class#id', format: 'dataTable' }
10
+ ],
11
+ hideLinkHrefIfSameAsText: true
12
+ });
13
+
14
+ module.exports = htmlToText;
@@ -0,0 +1,174 @@
1
+ 'use strict';
2
+
3
+ const HTMLParser = require('node-html-parser');
4
+ const he = require('he');
5
+ const moment = require('moment-timezone');
6
+ const textToHtml = require('./text-to-html');
7
+
8
+ function getHtmlBody(html) {
9
+ html = ((html && html.toString()) || '').trim();
10
+ if (!html) {
11
+ return '';
12
+ }
13
+
14
+ const root = HTMLParser.parse(html);
15
+ let body = root.querySelector('body');
16
+ if (!body) {
17
+ body = root.querySelector('html') || root;
18
+ }
19
+ return ((body && body.innerHTML) || '').trim();
20
+ }
21
+
22
+ function formatAddressHtml(addr) {
23
+ if (!addr || (!addr.name && !addr.address)) {
24
+ return false;
25
+ }
26
+
27
+ let parts = [];
28
+ if (addr.name) {
29
+ parts.push(he.encode(addr.name, { useNamedReferences: true }));
30
+ }
31
+ if (addr.address) {
32
+ parts.push(
33
+ `${he.encode('<', { useNamedReferences: true })}<a href="mailto:${he.encode(addr.address, { useNamedReferences: true })}">${he.encode(
34
+ addr.address,
35
+ { useNamedReferences: true }
36
+ )}</a>${he.encode('>', { useNamedReferences: true })}`
37
+ );
38
+ }
39
+
40
+ return parts.join(' ');
41
+ }
42
+
43
+ function formatAddressesHtml(addresses) {
44
+ addresses = [].concat(addresses || []);
45
+ if (!addresses || !addresses.length) {
46
+ return false;
47
+ }
48
+
49
+ let list = [];
50
+ for (let address of addresses) {
51
+ let entry = formatAddressHtml(address);
52
+ if (entry) {
53
+ list.push(entry);
54
+ }
55
+ }
56
+ if (!list.length) {
57
+ return false;
58
+ }
59
+ return list.join(', ');
60
+ }
61
+
62
+ function inlineHtml(action, messageContent, messageData, options) {
63
+ options = options || {};
64
+
65
+ messageContent = (messageContent || '').toString();
66
+
67
+ let originalBody;
68
+
69
+ if (messageData.text && !messageData.html) {
70
+ originalBody = textToHtml(messageData.text);
71
+ } else {
72
+ originalBody = getHtmlBody(messageData.html);
73
+ }
74
+
75
+ if (!originalBody) {
76
+ return messageContent;
77
+ }
78
+
79
+ // remove html headers and stuff
80
+ messageContent = getHtmlBody(messageContent);
81
+
82
+ let headerLines = [];
83
+
84
+ if (messageData.from) {
85
+ let entry = {
86
+ title: 'From',
87
+ content: formatAddressesHtml(messageData.from) || he.encode('<>', { useNamedReferences: true })
88
+ };
89
+ headerLines.push(entry);
90
+ }
91
+
92
+ if (messageData.subject) {
93
+ let entry = {
94
+ title: 'Subject',
95
+ content: `<b>${he.encode(messageData.subject, { useNamedReferences: true })}</b>`
96
+ };
97
+ headerLines.push(entry);
98
+ }
99
+
100
+ let dateStr = '';
101
+
102
+ if (messageData.date) {
103
+ let dateElm = moment(messageData.date);
104
+
105
+ if (options.locale) {
106
+ dateElm = dateElm.locale(options.locale);
107
+ }
108
+
109
+ if (options.tz) {
110
+ dateElm = dateElm.tz(options.tz);
111
+ }
112
+
113
+ dateStr = dateElm.format('llll');
114
+
115
+ let entry = {
116
+ title: 'Date',
117
+ content: he.encode(dateStr, { useNamedReferences: true })
118
+ };
119
+ headerLines.push(entry);
120
+ }
121
+
122
+ for (let key of ['to', 'cc', 'bcc']) {
123
+ if (messageData[key]) {
124
+ let content = formatAddressesHtml(messageData[key]);
125
+ if (content) {
126
+ let entry = {
127
+ title: key.replace(/^./, c => c.toUpperCase()),
128
+ content
129
+ };
130
+ headerLines.push(entry);
131
+ }
132
+ }
133
+ }
134
+
135
+ let headers = [];
136
+
137
+ switch (action) {
138
+ case 'forward':
139
+ headers.push(`<div>Begin forwarded message:</div><br class="Apple-interchange-newline">`);
140
+
141
+ for (let line of headerLines) {
142
+ headers.push(
143
+ `<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>`
144
+ );
145
+ }
146
+ break;
147
+ case 'reply':
148
+ headers.push(
149
+ `<div>On ${he.encode(dateStr, { useNamedReferences: true })}, ${
150
+ formatAddressesHtml(messageData.from) || he.encode('<>', { useNamedReferences: true })
151
+ } wrote:</div>`
152
+ );
153
+ break;
154
+ }
155
+
156
+ return `<html>
157
+
158
+ <head>
159
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
160
+ </head>
161
+
162
+ <body style="word-wrap: break-word; -webkit-nbsp-mode: space; line-break: after-white-space;">${messageContent}<br>
163
+ <div><br>
164
+ <blockquote type="cite" class="ee-block-1">
165
+ ${headers.join('\n')}<br>
166
+ <div>${originalBody}</div>
167
+ </blockquote>
168
+ </div>
169
+ </body>
170
+
171
+ </html>`;
172
+ }
173
+
174
+ module.exports = inlineHtml;
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ const moment = require('moment-timezone');
4
+ const htmlToText = require('./html-to-text');
5
+
6
+ function formatAddressText(addr) {
7
+ if (!addr || (!addr.name && !addr.address)) {
8
+ return false;
9
+ }
10
+
11
+ let parts = [];
12
+ if (addr.name) {
13
+ parts.push(addr.name);
14
+ }
15
+
16
+ if (addr.address) {
17
+ parts.push(`<${addr.address}>`);
18
+ }
19
+
20
+ return parts.join(' ');
21
+ }
22
+
23
+ function formatAddressesText(addresses) {
24
+ addresses = [].concat(addresses || []);
25
+ if (!addresses || !addresses.length) {
26
+ return false;
27
+ }
28
+ let list = [];
29
+ for (let address of addresses) {
30
+ let entry = formatAddressText(address);
31
+ if (entry) {
32
+ list.push(entry);
33
+ }
34
+ }
35
+ if (!list.length) {
36
+ return false;
37
+ }
38
+ return list.join(', ');
39
+ }
40
+
41
+ function escapeText(text) {
42
+ return '> ' + text.replace(/\r?\n/g, '\n> ');
43
+ }
44
+
45
+ function inlineText(type, messageContent, messageData, options) {
46
+ options = options || {};
47
+
48
+ messageContent = (messageContent || '').toString();
49
+
50
+ let originalBody;
51
+ if (!messageData.text && messageData.html) {
52
+ originalBody = htmlToText(messageData.html);
53
+ } else {
54
+ originalBody = messageData.text;
55
+ }
56
+
57
+ if (!originalBody) {
58
+ return messageContent;
59
+ }
60
+
61
+ let headerLines = [];
62
+
63
+ if (messageData.from) {
64
+ let entry = {
65
+ title: 'From',
66
+ content: formatAddressesText(messageData.from) || '<>'
67
+ };
68
+ headerLines.push(entry);
69
+ }
70
+
71
+ if (messageData.subject) {
72
+ let entry = {
73
+ title: 'Subject',
74
+ content: messageData.subject
75
+ };
76
+ headerLines.push(entry);
77
+ }
78
+
79
+ let dateStr = '';
80
+
81
+ if (messageData.date) {
82
+ let dateElm = moment(messageData.date);
83
+
84
+ if (options.locale) {
85
+ dateElm = dateElm.locale(options.locale);
86
+ }
87
+
88
+ if (options.tz) {
89
+ dateElm = dateElm.tz(options.tz);
90
+ }
91
+
92
+ dateStr = dateElm.format('llll');
93
+
94
+ let entry = {
95
+ title: 'Date',
96
+ content: dateStr
97
+ };
98
+ headerLines.push(entry);
99
+ }
100
+
101
+ for (let key of ['to', 'cc', 'bcc']) {
102
+ if (messageData[key]) {
103
+ let content = formatAddressesText(messageData[key]);
104
+ if (content) {
105
+ let entry = {
106
+ title: key.replace(/^./, c => c.toUpperCase()),
107
+ content
108
+ };
109
+ headerLines.push(entry);
110
+ }
111
+ }
112
+ }
113
+
114
+ let headers = [];
115
+
116
+ switch (type) {
117
+ case 'forward':
118
+ headers.push('Begin forwarded message:\n');
119
+
120
+ for (let line of headerLines) {
121
+ headers.push(`${line.title}: ${line.content}`);
122
+ }
123
+ break;
124
+ case 'reply':
125
+ headers.push(`On ${dateStr}, ${formatAddressesText(messageData.from) || '<>'} wrote:\n`);
126
+ break;
127
+ }
128
+
129
+ return `${messageContent}
130
+
131
+ ${escapeText(headers.join('\n'))}
132
+ >
133
+ ${escapeText(originalBody)}`;
134
+ }
135
+
136
+ module.exports = inlineText;
@@ -0,0 +1,193 @@
1
+ 'use strict';
2
+
3
+ const beautifyHtml = require('js-beautify').html;
4
+ const linkifyIt = require('linkify-it');
5
+ const tlds = require('tlds');
6
+ const he = require('he');
7
+
8
+ const linkify = linkifyIt()
9
+ .tlds(tlds) // Reload with full tlds list
10
+ .tlds('onion', true) // Add unofficial `.onion` domain
11
+ .add('git:', 'http:') // Add `git:` protocol as "alias"
12
+ .set({ fuzzyIP: true });
13
+
14
+ function generateQuotedTree(text) {
15
+ let lines = text.split(/\r?\n/);
16
+
17
+ let tree = {
18
+ type: 'text',
19
+ children: []
20
+ };
21
+
22
+ let createNode = (parent, type) => {
23
+ let node = {
24
+ parent,
25
+ type,
26
+ lines: [],
27
+ children: []
28
+ };
29
+ parent.children.push(node);
30
+ return node;
31
+ };
32
+
33
+ let walkNode = (curNode, lines) => {
34
+ for (let line of lines) {
35
+ if (/^>/.test(line)) {
36
+ if (curNode.type !== 'quote') {
37
+ curNode = createNode(curNode.parent, 'quote');
38
+ }
39
+ curNode.lines.push(line.replace(/^> ?/, ''));
40
+ } else if (curNode.type === 'quote') {
41
+ // process child
42
+
43
+ let quoteNode = curNode;
44
+
45
+ let childNode = createNode(curNode, 'text');
46
+ walkNode(childNode, quoteNode.lines);
47
+
48
+ curNode = createNode(curNode.parent, 'text');
49
+ curNode.lines.push(line);
50
+ } else {
51
+ curNode.lines.push(line);
52
+ }
53
+ }
54
+
55
+ if (curNode.type === 'quote') {
56
+ // process child
57
+
58
+ let quoteNode = curNode;
59
+
60
+ let childNode = createNode(curNode, 'text');
61
+ walkNode(childNode, quoteNode.lines);
62
+ }
63
+ };
64
+
65
+ let rootNode = createNode(tree, 'text');
66
+ walkNode(rootNode, lines);
67
+
68
+ return tree;
69
+ }
70
+
71
+ /**
72
+ * Return HTML escaped text with clickable links
73
+ * @param {String} text Text content to HTML escape
74
+ * @returns {String} HTML escaped content
75
+ */
76
+ function linkifyAndHtmlEscape(text) {
77
+ try {
78
+ let links = linkify.match(text);
79
+ if (links && links.length) {
80
+ let parts = [];
81
+ let cursor = 0;
82
+ for (let link of links) {
83
+ if (cursor < link.index) {
84
+ parts.push({
85
+ type: 'text',
86
+ content: text.substring(cursor, link.index)
87
+ });
88
+ cursor = link.index;
89
+ }
90
+ parts.push(Object.assign({ type: 'link' }, link));
91
+ cursor = link.lastIndex;
92
+ }
93
+
94
+ if (cursor < text.length) {
95
+ parts.push({
96
+ type: 'text',
97
+ content: text.substr(cursor)
98
+ });
99
+ }
100
+
101
+ return parts
102
+ .map(part => {
103
+ switch (part.type) {
104
+ case 'text':
105
+ // normal text, escape HTML
106
+ return he.encode(part.content, { useNamedReferences: true });
107
+ case 'link':
108
+ // URL with html escaped text content and URL
109
+ return `<a href="${he.encode(part.url, { useNamedReferences: true })}">${he.encode(part.text, {
110
+ useNamedReferences: true
111
+ })}</a>`;
112
+ }
113
+ return '';
114
+ })
115
+ .join('');
116
+ }
117
+ } catch (err) {
118
+ // ignore?
119
+ }
120
+
121
+ // No links or exception, so HTML escape everything
122
+ return he.encode(text, { useNamedReferences: true });
123
+ }
124
+
125
+ function textLinesToHtml(lines) {
126
+ let textparts = ['<p>'];
127
+ for (let i = 0; i < lines.length; i++) {
128
+ let line = lines[i];
129
+
130
+ if (!line.trim()) {
131
+ if (i && lines[i - 1].trim()) {
132
+ textparts.push('</p><p>');
133
+ }
134
+ } else if (i < lines.length - 1 && lines[i + 1].trim()) {
135
+ textparts.push(`${linkifyAndHtmlEscape(line)}<br />`);
136
+ } else {
137
+ textparts.push(`${linkifyAndHtmlEscape(line)}`);
138
+ }
139
+ }
140
+
141
+ return textparts.join('\n') + '</p>';
142
+ }
143
+
144
+ function textToHtml(text) {
145
+ let tree = generateQuotedTree(text);
146
+
147
+ let blockQuoteCounter = 0;
148
+ function processQuotedNode(curNode, level) {
149
+ let entries = [];
150
+ level = (level || 0) + 1;
151
+
152
+ if (curNode.type === 'text') {
153
+ for (let child of curNode.children) {
154
+ let content = processQuotedNode(child);
155
+ if (typeof content === 'string') {
156
+ entries.push(content);
157
+ }
158
+ }
159
+
160
+ if (curNode.lines && curNode.lines.length) {
161
+ entries.push(textLinesToHtml(curNode.lines));
162
+ }
163
+ }
164
+
165
+ if (curNode.type === 'quote') {
166
+ for (let child of curNode.children) {
167
+ let content = processQuotedNode(child, level);
168
+ if (typeof content === 'string') {
169
+ let id = ++blockQuoteCounter;
170
+ entries.push(
171
+ `<blockquote type="cite" class="ee-block-${level}"><!-- blocquote ${id} start-->${content}</blockquote><!-- blocquote ${id} end-->`
172
+ );
173
+ }
174
+ }
175
+ }
176
+
177
+ if (entries.length) {
178
+ return entries.join('\n');
179
+ }
180
+ }
181
+
182
+ let generatedHtml = processQuotedNode(tree);
183
+
184
+ try {
185
+ return beautifyHtml(generatedHtml);
186
+ } catch (err) {
187
+ // ignore, might fail
188
+ }
189
+
190
+ return generatedHtml;
191
+ }
192
+
193
+ module.exports = textToHtml;
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@postalsys/email-text-tools",
3
+ "version": "1.0.0",
4
+ "description": "Email text tools",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/postalsys/email-text-tools.git"
12
+ },
13
+ "keywords": [],
14
+ "author": "Postal Systems OÜ",
15
+ "license": "MIT",
16
+ "bugs": {
17
+ "url": "https://github.com/postalsys/email-text-tools/issues"
18
+ },
19
+ "homepage": "https://github.com/postalsys/email-text-tools#readme",
20
+ "devDependencies": {
21
+ "eslint-config-nodemailer": "1.2.0",
22
+ "eslint-config-prettier": "8.5.0"
23
+ },
24
+ "dependencies": {
25
+ "handlebars": "4.7.7",
26
+ "he": "1.2.0",
27
+ "html-to-text": "8.2.0",
28
+ "js-beautify": "1.14.4",
29
+ "linkify-it": "4.0.1",
30
+ "moment-timezone": "0.5.34",
31
+ "node-html-parser": "5.3.3",
32
+ "tlds": "1.231.0"
33
+ }
34
+ }