postal-mime 2.7.3 → 2.7.4
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/CHANGELOG.md +10 -0
- package/README.md +3 -0
- package/dist/address-parser.cjs +22 -19
- package/dist/base64-decoder.cjs +5 -4
- package/dist/base64-encoder.cjs +4 -1
- package/dist/decode-strings.cjs +5 -2
- package/dist/html-entities.cjs +4 -1
- package/dist/mime-node.cjs +7 -3
- package/dist/pass-through-decoder.cjs +4 -1
- package/dist/postal-mime.cjs +22 -16
- package/dist/qp-decoder.cjs +4 -2
- package/dist/text-format.cjs +5 -2
- package/package.json +4 -4
- package/postal-mime.d.ts +3 -1
- package/src/address-parser.js +25 -21
- package/src/base64-decoder.js +1 -3
- package/src/decode-strings.js +1 -1
- package/src/mime-node.js +4 -2
- package/src/postal-mime.js +28 -19
- package/src/qp-decoder.js +0 -1
- package/src/text-format.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.7.4](https://github.com/postalsys/postal-mime/compare/v2.7.3...v2.7.4) (2026-03-17)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* add missing originalKey to Header type and Uint8Array to Attachment content ([92cc91c](https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0))
|
|
9
|
+
* include originalKey in parsed headers output ([83521c8](https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347))
|
|
10
|
+
* preserve __esModule and .default in CJS build for bundler interop ([1466910](https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048))
|
|
11
|
+
* prevent RFC 2047 encoded-word address fabrication ([844f920](https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3))
|
|
12
|
+
|
|
3
13
|
## [2.7.3](https://github.com/postalsys/postal-mime/compare/v2.7.2...v2.7.3) (2026-01-09)
|
|
4
14
|
|
|
5
15
|
|
package/README.md
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
- **Handles complex MIME structures** - Multipart messages, nested parts, attachments
|
|
15
15
|
- **Security limits** - Built-in protection against deeply nested messages and oversized headers
|
|
16
16
|
|
|
17
|
+
> [!NOTE]
|
|
18
|
+
> Full documentation is available at [postal-mime.postalsys.com](https://postal-mime.postalsys.com/).
|
|
19
|
+
|
|
17
20
|
## Table of Contents
|
|
18
21
|
|
|
19
22
|
- [Source](#source)
|
package/dist/address-parser.cjs
CHANGED
|
@@ -141,27 +141,27 @@ function _handleAddress(tokens, depth) {
|
|
|
141
141
|
data.text = data.text.join(" ");
|
|
142
142
|
data.address = data.address.join(" ");
|
|
143
143
|
if (!data.address && /^=\?[^=]+?=$/.test(data.text.trim())) {
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
-
|
|
144
|
+
const decodedText = (0, import_decode_strings.decodeWords)(data.text);
|
|
145
|
+
if (/<[^<>]+@[^<>]+>/.test(decodedText)) {
|
|
146
|
+
const parsedSubAddresses = addressParser(decodedText);
|
|
147
|
+
if (parsedSubAddresses && parsedSubAddresses.length) {
|
|
148
|
+
return parsedSubAddresses;
|
|
149
|
+
}
|
|
147
150
|
}
|
|
151
|
+
return [{ address: "", name: decodedText }];
|
|
148
152
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
address.name = "";
|
|
159
|
-
} else {
|
|
160
|
-
address.address = "";
|
|
161
|
-
}
|
|
153
|
+
address = {
|
|
154
|
+
address: data.address || data.text || "",
|
|
155
|
+
name: (0, import_decode_strings.decodeWords)(data.text || data.address || "")
|
|
156
|
+
};
|
|
157
|
+
if (address.address === address.name) {
|
|
158
|
+
if ((address.address || "").match(/@/)) {
|
|
159
|
+
address.name = "";
|
|
160
|
+
} else {
|
|
161
|
+
address.address = "";
|
|
162
162
|
}
|
|
163
|
-
addresses.push(address);
|
|
164
163
|
}
|
|
164
|
+
addresses.push(address);
|
|
165
165
|
}
|
|
166
166
|
return addresses;
|
|
167
167
|
}
|
|
@@ -238,7 +238,7 @@ class Tokenizer {
|
|
|
238
238
|
this.operatorExpecting = this.operators[chr];
|
|
239
239
|
this.escaped = false;
|
|
240
240
|
return;
|
|
241
|
-
} else if (
|
|
241
|
+
} else if (this.operatorExpecting === '"' && chr === "\\") {
|
|
242
242
|
this.escaped = true;
|
|
243
243
|
return;
|
|
244
244
|
}
|
|
@@ -312,11 +312,14 @@ if (module.exports.default) {
|
|
|
312
312
|
var defaultExport = module.exports.default;
|
|
313
313
|
var namedExports = {};
|
|
314
314
|
for (var key in module.exports) {
|
|
315
|
-
if (key !== 'default') {
|
|
315
|
+
if (key !== 'default' && key !== '__esModule') {
|
|
316
316
|
namedExports[key] = module.exports[key];
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
module.exports = defaultExport;
|
|
320
320
|
Object.assign(module.exports, namedExports);
|
|
321
|
+
// Preserve __esModule and .default for bundler/transpiler interop
|
|
322
|
+
Object.defineProperty(module.exports, '__esModule', { value: true });
|
|
323
|
+
module.exports.default = defaultExport;
|
|
321
324
|
}
|
|
322
325
|
|
package/dist/base64-decoder.cjs
CHANGED
|
@@ -32,9 +32,7 @@ class Base64Decoder {
|
|
|
32
32
|
}
|
|
33
33
|
update(buffer) {
|
|
34
34
|
let str = this.decoder.decode(buffer);
|
|
35
|
-
|
|
36
|
-
str = str.replace(/[^a-zA-Z0-9+\/]+/g, "");
|
|
37
|
-
}
|
|
35
|
+
str = str.replace(/[^a-zA-Z0-9+\/]+/g, "");
|
|
38
36
|
this.remainder += str;
|
|
39
37
|
if (this.remainder.length >= this.maxChunkSize) {
|
|
40
38
|
let allowedBytes = Math.floor(this.remainder.length / 4) * 4;
|
|
@@ -64,11 +62,14 @@ if (module.exports.default) {
|
|
|
64
62
|
var defaultExport = module.exports.default;
|
|
65
63
|
var namedExports = {};
|
|
66
64
|
for (var key in module.exports) {
|
|
67
|
-
if (key !== 'default') {
|
|
65
|
+
if (key !== 'default' && key !== '__esModule') {
|
|
68
66
|
namedExports[key] = module.exports[key];
|
|
69
67
|
}
|
|
70
68
|
}
|
|
71
69
|
module.exports = defaultExport;
|
|
72
70
|
Object.assign(module.exports, namedExports);
|
|
71
|
+
// Preserve __esModule and .default for bundler/transpiler interop
|
|
72
|
+
Object.defineProperty(module.exports, '__esModule', { value: true });
|
|
73
|
+
module.exports.default = defaultExport;
|
|
73
74
|
}
|
|
74
75
|
|
package/dist/base64-encoder.cjs
CHANGED
|
@@ -62,11 +62,14 @@ if (module.exports.default) {
|
|
|
62
62
|
var defaultExport = module.exports.default;
|
|
63
63
|
var namedExports = {};
|
|
64
64
|
for (var key in module.exports) {
|
|
65
|
-
if (key !== 'default') {
|
|
65
|
+
if (key !== 'default' && key !== '__esModule') {
|
|
66
66
|
namedExports[key] = module.exports[key];
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
module.exports = defaultExport;
|
|
70
70
|
Object.assign(module.exports, namedExports);
|
|
71
|
+
// Preserve __esModule and .default for bundler/transpiler interop
|
|
72
|
+
Object.defineProperty(module.exports, '__esModule', { value: true });
|
|
73
|
+
module.exports.default = defaultExport;
|
|
71
74
|
}
|
|
72
75
|
|
package/dist/decode-strings.cjs
CHANGED
|
@@ -32,7 +32,7 @@ module.exports = __toCommonJS(decode_strings_exports);
|
|
|
32
32
|
const textEncoder = new TextEncoder();
|
|
33
33
|
const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
34
34
|
const base64Lookup = new Uint8Array(256);
|
|
35
|
-
for (
|
|
35
|
+
for (let i = 0; i < base64Chars.length; i++) {
|
|
36
36
|
base64Lookup[base64Chars.charCodeAt(i)] = i;
|
|
37
37
|
}
|
|
38
38
|
function decodeBase64(base64) {
|
|
@@ -244,11 +244,14 @@ if (module.exports.default) {
|
|
|
244
244
|
var defaultExport = module.exports.default;
|
|
245
245
|
var namedExports = {};
|
|
246
246
|
for (var key in module.exports) {
|
|
247
|
-
if (key !== 'default') {
|
|
247
|
+
if (key !== 'default' && key !== '__esModule') {
|
|
248
248
|
namedExports[key] = module.exports[key];
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
module.exports = defaultExport;
|
|
252
252
|
Object.assign(module.exports, namedExports);
|
|
253
|
+
// Preserve __esModule and .default for bundler/transpiler interop
|
|
254
|
+
Object.defineProperty(module.exports, '__esModule', { value: true });
|
|
255
|
+
module.exports.default = defaultExport;
|
|
253
256
|
}
|
|
254
257
|
|
package/dist/html-entities.cjs
CHANGED
|
@@ -2266,11 +2266,14 @@ if (module.exports.default) {
|
|
|
2266
2266
|
var defaultExport = module.exports.default;
|
|
2267
2267
|
var namedExports = {};
|
|
2268
2268
|
for (var key in module.exports) {
|
|
2269
|
-
if (key !== 'default') {
|
|
2269
|
+
if (key !== 'default' && key !== '__esModule') {
|
|
2270
2270
|
namedExports[key] = module.exports[key];
|
|
2271
2271
|
}
|
|
2272
2272
|
}
|
|
2273
2273
|
module.exports = defaultExport;
|
|
2274
2274
|
Object.assign(module.exports, namedExports);
|
|
2275
|
+
// Preserve __esModule and .default for bundler/transpiler interop
|
|
2276
|
+
Object.defineProperty(module.exports, '__esModule', { value: true });
|
|
2277
|
+
module.exports.default = defaultExport;
|
|
2275
2278
|
}
|
|
2276
2279
|
|
package/dist/mime-node.cjs
CHANGED
|
@@ -35,6 +35,7 @@ var import_decode_strings = require("./decode-strings.cjs");
|
|
|
35
35
|
var import_pass_through_decoder = __toESM(require("./pass-through-decoder.cjs"), 1);
|
|
36
36
|
var import_base64_decoder = __toESM(require("./base64-decoder.cjs"), 1);
|
|
37
37
|
var import_qp_decoder = __toESM(require("./qp-decoder.cjs"), 1);
|
|
38
|
+
const defaultDecoder = (0, import_decode_strings.getDecoder)();
|
|
38
39
|
class MimeNode {
|
|
39
40
|
constructor(options) {
|
|
40
41
|
this.options = options || {};
|
|
@@ -212,7 +213,7 @@ class MimeNode {
|
|
|
212
213
|
}
|
|
213
214
|
decodeFlowedText(str, delSp) {
|
|
214
215
|
return str.split(/\r?\n/).reduce((previousValue, currentValue) => {
|
|
215
|
-
if (
|
|
216
|
+
if (previousValue.endsWith(" ") && previousValue !== "-- " && !previousValue.endsWith("\n-- ")) {
|
|
216
217
|
if (delSp) {
|
|
217
218
|
return previousValue.slice(0, -1) + currentValue;
|
|
218
219
|
} else {
|
|
@@ -299,7 +300,7 @@ class MimeNode {
|
|
|
299
300
|
let error = new Error(`Maximum header size of ${this.options.maxHeadersSize} bytes exceeded`);
|
|
300
301
|
throw error;
|
|
301
302
|
}
|
|
302
|
-
this.headerLines.push(
|
|
303
|
+
this.headerLines.push(defaultDecoder.decode(line));
|
|
303
304
|
break;
|
|
304
305
|
case "body": {
|
|
305
306
|
this.contentDecoder.update(line);
|
|
@@ -313,11 +314,14 @@ if (module.exports.default) {
|
|
|
313
314
|
var defaultExport = module.exports.default;
|
|
314
315
|
var namedExports = {};
|
|
315
316
|
for (var key in module.exports) {
|
|
316
|
-
if (key !== 'default') {
|
|
317
|
+
if (key !== 'default' && key !== '__esModule') {
|
|
317
318
|
namedExports[key] = module.exports[key];
|
|
318
319
|
}
|
|
319
320
|
}
|
|
320
321
|
module.exports = defaultExport;
|
|
321
322
|
Object.assign(module.exports, namedExports);
|
|
323
|
+
// Preserve __esModule and .default for bundler/transpiler interop
|
|
324
|
+
Object.defineProperty(module.exports, '__esModule', { value: true });
|
|
325
|
+
module.exports.default = defaultExport;
|
|
322
326
|
}
|
|
323
327
|
|
|
@@ -40,11 +40,14 @@ if (module.exports.default) {
|
|
|
40
40
|
var defaultExport = module.exports.default;
|
|
41
41
|
var namedExports = {};
|
|
42
42
|
for (var key in module.exports) {
|
|
43
|
-
if (key !== 'default') {
|
|
43
|
+
if (key !== 'default' && key !== '__esModule') {
|
|
44
44
|
namedExports[key] = module.exports[key];
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
module.exports = defaultExport;
|
|
48
48
|
Object.assign(module.exports, namedExports);
|
|
49
|
+
// Preserve __esModule and .default for bundler/transpiler interop
|
|
50
|
+
Object.defineProperty(module.exports, '__esModule', { value: true });
|
|
51
|
+
module.exports.default = defaultExport;
|
|
49
52
|
}
|
|
50
53
|
|
package/dist/postal-mime.cjs
CHANGED
|
@@ -40,6 +40,9 @@ var import_decode_strings = require("./decode-strings.cjs");
|
|
|
40
40
|
var import_base64_encoder = require("./base64-encoder.cjs");
|
|
41
41
|
const MAX_NESTING_DEPTH = 256;
|
|
42
42
|
const MAX_HEADERS_SIZE = 2 * 1024 * 1024;
|
|
43
|
+
function toCamelCase(key) {
|
|
44
|
+
return key.replace(/-(.)/g, (o, c) => c.toUpperCase());
|
|
45
|
+
}
|
|
43
46
|
class PostalMime {
|
|
44
47
|
static parse(buf, options) {
|
|
45
48
|
const parser = new PostalMime(options);
|
|
@@ -124,22 +127,22 @@ class PostalMime {
|
|
|
124
127
|
readLine() {
|
|
125
128
|
let startPos = this.readPos;
|
|
126
129
|
let endPos = this.readPos;
|
|
127
|
-
let res = () => {
|
|
128
|
-
return {
|
|
129
|
-
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
|
|
130
|
-
done: this.readPos >= this.av.length
|
|
131
|
-
};
|
|
132
|
-
};
|
|
133
130
|
while (this.readPos < this.av.length) {
|
|
134
131
|
const c = this.av[this.readPos++];
|
|
135
132
|
if (c !== 13 && c !== 10) {
|
|
136
133
|
endPos = this.readPos;
|
|
137
134
|
}
|
|
138
135
|
if (c === 10) {
|
|
139
|
-
return
|
|
136
|
+
return {
|
|
137
|
+
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
|
|
138
|
+
done: this.readPos >= this.av.length
|
|
139
|
+
};
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
-
return
|
|
142
|
+
return {
|
|
143
|
+
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
|
|
144
|
+
done: this.readPos >= this.av.length
|
|
145
|
+
};
|
|
143
146
|
}
|
|
144
147
|
async processNodeTree() {
|
|
145
148
|
let textContent = {};
|
|
@@ -228,7 +231,7 @@ class PostalMime {
|
|
|
228
231
|
await walk(childNode, alternative, related);
|
|
229
232
|
}
|
|
230
233
|
};
|
|
231
|
-
await walk(this.root, false,
|
|
234
|
+
await walk(this.root, false, false);
|
|
232
235
|
textMap.forEach((mapEntry) => {
|
|
233
236
|
textTypes.forEach((textType) => {
|
|
234
237
|
if (!textContent[textType]) {
|
|
@@ -388,7 +391,7 @@ class PostalMime {
|
|
|
388
391
|
}
|
|
389
392
|
await this.processNodeTree();
|
|
390
393
|
const message = {
|
|
391
|
-
headers: this.root.headers.map((entry) => ({ key: entry.key, value: entry.value })).reverse()
|
|
394
|
+
headers: this.root.headers.map((entry) => ({ key: entry.key, originalKey: entry.originalKey, value: entry.value })).reverse()
|
|
392
395
|
};
|
|
393
396
|
for (const key of ["from", "sender"]) {
|
|
394
397
|
const addressHeader = this.root.headers.find((line) => line.key === key);
|
|
@@ -404,7 +407,7 @@ class PostalMime {
|
|
|
404
407
|
if (addressHeader && addressHeader.value) {
|
|
405
408
|
const addresses = (0, import_address_parser.default)(addressHeader.value);
|
|
406
409
|
if (addresses && addresses.length && addresses[0].address) {
|
|
407
|
-
const camelKey = key
|
|
410
|
+
const camelKey = toCamelCase(key);
|
|
408
411
|
message[camelKey] = addresses[0].address;
|
|
409
412
|
}
|
|
410
413
|
}
|
|
@@ -414,21 +417,21 @@ class PostalMime {
|
|
|
414
417
|
let addresses = [];
|
|
415
418
|
addressHeaders.filter((entry) => entry && entry.value).map((entry) => (0, import_address_parser.default)(entry.value)).forEach((parsed) => addresses = addresses.concat(parsed || []));
|
|
416
419
|
if (addresses && addresses.length) {
|
|
417
|
-
const camelKey = key
|
|
420
|
+
const camelKey = toCamelCase(key);
|
|
418
421
|
message[camelKey] = addresses;
|
|
419
422
|
}
|
|
420
423
|
}
|
|
421
424
|
for (const key of ["subject", "message-id", "in-reply-to", "references"]) {
|
|
422
425
|
const header = this.root.headers.find((line) => line.key === key);
|
|
423
426
|
if (header && header.value) {
|
|
424
|
-
const camelKey = key
|
|
427
|
+
const camelKey = toCamelCase(key);
|
|
425
428
|
message[camelKey] = (0, import_decode_strings.decodeWords)(header.value);
|
|
426
429
|
}
|
|
427
430
|
}
|
|
428
431
|
let dateHeader = this.root.headers.find((line) => line.key === "date");
|
|
429
432
|
if (dateHeader) {
|
|
430
433
|
let date = new Date(dateHeader.value);
|
|
431
|
-
if (
|
|
434
|
+
if (date.toString() === "Invalid Date") {
|
|
432
435
|
date = dateHeader.value;
|
|
433
436
|
} else {
|
|
434
437
|
date = date.toISOString();
|
|
@@ -464,7 +467,7 @@ class PostalMime {
|
|
|
464
467
|
}
|
|
465
468
|
break;
|
|
466
469
|
default:
|
|
467
|
-
throw new Error("
|
|
470
|
+
throw new Error("Unknown attachment encoding");
|
|
468
471
|
}
|
|
469
472
|
return message;
|
|
470
473
|
}
|
|
@@ -480,11 +483,14 @@ if (module.exports.default) {
|
|
|
480
483
|
var defaultExport = module.exports.default;
|
|
481
484
|
var namedExports = {};
|
|
482
485
|
for (var key in module.exports) {
|
|
483
|
-
if (key !== 'default') {
|
|
486
|
+
if (key !== 'default' && key !== '__esModule') {
|
|
484
487
|
namedExports[key] = module.exports[key];
|
|
485
488
|
}
|
|
486
489
|
}
|
|
487
490
|
module.exports = defaultExport;
|
|
488
491
|
Object.assign(module.exports, namedExports);
|
|
492
|
+
// Preserve __esModule and .default for bundler/transpiler interop
|
|
493
|
+
Object.defineProperty(module.exports, '__esModule', { value: true });
|
|
494
|
+
module.exports.default = defaultExport;
|
|
489
495
|
}
|
|
490
496
|
|
package/dist/qp-decoder.cjs
CHANGED
|
@@ -86,7 +86,6 @@ class QPDecoder {
|
|
|
86
86
|
}
|
|
87
87
|
if (encodedBytes.length) {
|
|
88
88
|
this.chunks.push(this.decodeQPBytes(encodedBytes));
|
|
89
|
-
encodedBytes = [];
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
update(buffer) {
|
|
@@ -122,11 +121,14 @@ if (module.exports.default) {
|
|
|
122
121
|
var defaultExport = module.exports.default;
|
|
123
122
|
var namedExports = {};
|
|
124
123
|
for (var key in module.exports) {
|
|
125
|
-
if (key !== 'default') {
|
|
124
|
+
if (key !== 'default' && key !== '__esModule') {
|
|
126
125
|
namedExports[key] = module.exports[key];
|
|
127
126
|
}
|
|
128
127
|
}
|
|
129
128
|
module.exports = defaultExport;
|
|
130
129
|
Object.assign(module.exports, namedExports);
|
|
130
|
+
// Preserve __esModule and .default for bundler/transpiler interop
|
|
131
|
+
Object.defineProperty(module.exports, '__esModule', { value: true });
|
|
132
|
+
module.exports.default = defaultExport;
|
|
131
133
|
}
|
|
132
134
|
|
package/dist/text-format.cjs
CHANGED
|
@@ -51,7 +51,7 @@ function decodeHTMLEntities(str) {
|
|
|
51
51
|
} else {
|
|
52
52
|
codePoint = parseInt(entity.substr(1), 10);
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
let output = "";
|
|
55
55
|
if (codePoint >= 55296 && codePoint <= 57343 || codePoint > 1114111) {
|
|
56
56
|
return "\uFFFD";
|
|
57
57
|
}
|
|
@@ -271,11 +271,14 @@ if (module.exports.default) {
|
|
|
271
271
|
var defaultExport = module.exports.default;
|
|
272
272
|
var namedExports = {};
|
|
273
273
|
for (var key in module.exports) {
|
|
274
|
-
if (key !== 'default') {
|
|
274
|
+
if (key !== 'default' && key !== '__esModule') {
|
|
275
275
|
namedExports[key] = module.exports[key];
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
module.exports = defaultExport;
|
|
279
279
|
Object.assign(module.exports, namedExports);
|
|
280
|
+
// Preserve __esModule and .default for bundler/transpiler interop
|
|
281
|
+
Object.defineProperty(module.exports, '__esModule', { value: true });
|
|
282
|
+
module.exports.default = defaultExport;
|
|
280
283
|
}
|
|
281
284
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postal-mime",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.4",
|
|
4
4
|
"description": "Email parser for Node.js and browser environments",
|
|
5
5
|
"main": "./dist/postal-mime.cjs",
|
|
6
6
|
"module": "./src/postal-mime.js",
|
|
@@ -38,14 +38,14 @@
|
|
|
38
38
|
"author": "Andris Reinman",
|
|
39
39
|
"license": "MIT-0",
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@types/node": "
|
|
41
|
+
"@types/node": "25.5.0",
|
|
42
42
|
"cross-blob": "3.0.2",
|
|
43
43
|
"cross-env": "10.1.0",
|
|
44
|
-
"esbuild": "0.27.
|
|
44
|
+
"esbuild": "0.27.4",
|
|
45
45
|
"eslint": "8.57.0",
|
|
46
46
|
"eslint-cli": "1.1.1",
|
|
47
47
|
"iframe-resizer": "4.3.6",
|
|
48
|
-
"prettier": "3.
|
|
48
|
+
"prettier": "3.8.1",
|
|
49
49
|
"typescript": "5.9.3"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/postal-mime.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export type RawEmail = string | ArrayBuffer | Uint8Array | Blob | Buffer | Reada
|
|
|
3
3
|
export type Header = {
|
|
4
4
|
/** Lowercase header name */
|
|
5
5
|
key: string;
|
|
6
|
+
/** Original header name preserving case */
|
|
7
|
+
originalKey: string;
|
|
6
8
|
/** Header value */
|
|
7
9
|
value: string;
|
|
8
10
|
};
|
|
@@ -36,7 +38,7 @@ export type Attachment = {
|
|
|
36
38
|
description?: string;
|
|
37
39
|
contentId?: string;
|
|
38
40
|
method?: string;
|
|
39
|
-
content: ArrayBuffer | string;
|
|
41
|
+
content: ArrayBuffer | Uint8Array | string;
|
|
40
42
|
encoding?: "base64" | "utf8";
|
|
41
43
|
};
|
|
42
44
|
|
package/src/address-parser.js
CHANGED
|
@@ -142,13 +142,13 @@ function _handleAddress(tokens, depth) {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// If there's still
|
|
145
|
+
// If there's still no text but a comment exists, replace the two
|
|
146
146
|
if (!data.text.length && data.comment.length) {
|
|
147
147
|
data.text = data.comment;
|
|
148
148
|
data.comment = [];
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
// Keep only the first address
|
|
151
|
+
// Keep only the first address occurrence, push others to regular text
|
|
152
152
|
if (data.address.length > 1) {
|
|
153
153
|
data.text = data.text.concat(data.address.splice(1));
|
|
154
154
|
}
|
|
@@ -159,30 +159,34 @@ function _handleAddress(tokens, depth) {
|
|
|
159
159
|
|
|
160
160
|
if (!data.address && /^=\?[^=]+?=$/.test(data.text.trim())) {
|
|
161
161
|
// try to extract words from text content
|
|
162
|
-
const
|
|
163
|
-
if
|
|
164
|
-
|
|
162
|
+
const decodedText = decodeWords(data.text);
|
|
163
|
+
// Security: only re-parse if decoded text contains angle-bracket addresses.
|
|
164
|
+
// Without this, a bare encoded email (e.g. =?utf-8?B?dGVzdEBldmlsLmNv?=)
|
|
165
|
+
// would be fabricated into an address from attacker-controlled input.
|
|
166
|
+
if (/<[^<>]+@[^<>]+>/.test(decodedText)) {
|
|
167
|
+
const parsedSubAddresses = addressParser(decodedText);
|
|
168
|
+
if (parsedSubAddresses && parsedSubAddresses.length) {
|
|
169
|
+
return parsedSubAddresses;
|
|
170
|
+
}
|
|
165
171
|
}
|
|
172
|
+
// No usable address found - treat decoded text as display name only
|
|
173
|
+
return [{ address: '', name: decodedText }];
|
|
166
174
|
}
|
|
167
175
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
address: data.address || data.text || '',
|
|
173
|
-
name: decodeWords(data.text || data.address || '')
|
|
174
|
-
};
|
|
176
|
+
address = {
|
|
177
|
+
address: data.address || data.text || '',
|
|
178
|
+
name: decodeWords(data.text || data.address || '')
|
|
179
|
+
};
|
|
175
180
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
181
|
+
if (address.address === address.name) {
|
|
182
|
+
if ((address.address || '').match(/@/)) {
|
|
183
|
+
address.name = '';
|
|
184
|
+
} else {
|
|
185
|
+
address.address = '';
|
|
182
186
|
}
|
|
183
|
-
|
|
184
|
-
addresses.push(address);
|
|
185
187
|
}
|
|
188
|
+
|
|
189
|
+
addresses.push(address);
|
|
186
190
|
}
|
|
187
191
|
|
|
188
192
|
return addresses;
|
|
@@ -280,7 +284,7 @@ class Tokenizer {
|
|
|
280
284
|
this.operatorExpecting = this.operators[chr];
|
|
281
285
|
this.escaped = false;
|
|
282
286
|
return;
|
|
283
|
-
} else if (
|
|
287
|
+
} else if (this.operatorExpecting === '"' && chr === '\\') {
|
|
284
288
|
this.escaped = true;
|
|
285
289
|
return;
|
|
286
290
|
}
|
package/src/base64-decoder.js
CHANGED
|
@@ -16,9 +16,7 @@ export default class Base64Decoder {
|
|
|
16
16
|
update(buffer) {
|
|
17
17
|
let str = this.decoder.decode(buffer);
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
str = str.replace(/[^a-zA-Z0-9+\/]+/g, '');
|
|
21
|
-
}
|
|
19
|
+
str = str.replace(/[^a-zA-Z0-9+\/]+/g, '');
|
|
22
20
|
|
|
23
21
|
this.remainder += str;
|
|
24
22
|
|
package/src/decode-strings.js
CHANGED
|
@@ -4,7 +4,7 @@ const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456
|
|
|
4
4
|
|
|
5
5
|
// Use a lookup table to find the index.
|
|
6
6
|
const base64Lookup = new Uint8Array(256);
|
|
7
|
-
for (
|
|
7
|
+
for (let i = 0; i < base64Chars.length; i++) {
|
|
8
8
|
base64Lookup[base64Chars.charCodeAt(i)] = i;
|
|
9
9
|
}
|
|
10
10
|
|
package/src/mime-node.js
CHANGED
|
@@ -3,6 +3,8 @@ import PassThroughDecoder from './pass-through-decoder.js';
|
|
|
3
3
|
import Base64Decoder from './base64-decoder.js';
|
|
4
4
|
import QPDecoder from './qp-decoder.js';
|
|
5
5
|
|
|
6
|
+
const defaultDecoder = getDecoder();
|
|
7
|
+
|
|
6
8
|
export default class MimeNode {
|
|
7
9
|
constructor(options) {
|
|
8
10
|
this.options = options || {};
|
|
@@ -233,7 +235,7 @@ export default class MimeNode {
|
|
|
233
235
|
// remove soft linebreaks
|
|
234
236
|
// soft linebreaks are added after space symbols
|
|
235
237
|
.reduce((previousValue, currentValue) => {
|
|
236
|
-
if (
|
|
238
|
+
if (previousValue.endsWith(' ') && previousValue !== '-- ' && !previousValue.endsWith('\n-- ')) {
|
|
237
239
|
if (delSp) {
|
|
238
240
|
// delsp adds space to text to be able to fold it
|
|
239
241
|
// these spaces can be removed once the text is unfolded
|
|
@@ -360,7 +362,7 @@ export default class MimeNode {
|
|
|
360
362
|
throw error;
|
|
361
363
|
}
|
|
362
364
|
|
|
363
|
-
this.headerLines.push(
|
|
365
|
+
this.headerLines.push(defaultDecoder.decode(line));
|
|
364
366
|
break;
|
|
365
367
|
case 'body': {
|
|
366
368
|
// add line to body
|
package/src/postal-mime.js
CHANGED
|
@@ -9,6 +9,10 @@ export { addressParser, decodeWords };
|
|
|
9
9
|
const MAX_NESTING_DEPTH = 256;
|
|
10
10
|
const MAX_HEADERS_SIZE = 2 * 1024 * 1024;
|
|
11
11
|
|
|
12
|
+
function toCamelCase(key) {
|
|
13
|
+
return key.replace(/-(.)/g, (o, c) => c.toUpperCase());
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
export default class PostalMime {
|
|
13
17
|
static parse(buf, options) {
|
|
14
18
|
const parser = new PostalMime(options);
|
|
@@ -76,9 +80,11 @@ export default class PostalMime {
|
|
|
76
80
|
let boundaryEnd = boundary.value.length + 2;
|
|
77
81
|
let isTerminator = false;
|
|
78
82
|
|
|
79
|
-
if (
|
|
83
|
+
if (
|
|
84
|
+
line.length >= boundary.value.length + 4 &&
|
|
80
85
|
line[boundary.value.length + 2] === 0x2d &&
|
|
81
|
-
line[boundary.value.length + 3] === 0x2d
|
|
86
|
+
line[boundary.value.length + 3] === 0x2d
|
|
87
|
+
) {
|
|
82
88
|
isTerminator = true;
|
|
83
89
|
boundaryEnd = boundary.value.length + 4;
|
|
84
90
|
}
|
|
@@ -130,13 +136,6 @@ export default class PostalMime {
|
|
|
130
136
|
let startPos = this.readPos;
|
|
131
137
|
let endPos = this.readPos;
|
|
132
138
|
|
|
133
|
-
let res = () => {
|
|
134
|
-
return {
|
|
135
|
-
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
|
|
136
|
-
done: this.readPos >= this.av.length
|
|
137
|
-
};
|
|
138
|
-
};
|
|
139
|
-
|
|
140
139
|
while (this.readPos < this.av.length) {
|
|
141
140
|
const c = this.av[this.readPos++];
|
|
142
141
|
|
|
@@ -145,11 +144,17 @@ export default class PostalMime {
|
|
|
145
144
|
}
|
|
146
145
|
|
|
147
146
|
if (c === 0x0a) {
|
|
148
|
-
return
|
|
147
|
+
return {
|
|
148
|
+
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
|
|
149
|
+
done: this.readPos >= this.av.length
|
|
150
|
+
};
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
return
|
|
154
|
+
return {
|
|
155
|
+
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
|
|
156
|
+
done: this.readPos >= this.av.length
|
|
157
|
+
};
|
|
153
158
|
}
|
|
154
159
|
|
|
155
160
|
async processNodeTree() {
|
|
@@ -220,7 +225,9 @@ export default class PostalMime {
|
|
|
220
225
|
// is it an attachment
|
|
221
226
|
else if (node.content) {
|
|
222
227
|
const filename =
|
|
223
|
-
node.contentDisposition?.parsed?.params?.filename ||
|
|
228
|
+
node.contentDisposition?.parsed?.params?.filename ||
|
|
229
|
+
node.contentType.parsed.params.name ||
|
|
230
|
+
null;
|
|
224
231
|
const attachment = {
|
|
225
232
|
filename: filename ? decodeWords(filename) : null,
|
|
226
233
|
mimeType: node.contentType.parsed.value,
|
|
@@ -274,7 +281,7 @@ export default class PostalMime {
|
|
|
274
281
|
}
|
|
275
282
|
};
|
|
276
283
|
|
|
277
|
-
await walk(this.root, false,
|
|
284
|
+
await walk(this.root, false, false);
|
|
278
285
|
|
|
279
286
|
textMap.forEach(mapEntry => {
|
|
280
287
|
textTypes.forEach(textType => {
|
|
@@ -471,7 +478,9 @@ export default class PostalMime {
|
|
|
471
478
|
await this.processNodeTree();
|
|
472
479
|
|
|
473
480
|
const message = {
|
|
474
|
-
headers: this.root.headers
|
|
481
|
+
headers: this.root.headers
|
|
482
|
+
.map(entry => ({ key: entry.key, originalKey: entry.originalKey, value: entry.value }))
|
|
483
|
+
.reverse()
|
|
475
484
|
};
|
|
476
485
|
|
|
477
486
|
for (const key of ['from', 'sender']) {
|
|
@@ -489,7 +498,7 @@ export default class PostalMime {
|
|
|
489
498
|
if (addressHeader && addressHeader.value) {
|
|
490
499
|
const addresses = addressParser(addressHeader.value);
|
|
491
500
|
if (addresses && addresses.length && addresses[0].address) {
|
|
492
|
-
const camelKey = key
|
|
501
|
+
const camelKey = toCamelCase(key);
|
|
493
502
|
message[camelKey] = addresses[0].address;
|
|
494
503
|
}
|
|
495
504
|
}
|
|
@@ -505,7 +514,7 @@ export default class PostalMime {
|
|
|
505
514
|
.forEach(parsed => (addresses = addresses.concat(parsed || [])));
|
|
506
515
|
|
|
507
516
|
if (addresses && addresses.length) {
|
|
508
|
-
const camelKey = key
|
|
517
|
+
const camelKey = toCamelCase(key);
|
|
509
518
|
message[camelKey] = addresses;
|
|
510
519
|
}
|
|
511
520
|
}
|
|
@@ -513,7 +522,7 @@ export default class PostalMime {
|
|
|
513
522
|
for (const key of ['subject', 'message-id', 'in-reply-to', 'references']) {
|
|
514
523
|
const header = this.root.headers.find(line => line.key === key);
|
|
515
524
|
if (header && header.value) {
|
|
516
|
-
const camelKey = key
|
|
525
|
+
const camelKey = toCamelCase(key);
|
|
517
526
|
message[camelKey] = decodeWords(header.value);
|
|
518
527
|
}
|
|
519
528
|
}
|
|
@@ -521,7 +530,7 @@ export default class PostalMime {
|
|
|
521
530
|
let dateHeader = this.root.headers.find(line => line.key === 'date');
|
|
522
531
|
if (dateHeader) {
|
|
523
532
|
let date = new Date(dateHeader.value);
|
|
524
|
-
if (
|
|
533
|
+
if (date.toString() === 'Invalid Date') {
|
|
525
534
|
date = dateHeader.value;
|
|
526
535
|
} else {
|
|
527
536
|
// enforce ISO format if seems to be a valid date
|
|
@@ -567,7 +576,7 @@ export default class PostalMime {
|
|
|
567
576
|
break;
|
|
568
577
|
|
|
569
578
|
default:
|
|
570
|
-
throw new Error('
|
|
579
|
+
throw new Error('Unknown attachment encoding');
|
|
571
580
|
}
|
|
572
581
|
|
|
573
582
|
return message;
|
package/src/qp-decoder.js
CHANGED
package/src/text-format.js
CHANGED
|
@@ -20,7 +20,7 @@ export function decodeHTMLEntities(str) {
|
|
|
20
20
|
codePoint = parseInt(entity.substr(1), 10);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
let output = '';
|
|
24
24
|
|
|
25
25
|
if ((codePoint >= 0xd800 && codePoint <= 0xdfff) || codePoint > 0x10ffff) {
|
|
26
26
|
// Invalid range, return a replacement character instead
|