@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 +15 -0
- package/.prettierrc.js +8 -0
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/examples/fixtures/message-1.json +74 -0
- package/examples/inline-html.js +24 -0
- package/examples/inline-text.js +24 -0
- package/index.js +8 -0
- package/lib/html-to-text.js +14 -0
- package/lib/inline-html.js +174 -0
- package/lib/inline-text.js +136 -0
- package/lib/text-to-html.js +193 -0
- package/package.json +34 -0
package/.eslintrc
ADDED
package/.prettierrc.js
ADDED
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,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 <<a href=\"mailto:andris@postalsys.com\" class=\"\">andris@postalsys.com</a>> 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
|
+
}
|