postal-mime 2.0.0 → 2.0.2

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.
@@ -0,0 +1,96 @@
1
+ import { blobToArrayBuffer } from './decode-strings.js';
2
+
3
+ export default class QPDecoder {
4
+ constructor(opts) {
5
+ opts = opts || {};
6
+
7
+ this.decoder = opts.decoder || new TextDecoder();
8
+
9
+ this.maxChunkSize = 100 * 1024;
10
+
11
+ this.remainder = '';
12
+
13
+ this.chunks = [];
14
+ }
15
+
16
+ decodeQPBytes(encodedBytes) {
17
+ let buf = new ArrayBuffer(encodedBytes.length);
18
+ let dataView = new DataView(buf);
19
+ for (let i = 0, len = encodedBytes.length; i < len; i++) {
20
+ dataView.setUint8(i, parseInt(encodedBytes[i], 16));
21
+ }
22
+ return buf;
23
+ }
24
+
25
+ decodeChunks(str) {
26
+ // unwrap newlines
27
+ str = str.replace(/=\r?\n/g, '');
28
+
29
+ let list = str.split(/(?==)/);
30
+ let encodedBytes = [];
31
+ for (let part of list) {
32
+ if (part.charAt(0) !== '=') {
33
+ if (encodedBytes.length) {
34
+ this.chunks.push(this.decodeQPBytes(encodedBytes));
35
+ encodedBytes = [];
36
+ }
37
+ this.chunks.push(part);
38
+ continue;
39
+ }
40
+
41
+ if (part.length === 3) {
42
+ encodedBytes.push(part.substr(1));
43
+ continue;
44
+ }
45
+
46
+ if (part.length > 3) {
47
+ encodedBytes.push(part.substr(1, 2));
48
+ this.chunks.push(this.decodeQPBytes(encodedBytes));
49
+ encodedBytes = [];
50
+
51
+ part = part.substr(3);
52
+ this.chunks.push(part);
53
+ }
54
+ }
55
+ if (encodedBytes.length) {
56
+ this.chunks.push(this.decodeQPBytes(encodedBytes));
57
+ encodedBytes = [];
58
+ }
59
+ }
60
+
61
+ update(buffer) {
62
+ // expect full lines, so add line terminator as well
63
+ let str = this.decoder.decode(buffer) + '\n';
64
+
65
+ str = this.remainder + str;
66
+
67
+ if (str.length < this.maxChunkSize) {
68
+ this.remainder = str;
69
+ return;
70
+ }
71
+
72
+ this.remainder = '';
73
+
74
+ let partialEnding = str.match(/=[a-fA-F0-9]?$/);
75
+ if (partialEnding) {
76
+ if (partialEnding.index === 0) {
77
+ this.remainder = str;
78
+ return;
79
+ }
80
+ this.remainder = str.substr(partialEnding.index);
81
+ str = str.substr(0, partialEnding.index);
82
+ }
83
+
84
+ this.decodeChunks(str);
85
+ }
86
+
87
+ finalize() {
88
+ if (this.remainder.length) {
89
+ this.decodeChunks(this.remainder);
90
+ this.remainder = '';
91
+ }
92
+
93
+ // convert an array of arraybuffers into a blob and then back into a single arraybuffer
94
+ return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' }));
95
+ }
96
+ }
@@ -0,0 +1,334 @@
1
+ import htmlEntities from './html-entities.js';
2
+
3
+ export function decodeHTMLEntities(str) {
4
+ return str.replace(/&(#\d+|#x[a-f0-9]+|[a-z]+\d*);?/gi, (match, entity) => {
5
+ if (typeof htmlEntities[match] === 'string') {
6
+ return htmlEntities[match];
7
+ }
8
+
9
+ if (entity.charAt(0) !== '#' || match.charAt(match.length - 1) !== ';') {
10
+ // keep as is, invalid or unknown sequence
11
+ return match;
12
+ }
13
+
14
+ let codePoint;
15
+ if (entity.charAt(1) === 'x') {
16
+ // hex
17
+ codePoint = parseInt(entity.substr(2), 16);
18
+ } else {
19
+ // dec
20
+ codePoint = parseInt(entity.substr(1), 10);
21
+ }
22
+
23
+ var output = '';
24
+
25
+ if ((codePoint >= 0xd800 && codePoint <= 0xdfff) || codePoint > 0x10ffff) {
26
+ // Invalid range, return a replacement character instead
27
+ return '\uFFFD';
28
+ }
29
+
30
+ if (codePoint > 0xffff) {
31
+ codePoint -= 0x10000;
32
+ output += String.fromCharCode(((codePoint >>> 10) & 0x3ff) | 0xd800);
33
+ codePoint = 0xdc00 | (codePoint & 0x3ff);
34
+ }
35
+
36
+ output += String.fromCharCode(codePoint);
37
+
38
+ return output;
39
+ });
40
+ }
41
+
42
+ export function escapeHtml(str) {
43
+ return str.trim().replace(/[<>"'?&]/g, c => {
44
+ let hex = c.charCodeAt(0).toString(16);
45
+ if (hex.length < 2) {
46
+ hex = '0' + hex;
47
+ }
48
+ return '&#x' + hex.toUpperCase() + ';';
49
+ });
50
+ }
51
+
52
+ export function textToHtml(str) {
53
+ let html = escapeHtml(str).replace(/\n/g, '<br />');
54
+ return '<div>' + html + '</div>';
55
+ }
56
+
57
+ export function htmlToText(str) {
58
+ str = str
59
+ // we can't process tags on multiple lines so remove newlines first
60
+ .replace(/\r?\n/g, '\u0001')
61
+ .replace(/<\!\-\-.*?\-\->/gi, ' ')
62
+
63
+ .replace(/<br\b[^>]*>/gi, '\n')
64
+ .replace(/<\/?(p|div|table|tr|td|th)\b[^>]*>/gi, '\n\n')
65
+ .replace(/<script\b[^>]*>.*?<\/script\b[^>]*>/gi, ' ')
66
+ .replace(/^.*<body\b[^>]*>/i, '')
67
+ .replace(/^.*<\/head\b[^>]*>/i, '')
68
+ .replace(/^.*<\!doctype\b[^>]*>/i, '')
69
+ .replace(/<\/body\b[^>]*>.*$/i, '')
70
+ .replace(/<\/html\b[^>]*>.*$/i, '')
71
+
72
+ .replace(/<a\b[^>]*href\s*=\s*["']?([^\s"']+)[^>]*>/gi, ' ($1) ')
73
+
74
+ .replace(/<\/?(span|em|i|strong|b|u|a)\b[^>]*>/gi, '')
75
+
76
+ .replace(/<li\b[^>]*>[\n\u0001\s]*/gi, '* ')
77
+
78
+ .replace(/<hr\b[^>]*>/g, '\n-------------\n')
79
+
80
+ .replace(/<[^>]*>/g, ' ')
81
+
82
+ // convert linebreak placeholders back to newlines
83
+ .replace(/\u0001/g, '\n')
84
+
85
+ .replace(/[ \t]+/g, ' ')
86
+
87
+ .replace(/^\s+$/gm, '')
88
+
89
+ .replace(/\n\n+/g, '\n\n')
90
+ .replace(/^\n+/, '\n')
91
+ .replace(/\n+$/, '\n');
92
+
93
+ str = decodeHTMLEntities(str);
94
+
95
+ return str;
96
+ }
97
+
98
+ function formatTextAddress(address) {
99
+ return []
100
+ .concat(address.name || [])
101
+ .concat(address.name ? `<${address.address}>` : address.address)
102
+ .join(' ');
103
+ }
104
+
105
+ function formatTextAddresses(addresses) {
106
+ let parts = [];
107
+
108
+ let processAddress = (address, partCounter) => {
109
+ if (partCounter) {
110
+ parts.push(', ');
111
+ }
112
+
113
+ if (address.group) {
114
+ let groupStart = `${address.name}:`;
115
+ let groupEnd = `;`;
116
+
117
+ parts.push(groupStart);
118
+ address.group.forEach(processAddress);
119
+ parts.push(groupEnd);
120
+ } else {
121
+ parts.push(formatTextAddress(address));
122
+ }
123
+ };
124
+
125
+ addresses.forEach(processAddress);
126
+
127
+ return parts.join('');
128
+ }
129
+
130
+ function formatHtmlAddress(address) {
131
+ return `<a href="mailto:${escapeHtml(address.address)}" class="postal-email-address">${escapeHtml(address.name || `<${address.address}>`)}</a>`;
132
+ }
133
+
134
+ function formatHtmlAddresses(addresses) {
135
+ let parts = [];
136
+
137
+ let processAddress = (address, partCounter) => {
138
+ if (partCounter) {
139
+ parts.push('<span class="postal-email-address-separator">, </span>');
140
+ }
141
+
142
+ if (address.group) {
143
+ let groupStart = `<span class="postal-email-address-group">${escapeHtml(address.name)}:</span>`;
144
+ let groupEnd = `<span class="postal-email-address-group">;</span>`;
145
+
146
+ parts.push(groupStart);
147
+ address.group.forEach(processAddress);
148
+ parts.push(groupEnd);
149
+ } else {
150
+ parts.push(formatHtmlAddress(address));
151
+ }
152
+ };
153
+
154
+ addresses.forEach(processAddress);
155
+
156
+ return parts.join(' ');
157
+ }
158
+
159
+ function foldLines(str, lineLength, afterSpace) {
160
+ str = (str || '').toString();
161
+ lineLength = lineLength || 76;
162
+
163
+ let pos = 0,
164
+ len = str.length,
165
+ result = '',
166
+ line,
167
+ match;
168
+
169
+ while (pos < len) {
170
+ line = str.substr(pos, lineLength);
171
+ if (line.length < lineLength) {
172
+ result += line;
173
+ break;
174
+ }
175
+ if ((match = line.match(/^[^\n\r]*(\r?\n|\r)/))) {
176
+ line = match[0];
177
+ result += line;
178
+ pos += line.length;
179
+ continue;
180
+ } else if ((match = line.match(/(\s+)[^\s]*$/)) && match[0].length - (afterSpace ? (match[1] || '').length : 0) < line.length) {
181
+ line = line.substr(0, line.length - (match[0].length - (afterSpace ? (match[1] || '').length : 0)));
182
+ } else if ((match = str.substr(pos + line.length).match(/^[^\s]+(\s*)/))) {
183
+ line = line + match[0].substr(0, match[0].length - (!afterSpace ? (match[1] || '').length : 0));
184
+ }
185
+
186
+ result += line;
187
+ pos += line.length;
188
+ if (pos < len) {
189
+ result += '\r\n';
190
+ }
191
+ }
192
+
193
+ return result;
194
+ }
195
+
196
+ export function formatTextHeader(message) {
197
+ let rows = [];
198
+
199
+ if (message.from) {
200
+ rows.push({ key: 'From', val: formatTextAddress(message.from) });
201
+ }
202
+
203
+ if (message.subject) {
204
+ rows.push({ key: 'Subject', val: message.subject });
205
+ }
206
+
207
+ if (message.date) {
208
+ let dateOptions = {
209
+ year: 'numeric',
210
+ month: 'numeric',
211
+ day: 'numeric',
212
+ hour: 'numeric',
213
+ minute: 'numeric',
214
+ second: 'numeric',
215
+ hour12: false
216
+ };
217
+
218
+ let dateStr = typeof Intl === 'undefined' ? message.date : new Intl.DateTimeFormat('default', dateOptions).format(new Date(message.date));
219
+
220
+ rows.push({ key: 'Date', val: dateStr });
221
+ }
222
+
223
+ if (message.to && message.to.length) {
224
+ rows.push({ key: 'To', val: formatTextAddresses(message.to) });
225
+ }
226
+
227
+ if (message.cc && message.cc.length) {
228
+ rows.push({ key: 'Cc', val: formatTextAddresses(message.cc) });
229
+ }
230
+
231
+ if (message.bcc && message.bcc.length) {
232
+ rows.push({ key: 'Bcc', val: formatTextAddresses(message.bcc) });
233
+ }
234
+
235
+ // Align keys and values by adding space between these two
236
+ // Also make sure that the separator line is as long as the longest line
237
+ // Should end up with something like this:
238
+ /*
239
+ -----------------------------
240
+ From: xx xx <xxx@xxx.com>
241
+ Subject: Example Subject
242
+ Date: 16/02/2021, 02:57:06
243
+ To: not@found.com
244
+ -----------------------------
245
+ */
246
+
247
+ let maxKeyLength = rows
248
+ .map(r => r.key.length)
249
+ .reduce((acc, cur) => {
250
+ return cur > acc ? cur : acc;
251
+ }, 0);
252
+
253
+ rows = rows.flatMap(row => {
254
+ let sepLen = maxKeyLength - row.key.length;
255
+ let prefix = `${row.key}: ${' '.repeat(sepLen)}`;
256
+ let emptyPrefix = `${' '.repeat(row.key.length + 1)} ${' '.repeat(sepLen)}`;
257
+
258
+ let foldedLines = foldLines(row.val, 80, true)
259
+ .split(/\r?\n/)
260
+ .map(line => line.trim());
261
+
262
+ return foldedLines.map((line, i) => `${i ? emptyPrefix : prefix}${line}`);
263
+ });
264
+
265
+ let maxLineLength = rows
266
+ .map(r => r.length)
267
+ .reduce((acc, cur) => {
268
+ return cur > acc ? cur : acc;
269
+ }, 0);
270
+
271
+ let lineMarker = '-'.repeat(maxLineLength);
272
+
273
+ let template = `
274
+ ${lineMarker}
275
+ ${rows.join('\n')}
276
+ ${lineMarker}
277
+ `;
278
+
279
+ return template;
280
+ }
281
+
282
+ export function formatHtmlHeader(message) {
283
+ let rows = [];
284
+
285
+ if (message.from) {
286
+ rows.push(`<div class="postal-email-header-key">From</div><div class="postal-email-header-value">${formatHtmlAddress(message.from)}</div>`);
287
+ }
288
+
289
+ if (message.subject) {
290
+ rows.push(
291
+ `<div class="postal-email-header-key">Subject</div><div class="postal-email-header-value postal-email-header-subject">${escapeHtml(
292
+ message.subject
293
+ )}</div>`
294
+ );
295
+ }
296
+
297
+ if (message.date) {
298
+ let dateOptions = {
299
+ year: 'numeric',
300
+ month: 'numeric',
301
+ day: 'numeric',
302
+ hour: 'numeric',
303
+ minute: 'numeric',
304
+ second: 'numeric',
305
+ hour12: false
306
+ };
307
+
308
+ let dateStr = typeof Intl === 'undefined' ? message.date : new Intl.DateTimeFormat('default', dateOptions).format(new Date(message.date));
309
+
310
+ rows.push(
311
+ `<div class="postal-email-header-key">Date</div><div class="postal-email-header-value postal-email-header-date" data-date="${escapeHtml(
312
+ message.date
313
+ )}">${escapeHtml(dateStr)}</div>`
314
+ );
315
+ }
316
+
317
+ if (message.to && message.to.length) {
318
+ rows.push(`<div class="postal-email-header-key">To</div><div class="postal-email-header-value">${formatHtmlAddresses(message.to)}</div>`);
319
+ }
320
+
321
+ if (message.cc && message.cc.length) {
322
+ rows.push(`<div class="postal-email-header-key">Cc</div><div class="postal-email-header-value">${formatHtmlAddresses(message.cc)}</div>`);
323
+ }
324
+
325
+ if (message.bcc && message.bcc.length) {
326
+ rows.push(`<div class="postal-email-header-key">Bcc</div><div class="postal-email-header-value">${formatHtmlAddresses(message.bcc)}</div>`);
327
+ }
328
+
329
+ let template = `<div class="postal-email-header">${rows.length ? '<div class="postal-email-header-row">' : ''}${rows.join(
330
+ '</div>\n<div class="postal-email-header-row">'
331
+ )}${rows.length ? '</div>' : ''}</div>`;
332
+
333
+ return template;
334
+ }
@@ -1,37 +0,0 @@
1
- on:
2
- push:
3
- branches:
4
- - master
5
-
6
- permissions:
7
- contents: write
8
- pull-requests: write
9
- id-token: write
10
-
11
- name: release
12
- jobs:
13
- release-please:
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: google-github-actions/release-please-action@v3
17
- id: release
18
- with:
19
- release-type: node
20
- package-name: ${{vars.NPM_MODULE_NAME}}
21
- pull-request-title-pattern: 'chore${scope}: release ${version} [skip-ci]'
22
- # The logic below handles the npm publication:
23
- - uses: actions/checkout@v3
24
- # these if statements ensure that a publication only occurs when
25
- # a new release is created:
26
- if: ${{ steps.release.outputs.release_created }}
27
- - uses: actions/setup-node@v3
28
- with:
29
- node-version: 18
30
- registry-url: 'https://registry.npmjs.org'
31
- if: ${{ steps.release.outputs.release_created }}
32
- - run: npm ci
33
- if: ${{ steps.release.outputs.release_created }}
34
- - run: npm publish --provenance --access public
35
- env:
36
- NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
37
- if: ${{ steps.release.outputs.release_created }}
File without changes