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.
- package/.prettierrc.cjs +8 -0
- package/CHANGELOG.md +15 -0
- package/README.md +0 -2
- package/package.json +4 -4
- package/postal-mime.d.ts +1 -1
- package/src/address-parser.js +321 -0
- package/src/base64-decoder.js +50 -0
- package/src/decode-strings.js +268 -0
- package/src/html-entities.js +2236 -0
- package/src/mime-node.js +271 -0
- package/src/package.json +3 -0
- package/src/pass-through-decoder.js +17 -0
- package/src/postal-mime.js +395 -0
- package/src/qp-decoder.js +96 -0
- package/src/text-format.js +334 -0
- package/.github/workflows/release.yaml +0 -37
- /package/{.eslintrc.js → .eslintrc.cjs} +0 -0
package/src/mime-node.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { getDecoder, decodeParameterValueContinuations, textEncoder } from './decode-strings.js';
|
|
2
|
+
import PassThroughDecoder from './pass-through-decoder.js';
|
|
3
|
+
import Base64Decoder from './base64-decoder.js';
|
|
4
|
+
import QPDecoder from './qp-decoder.js';
|
|
5
|
+
|
|
6
|
+
export default class MimeNode {
|
|
7
|
+
constructor(opts) {
|
|
8
|
+
opts = opts || {};
|
|
9
|
+
|
|
10
|
+
this.postalMime = opts.postalMime;
|
|
11
|
+
|
|
12
|
+
this.root = !!opts.parentNode;
|
|
13
|
+
this.childNodes = [];
|
|
14
|
+
if (opts.parentNode) {
|
|
15
|
+
opts.parentNode.childNodes.push(this);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.state = 'header';
|
|
19
|
+
|
|
20
|
+
this.headerLines = [];
|
|
21
|
+
this.decoders = new Map();
|
|
22
|
+
|
|
23
|
+
this.contentType = {
|
|
24
|
+
value: 'text/plain',
|
|
25
|
+
default: true
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
this.contentTransferEncoding = {
|
|
29
|
+
value: '8bit'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
this.contentDisposition = {
|
|
33
|
+
value: ''
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
this.headers = [];
|
|
37
|
+
|
|
38
|
+
this.contentDecoder = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setupContentDecoder(transferEncoding) {
|
|
42
|
+
if (/base64/i.test(transferEncoding)) {
|
|
43
|
+
this.contentDecoder = new Base64Decoder();
|
|
44
|
+
} else if (/quoted-printable/i.test(transferEncoding)) {
|
|
45
|
+
this.contentDecoder = new QPDecoder({ decoder: getDecoder(this.contentType.parsed.params.charset) });
|
|
46
|
+
} else {
|
|
47
|
+
this.contentDecoder = new PassThroughDecoder();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async finalize() {
|
|
52
|
+
if (this.state === 'finished') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (this.state === 'header') {
|
|
57
|
+
this.processHeaders();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// remove self from boundary listing
|
|
61
|
+
let boundaries = this.postalMime.boundaries;
|
|
62
|
+
for (let i = boundaries.length - 1; i >= 0; i--) {
|
|
63
|
+
let boundary = boundaries[i];
|
|
64
|
+
if (boundary.node === this) {
|
|
65
|
+
boundaries.splice(i, 1);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await this.finalizeChildNodes();
|
|
71
|
+
|
|
72
|
+
this.content = this.contentDecoder ? await this.contentDecoder.finalize() : null;
|
|
73
|
+
|
|
74
|
+
this.state = 'finished';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async finalizeChildNodes() {
|
|
78
|
+
for (let childNode of this.childNodes) {
|
|
79
|
+
await childNode.finalize();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
parseStructuredHeader(str) {
|
|
84
|
+
let response = {
|
|
85
|
+
value: false,
|
|
86
|
+
params: {}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
let key = false;
|
|
90
|
+
let value = '';
|
|
91
|
+
let stage = 'value';
|
|
92
|
+
|
|
93
|
+
let quote = false;
|
|
94
|
+
let escaped = false;
|
|
95
|
+
let chr;
|
|
96
|
+
|
|
97
|
+
for (let i = 0, len = str.length; i < len; i++) {
|
|
98
|
+
chr = str.charAt(i);
|
|
99
|
+
switch (stage) {
|
|
100
|
+
case 'key':
|
|
101
|
+
if (chr === '=') {
|
|
102
|
+
key = value.trim().toLowerCase();
|
|
103
|
+
stage = 'value';
|
|
104
|
+
value = '';
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
value += chr;
|
|
108
|
+
break;
|
|
109
|
+
case 'value':
|
|
110
|
+
if (escaped) {
|
|
111
|
+
value += chr;
|
|
112
|
+
} else if (chr === '\\') {
|
|
113
|
+
escaped = true;
|
|
114
|
+
continue;
|
|
115
|
+
} else if (quote && chr === quote) {
|
|
116
|
+
quote = false;
|
|
117
|
+
} else if (!quote && chr === '"') {
|
|
118
|
+
quote = chr;
|
|
119
|
+
} else if (!quote && chr === ';') {
|
|
120
|
+
if (key === false) {
|
|
121
|
+
response.value = value.trim();
|
|
122
|
+
} else {
|
|
123
|
+
response.params[key] = value.trim();
|
|
124
|
+
}
|
|
125
|
+
stage = 'key';
|
|
126
|
+
value = '';
|
|
127
|
+
} else {
|
|
128
|
+
value += chr;
|
|
129
|
+
}
|
|
130
|
+
escaped = false;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// finalize remainder
|
|
136
|
+
value = value.trim();
|
|
137
|
+
if (stage === 'value') {
|
|
138
|
+
if (key === false) {
|
|
139
|
+
// default value
|
|
140
|
+
response.value = value;
|
|
141
|
+
} else {
|
|
142
|
+
// subkey value
|
|
143
|
+
response.params[key] = value;
|
|
144
|
+
}
|
|
145
|
+
} else if (value) {
|
|
146
|
+
// treat as key without value, see emptykey:
|
|
147
|
+
// Header-Key: somevalue; key=value; emptykey
|
|
148
|
+
response.params[value.toLowerCase()] = '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (response.value) {
|
|
152
|
+
response.value = response.value.toLowerCase();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// convert Parameter Value Continuations into single strings
|
|
156
|
+
decodeParameterValueContinuations(response);
|
|
157
|
+
|
|
158
|
+
return response;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
decodeFlowedText(str, delSp) {
|
|
162
|
+
return (
|
|
163
|
+
str
|
|
164
|
+
.split(/\r?\n/)
|
|
165
|
+
// remove soft linebreaks
|
|
166
|
+
// soft linebreaks are added after space symbols
|
|
167
|
+
.reduce((previousValue, currentValue) => {
|
|
168
|
+
if (/ $/.test(previousValue) && !/(^|\n)-- $/.test(previousValue)) {
|
|
169
|
+
if (delSp) {
|
|
170
|
+
// delsp adds space to text to be able to fold it
|
|
171
|
+
// these spaces can be removed once the text is unfolded
|
|
172
|
+
return previousValue.slice(0, -1) + currentValue;
|
|
173
|
+
} else {
|
|
174
|
+
return previousValue + currentValue;
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
return previousValue + '\n' + currentValue;
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
// remove whitespace stuffing
|
|
181
|
+
// http://tools.ietf.org/html/rfc3676#section-4.4
|
|
182
|
+
.replace(/^ /gm, '')
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getTextContent() {
|
|
187
|
+
if (!this.content) {
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let str = getDecoder(this.contentType.parsed.params.charset).decode(this.content);
|
|
192
|
+
|
|
193
|
+
if (/^flowed$/i.test(this.contentType.parsed.params.format)) {
|
|
194
|
+
str = this.decodeFlowedText(str, /^yes$/i.test(this.contentType.parsed.params.delsp));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return str;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
processHeaders() {
|
|
201
|
+
for (let i = this.headerLines.length - 1; i >= 0; i--) {
|
|
202
|
+
let line = this.headerLines[i];
|
|
203
|
+
if (i && /^\s/.test(line)) {
|
|
204
|
+
this.headerLines[i - 1] += '\n' + line;
|
|
205
|
+
this.headerLines.splice(i, 1);
|
|
206
|
+
} else {
|
|
207
|
+
// remove folding and extra WS
|
|
208
|
+
line = line.replace(/\s+/g, ' ');
|
|
209
|
+
let sep = line.indexOf(':');
|
|
210
|
+
let key = sep < 0 ? line.trim() : line.substr(0, sep).trim();
|
|
211
|
+
let value = sep < 0 ? '' : line.substr(sep + 1).trim();
|
|
212
|
+
this.headers.push({ key: key.toLowerCase(), originalKey: key, value });
|
|
213
|
+
|
|
214
|
+
switch (key.toLowerCase()) {
|
|
215
|
+
case 'content-type':
|
|
216
|
+
if (this.contentType.default) {
|
|
217
|
+
this.contentType = { value, parsed: {} };
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
case 'content-transfer-encoding':
|
|
221
|
+
this.contentTransferEncoding = { value, parsed: {} };
|
|
222
|
+
break;
|
|
223
|
+
case 'content-disposition':
|
|
224
|
+
this.contentDisposition = { value, parsed: {} };
|
|
225
|
+
break;
|
|
226
|
+
case 'content-id':
|
|
227
|
+
this.contentId = value;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.contentType.parsed = this.parseStructuredHeader(this.contentType.value);
|
|
234
|
+
this.contentType.multipart = /^multipart\//i.test(this.contentType.parsed.value)
|
|
235
|
+
? this.contentType.parsed.value.substr(this.contentType.parsed.value.indexOf('/') + 1)
|
|
236
|
+
: false;
|
|
237
|
+
|
|
238
|
+
if (this.contentType.multipart && this.contentType.parsed.params.boundary) {
|
|
239
|
+
// add self to boundary terminator listing
|
|
240
|
+
this.postalMime.boundaries.push({
|
|
241
|
+
value: textEncoder.encode(this.contentType.parsed.params.boundary),
|
|
242
|
+
node: this
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this.contentDisposition.parsed = this.parseStructuredHeader(this.contentDisposition.value);
|
|
247
|
+
|
|
248
|
+
this.contentTransferEncoding.encoding = this.contentTransferEncoding.value
|
|
249
|
+
.toLowerCase()
|
|
250
|
+
.split(/[^\w-]/)
|
|
251
|
+
.shift();
|
|
252
|
+
|
|
253
|
+
this.setupContentDecoder(this.contentTransferEncoding.encoding);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
feed(line) {
|
|
257
|
+
switch (this.state) {
|
|
258
|
+
case 'header':
|
|
259
|
+
if (!line.length) {
|
|
260
|
+
this.state = 'body';
|
|
261
|
+
return this.processHeaders();
|
|
262
|
+
}
|
|
263
|
+
this.headerLines.push(getDecoder().decode(line));
|
|
264
|
+
break;
|
|
265
|
+
case 'body': {
|
|
266
|
+
// add line to body
|
|
267
|
+
this.contentDecoder.update(line);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
package/src/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { blobToArrayBuffer } from './decode-strings.js';
|
|
2
|
+
|
|
3
|
+
export default class PassThroughDecoder {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.chunks = [];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
update(line) {
|
|
9
|
+
this.chunks.push(line);
|
|
10
|
+
this.chunks.push('\n');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
finalize() {
|
|
14
|
+
// convert an array of arraybuffers into a blob and then back into a single arraybuffer
|
|
15
|
+
return blobToArrayBuffer(new Blob(this.chunks, { type: 'application/octet-stream' }));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import MimeNode from './mime-node.js';
|
|
2
|
+
import { textToHtml, htmlToText, formatTextHeader, formatHtmlHeader } from './text-format.js';
|
|
3
|
+
import addressParser from './address-parser.js';
|
|
4
|
+
import { decodeWords, textEncoder, blobToArrayBuffer } from './decode-strings.js';
|
|
5
|
+
|
|
6
|
+
export default class PostalMime {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.root = this.currentNode = new MimeNode({
|
|
9
|
+
postalMime: this
|
|
10
|
+
});
|
|
11
|
+
this.boundaries = [];
|
|
12
|
+
|
|
13
|
+
this.textContent = {};
|
|
14
|
+
this.attachments = [];
|
|
15
|
+
|
|
16
|
+
this.started = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async finalize() {
|
|
20
|
+
// close all pending nodes
|
|
21
|
+
await this.root.finalize();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async processLine(line, isFinal) {
|
|
25
|
+
let boundaries = this.boundaries;
|
|
26
|
+
|
|
27
|
+
// check if this is a mime boundary
|
|
28
|
+
if (boundaries.length && line.length > 2 && line[0] === 0x2d && line[1] === 0x2d) {
|
|
29
|
+
// could be a boundary marker
|
|
30
|
+
for (let i = boundaries.length - 1; i >= 0; i--) {
|
|
31
|
+
let boundary = boundaries[i];
|
|
32
|
+
|
|
33
|
+
if (line.length !== boundary.value.length + 2 && line.length !== boundary.value.length + 4) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let isTerminator = line.length === boundary.value.length + 4;
|
|
38
|
+
|
|
39
|
+
if (isTerminator && (line[line.length - 2] !== 0x2d || line[line.length - 1] !== 0x2d)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let boudaryMatches = true;
|
|
44
|
+
for (let i = 0; i < boundary.value.length; i++) {
|
|
45
|
+
if (line[i + 2] !== boundary.value[i]) {
|
|
46
|
+
boudaryMatches = false;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!boudaryMatches) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isTerminator) {
|
|
55
|
+
await boundary.node.finalize();
|
|
56
|
+
|
|
57
|
+
this.currentNode = boundary.node.parentNode || this.root;
|
|
58
|
+
} else {
|
|
59
|
+
// finalize any open child nodes (should be just one though)
|
|
60
|
+
await boundary.node.finalizeChildNodes();
|
|
61
|
+
|
|
62
|
+
this.currentNode = new MimeNode({
|
|
63
|
+
postalMime: this,
|
|
64
|
+
parentNode: boundary.node
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (isFinal) {
|
|
69
|
+
return this.finalize();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.currentNode.feed(line);
|
|
77
|
+
|
|
78
|
+
if (isFinal) {
|
|
79
|
+
return this.finalize();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
readLine() {
|
|
84
|
+
let startPos = this.readPos;
|
|
85
|
+
let endPos = this.readPos;
|
|
86
|
+
|
|
87
|
+
let res = () => {
|
|
88
|
+
return {
|
|
89
|
+
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
|
|
90
|
+
done: this.readPos >= this.av.length
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
while (this.readPos < this.av.length) {
|
|
95
|
+
const c = this.av[this.readPos++];
|
|
96
|
+
|
|
97
|
+
if (c !== 0x0d && c !== 0x0a) {
|
|
98
|
+
endPos = this.readPos;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (c === 0x0a) {
|
|
102
|
+
return res();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return res();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async processNodeTree() {
|
|
110
|
+
// get text nodes
|
|
111
|
+
|
|
112
|
+
let textContent = {};
|
|
113
|
+
|
|
114
|
+
let textTypes = new Set();
|
|
115
|
+
let textMap = (this.textMap = new Map());
|
|
116
|
+
|
|
117
|
+
let walk = async (node, alternative, related) => {
|
|
118
|
+
alternative = alternative || false;
|
|
119
|
+
related = related || false;
|
|
120
|
+
|
|
121
|
+
if (!node.contentType.multipart) {
|
|
122
|
+
// regular node
|
|
123
|
+
|
|
124
|
+
// is it inline message/rfc822
|
|
125
|
+
if (node.contentType.parsed.value === 'message/rfc822' && node.contentDisposition.parsed.value !== 'attachment') {
|
|
126
|
+
const subParser = new PostalMime();
|
|
127
|
+
node.subMessage = await subParser.parse(node.content);
|
|
128
|
+
|
|
129
|
+
if (!textMap.has(node)) {
|
|
130
|
+
textMap.set(node, {});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let textEntry = textMap.get(node);
|
|
134
|
+
|
|
135
|
+
// default to text if there is no content
|
|
136
|
+
if (node.subMessage.text || !node.subMessage.html) {
|
|
137
|
+
textEntry.plain = textEntry.plain || [];
|
|
138
|
+
textEntry.plain.push({ type: 'subMessage', value: node.subMessage });
|
|
139
|
+
textTypes.add('plain');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (node.subMessage.html) {
|
|
143
|
+
textEntry.html = textEntry.html || [];
|
|
144
|
+
textEntry.html.push({ type: 'subMessage', value: node.subMessage });
|
|
145
|
+
textTypes.add('html');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (subParser.textMap) {
|
|
149
|
+
subParser.textMap.forEach((subTextEntry, subTextNode) => {
|
|
150
|
+
textMap.set(subTextNode, subTextEntry);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (let attachment of node.subMessage.attachments || []) {
|
|
155
|
+
this.attachments.push(attachment);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// is it text?
|
|
160
|
+
else if (
|
|
161
|
+
(/^text\//i.test(node.contentType.parsed.value) || node.contentType.parsed.value === 'message/delivery-status') &&
|
|
162
|
+
node.contentDisposition.parsed.value !== 'attachment'
|
|
163
|
+
) {
|
|
164
|
+
let textType = node.contentType.parsed.value.substr(node.contentType.parsed.value.indexOf('/') + 1);
|
|
165
|
+
if (node.contentType.parsed.value === 'message/delivery-status') {
|
|
166
|
+
textType = 'plain';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let selectorNode = alternative || node;
|
|
170
|
+
if (!textMap.has(selectorNode)) {
|
|
171
|
+
textMap.set(selectorNode, {});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let textEntry = textMap.get(selectorNode);
|
|
175
|
+
textEntry[textType] = textEntry[textType] || [];
|
|
176
|
+
textEntry[textType].push({ type: 'text', value: node.getTextContent() });
|
|
177
|
+
textTypes.add(textType);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// is it an attachment
|
|
181
|
+
else if (node.content) {
|
|
182
|
+
let filename = node.contentDisposition.parsed.params.filename || node.contentType.parsed.params.name || null;
|
|
183
|
+
let attachment = {
|
|
184
|
+
filename: decodeWords(filename),
|
|
185
|
+
mimeType: node.contentType.parsed.value,
|
|
186
|
+
disposition: node.contentDisposition.parsed.value || null
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (related && node.contentId) {
|
|
190
|
+
attachment.related = true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (node.contentId) {
|
|
194
|
+
attachment.contentId = node.contentId;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
attachment.content = node.content;
|
|
198
|
+
|
|
199
|
+
this.attachments.push(attachment);
|
|
200
|
+
}
|
|
201
|
+
} else if (node.contentType.multipart === 'alternative') {
|
|
202
|
+
alternative = node;
|
|
203
|
+
} else if (node.contentType.multipart === 'related') {
|
|
204
|
+
related = node;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (let childNode of node.childNodes) {
|
|
208
|
+
await walk(childNode, alternative, related);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
await walk(this.root, false, []);
|
|
213
|
+
|
|
214
|
+
textMap.forEach(mapEntry => {
|
|
215
|
+
textTypes.forEach(textType => {
|
|
216
|
+
if (!textContent[textType]) {
|
|
217
|
+
textContent[textType] = [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (mapEntry[textType]) {
|
|
221
|
+
mapEntry[textType].forEach(textEntry => {
|
|
222
|
+
switch (textEntry.type) {
|
|
223
|
+
case 'text':
|
|
224
|
+
textContent[textType].push(textEntry.value);
|
|
225
|
+
break;
|
|
226
|
+
|
|
227
|
+
case 'subMessage':
|
|
228
|
+
{
|
|
229
|
+
switch (textType) {
|
|
230
|
+
case 'html':
|
|
231
|
+
textContent[textType].push(formatHtmlHeader(textEntry.value));
|
|
232
|
+
break;
|
|
233
|
+
case 'plain':
|
|
234
|
+
textContent[textType].push(formatTextHeader(textEntry.value));
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
let alternativeType;
|
|
243
|
+
switch (textType) {
|
|
244
|
+
case 'html':
|
|
245
|
+
alternativeType = 'plain';
|
|
246
|
+
break;
|
|
247
|
+
case 'plain':
|
|
248
|
+
alternativeType = 'html';
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
(mapEntry[alternativeType] || []).forEach(textEntry => {
|
|
253
|
+
switch (textEntry.type) {
|
|
254
|
+
case 'text':
|
|
255
|
+
switch (textType) {
|
|
256
|
+
case 'html':
|
|
257
|
+
textContent[textType].push(textToHtml(textEntry.value));
|
|
258
|
+
break;
|
|
259
|
+
case 'plain':
|
|
260
|
+
textContent[textType].push(htmlToText(textEntry.value));
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
|
|
265
|
+
case 'subMessage':
|
|
266
|
+
{
|
|
267
|
+
switch (textType) {
|
|
268
|
+
case 'html':
|
|
269
|
+
textContent[textType].push(formatHtmlHeader(textEntry.value));
|
|
270
|
+
break;
|
|
271
|
+
case 'plain':
|
|
272
|
+
textContent[textType].push(formatTextHeader(textEntry.value));
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
Object.keys(textContent).forEach(textType => {
|
|
284
|
+
textContent[textType] = textContent[textType].join('\n');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
this.textContent = textContent;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async parse(buf) {
|
|
291
|
+
if (this.started) {
|
|
292
|
+
throw new Error('Can not reuse parser, create a new PostalMime object');
|
|
293
|
+
}
|
|
294
|
+
this.started = true;
|
|
295
|
+
|
|
296
|
+
// should it thrown on empty value instead?
|
|
297
|
+
buf = buf || ArrayBuffer(0);
|
|
298
|
+
|
|
299
|
+
if (typeof buf === 'string') {
|
|
300
|
+
// cast string input to ArrayBuffer
|
|
301
|
+
buf = textEncoder.encode(buf);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (buf instanceof Blob || Object.prototype.toString.call(buf) === '[object Blob]') {
|
|
305
|
+
// can't process blob directly, cast to ArrayBuffer
|
|
306
|
+
buf = await blobToArrayBuffer(buf);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (buf.buffer instanceof ArrayBuffer) {
|
|
310
|
+
// Node.js Buffer object or Uint8Array
|
|
311
|
+
buf = new Uint8Array(buf).buffer;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.buf = buf;
|
|
315
|
+
this.av = new Uint8Array(buf);
|
|
316
|
+
this.readPos = 0;
|
|
317
|
+
|
|
318
|
+
while (this.readPos < this.av.length) {
|
|
319
|
+
const line = this.readLine();
|
|
320
|
+
|
|
321
|
+
await this.processLine(line.bytes, line.done);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
await this.processNodeTree();
|
|
325
|
+
|
|
326
|
+
let message = {
|
|
327
|
+
headers: this.root.headers.map(entry => ({ key: entry.key, value: entry.value })).reverse()
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
for (let key of ['from', 'sender', 'reply-to']) {
|
|
331
|
+
let addressHeader = this.root.headers.find(line => line.key === key);
|
|
332
|
+
if (addressHeader && addressHeader.value) {
|
|
333
|
+
let addresses = addressParser(addressHeader.value);
|
|
334
|
+
if (addresses && addresses.length) {
|
|
335
|
+
message[key.replace(/\-(.)/g, (o, c) => c.toUpperCase())] = addresses[0];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
for (let key of ['delivered-to', 'return-path']) {
|
|
341
|
+
let addressHeader = this.root.headers.find(line => line.key === key);
|
|
342
|
+
if (addressHeader && addressHeader.value) {
|
|
343
|
+
let addresses = addressParser(addressHeader.value);
|
|
344
|
+
if (addresses && addresses.length && addresses[0].address) {
|
|
345
|
+
message[key.replace(/\-(.)/g, (o, c) => c.toUpperCase())] = addresses[0].address;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (let key of ['to', 'cc', 'bcc']) {
|
|
351
|
+
let addressHeaders = this.root.headers.filter(line => line.key === key);
|
|
352
|
+
let addresses = [];
|
|
353
|
+
|
|
354
|
+
addressHeaders
|
|
355
|
+
.filter(entry => entry && entry.value)
|
|
356
|
+
.map(entry => addressParser(entry.value))
|
|
357
|
+
.forEach(parsed => (addresses = addresses.concat(parsed || [])));
|
|
358
|
+
|
|
359
|
+
if (addresses && addresses.length) {
|
|
360
|
+
message[key] = addresses;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
for (let key of ['subject', 'message-id', 'in-reply-to', 'references']) {
|
|
365
|
+
let header = this.root.headers.find(line => line.key === key);
|
|
366
|
+
if (header && header.value) {
|
|
367
|
+
message[key.replace(/\-(.)/g, (o, c) => c.toUpperCase())] = decodeWords(header.value);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let dateHeader = this.root.headers.find(line => line.key === 'date');
|
|
372
|
+
if (dateHeader) {
|
|
373
|
+
let date = new Date(dateHeader.value);
|
|
374
|
+
if (!date || date.toString() === 'Invalid Date') {
|
|
375
|
+
date = dateHeader.value;
|
|
376
|
+
} else {
|
|
377
|
+
// enforce ISO format if seems to be a valid date
|
|
378
|
+
date = date.toISOString();
|
|
379
|
+
}
|
|
380
|
+
message.date = date;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (this.textContent && this.textContent.html) {
|
|
384
|
+
message.html = this.textContent.html;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (this.textContent && this.textContent.plain) {
|
|
388
|
+
message.text = this.textContent.plain;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
message.attachments = this.attachments;
|
|
392
|
+
|
|
393
|
+
return message;
|
|
394
|
+
}
|
|
395
|
+
}
|