postal-mime 2.7.1 → 2.7.3
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 +15 -0
- package/README.md +3 -3
- package/dist/mime-node.cjs +47 -1
- package/dist/postal-mime.cjs +27 -14
- package/package.json +2 -2
- package/postal-mime.d.ts +3 -2
- package/src/mime-node.js +59 -1
- package/src/postal-mime.js +35 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.7.3](https://github.com/postalsys/postal-mime/compare/v2.7.2...v2.7.3) (2026-01-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* correct TypeScript type definitions to match implementation ([b225d7c](https://github.com/postalsys/postal-mime/commit/b225d7cca422cb9bc3ab5301e94c4c0bef9a69e2))
|
|
9
|
+
|
|
10
|
+
## [2.7.2](https://github.com/postalsys/postal-mime/compare/v2.7.1...v2.7.2) (2026-01-08)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* add null checks for contentType.parsed access ([ad8f4c6](https://github.com/postalsys/postal-mime/commit/ad8f4c62e0972fd0244859ee5a5184b2cac26395))
|
|
16
|
+
* improve RFC compliance for MIME parsing ([e004c3a](https://github.com/postalsys/postal-mime/commit/e004c3acb29d72ed7eaf1b0b66351cf8b82b970d))
|
|
17
|
+
|
|
3
18
|
## [2.7.1](https://github.com/postalsys/postal-mime/compare/v2.7.0...v2.7.1) (2025-12-22)
|
|
4
19
|
|
|
5
20
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# postal-mime
|
|
2
2
|
|
|
3
|
-
**postal-mime** is an email parsing library
|
|
3
|
+
**postal-mime** is an email parsing library for Node.js, browsers (including Web Workers), and serverless environments (like Cloudflare Email Workers). It takes in a raw email message (RFC822 format) and outputs a structured object containing headers, recipients, attachments, and more.
|
|
4
4
|
|
|
5
5
|
> [!TIP]
|
|
6
6
|
> PostalMime is developed by the makers of [EmailEngine](https://emailengine.app/?utm_source=github&utm_campaign=imapflow&utm_medium=readme-link)—a self-hosted email gateway that provides a REST API for IMAP and SMTP servers and sends webhooks whenever something changes in registered accounts.
|
|
@@ -243,7 +243,7 @@ if (email.from && isMailbox(email.from)) {
|
|
|
243
243
|
PostalMime.parse(email, options) -> Promise<Email>
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
-
- **email**: An RFC822 formatted email. This can be a `string`, `ArrayBuffer/Uint8Array`, `Blob
|
|
246
|
+
- **email**: An RFC822 formatted email. This can be a `string`, `ArrayBuffer/Uint8Array`, `Blob`, `Buffer` (Node.js), or a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
|
|
247
247
|
- **options**: Optional configuration object:
|
|
248
248
|
- **rfc822Attachments** (boolean, default: `false`): Treat `message/rfc822` attachments without a Content-Disposition as attachments.
|
|
249
249
|
- **forceRfc822Attachments** (boolean, default: `false`): Treat _all_ `message/rfc822` parts as attachments.
|
|
@@ -419,6 +419,6 @@ console.log(decoded); // Hello, エポスカード
|
|
|
419
419
|
|
|
420
420
|
## License
|
|
421
421
|
|
|
422
|
-
© 2021–
|
|
422
|
+
© 2021–2026 Andris Reinman
|
|
423
423
|
|
|
424
424
|
`postal-mime` is licensed under the **MIT No Attribution license**.
|
package/dist/mime-node.cjs
CHANGED
|
@@ -54,8 +54,10 @@ class MimeNode {
|
|
|
54
54
|
this.state = "header";
|
|
55
55
|
this.headerLines = [];
|
|
56
56
|
this.headerSize = 0;
|
|
57
|
+
const parentMultipartType = this.options.parentMultipartType || null;
|
|
58
|
+
const defaultContentType = parentMultipartType === "digest" ? "message/rfc822" : "text/plain";
|
|
57
59
|
this.contentType = {
|
|
58
|
-
value:
|
|
60
|
+
value: defaultContentType,
|
|
59
61
|
default: true
|
|
60
62
|
};
|
|
61
63
|
this.contentTransferEncoding = {
|
|
@@ -100,7 +102,51 @@ class MimeNode {
|
|
|
100
102
|
await childNode.finalize();
|
|
101
103
|
}
|
|
102
104
|
}
|
|
105
|
+
// Strip RFC 822 comments (parenthesized text) from structured header values
|
|
106
|
+
stripComments(str) {
|
|
107
|
+
let result = "";
|
|
108
|
+
let depth = 0;
|
|
109
|
+
let escaped = false;
|
|
110
|
+
let inQuote = false;
|
|
111
|
+
for (let i = 0; i < str.length; i++) {
|
|
112
|
+
const chr = str.charAt(i);
|
|
113
|
+
if (escaped) {
|
|
114
|
+
if (depth === 0) {
|
|
115
|
+
result += chr;
|
|
116
|
+
}
|
|
117
|
+
escaped = false;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (chr === "\\") {
|
|
121
|
+
escaped = true;
|
|
122
|
+
if (depth === 0) {
|
|
123
|
+
result += chr;
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (chr === '"' && depth === 0) {
|
|
128
|
+
inQuote = !inQuote;
|
|
129
|
+
result += chr;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!inQuote) {
|
|
133
|
+
if (chr === "(") {
|
|
134
|
+
depth++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (chr === ")" && depth > 0) {
|
|
138
|
+
depth--;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (depth === 0) {
|
|
143
|
+
result += chr;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
103
148
|
parseStructuredHeader(str) {
|
|
149
|
+
str = this.stripComments(str);
|
|
104
150
|
let response = {
|
|
105
151
|
value: false,
|
|
106
152
|
params: {}
|
package/dist/postal-mime.cjs
CHANGED
|
@@ -69,21 +69,33 @@ class PostalMime {
|
|
|
69
69
|
if (boundaries.length && line.length > 2 && line[0] === 45 && line[1] === 45) {
|
|
70
70
|
for (let i = boundaries.length - 1; i >= 0; i--) {
|
|
71
71
|
let boundary = boundaries[i];
|
|
72
|
-
if (line.length
|
|
72
|
+
if (line.length < boundary.value.length + 2) {
|
|
73
73
|
continue;
|
|
74
74
|
}
|
|
75
|
-
let
|
|
76
|
-
|
|
75
|
+
let boundaryMatches = true;
|
|
76
|
+
for (let j = 0; j < boundary.value.length; j++) {
|
|
77
|
+
if (line[j + 2] !== boundary.value[j]) {
|
|
78
|
+
boundaryMatches = false;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!boundaryMatches) {
|
|
77
83
|
continue;
|
|
78
84
|
}
|
|
79
|
-
let
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
let boundaryEnd = boundary.value.length + 2;
|
|
86
|
+
let isTerminator = false;
|
|
87
|
+
if (line.length >= boundary.value.length + 4 && line[boundary.value.length + 2] === 45 && line[boundary.value.length + 3] === 45) {
|
|
88
|
+
isTerminator = true;
|
|
89
|
+
boundaryEnd = boundary.value.length + 4;
|
|
90
|
+
}
|
|
91
|
+
let hasValidTrailing = true;
|
|
92
|
+
for (let j = boundaryEnd; j < line.length; j++) {
|
|
93
|
+
if (line[j] !== 32 && line[j] !== 9) {
|
|
94
|
+
hasValidTrailing = false;
|
|
83
95
|
break;
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
|
-
if (!
|
|
98
|
+
if (!hasValidTrailing) {
|
|
87
99
|
continue;
|
|
88
100
|
}
|
|
89
101
|
if (isTerminator) {
|
|
@@ -94,6 +106,7 @@ class PostalMime {
|
|
|
94
106
|
this.currentNode = new import_mime_node.default({
|
|
95
107
|
postalMime: this,
|
|
96
108
|
parentNode: boundary.node,
|
|
109
|
+
parentMultipartType: boundary.node.contentType.multipart,
|
|
97
110
|
...this.mimeOptions
|
|
98
111
|
});
|
|
99
112
|
}
|
|
@@ -286,11 +299,11 @@ class PostalMime {
|
|
|
286
299
|
this.textContent = textContent;
|
|
287
300
|
}
|
|
288
301
|
isInlineTextNode(node) {
|
|
289
|
-
var _a, _b;
|
|
302
|
+
var _a, _b, _c;
|
|
290
303
|
if (((_b = (_a = node.contentDisposition) == null ? void 0 : _a.parsed) == null ? void 0 : _b.value) === "attachment") {
|
|
291
304
|
return false;
|
|
292
305
|
}
|
|
293
|
-
switch (node.contentType.parsed.value) {
|
|
306
|
+
switch ((_c = node.contentType.parsed) == null ? void 0 : _c.value) {
|
|
294
307
|
case "text/html":
|
|
295
308
|
case "text/plain":
|
|
296
309
|
return true;
|
|
@@ -301,11 +314,11 @@ class PostalMime {
|
|
|
301
314
|
}
|
|
302
315
|
}
|
|
303
316
|
isInlineMessageRfc822(node) {
|
|
304
|
-
var _a, _b;
|
|
305
|
-
if (node.contentType.parsed.value !== "message/rfc822") {
|
|
317
|
+
var _a, _b, _c;
|
|
318
|
+
if (((_a = node.contentType.parsed) == null ? void 0 : _a.value) !== "message/rfc822") {
|
|
306
319
|
return false;
|
|
307
320
|
}
|
|
308
|
-
let disposition = ((
|
|
321
|
+
let disposition = ((_c = (_b = node.contentDisposition) == null ? void 0 : _b.parsed) == null ? void 0 : _c.value) || (this.options.rfc822Attachments ? "attachment" : "inline");
|
|
309
322
|
return disposition === "inline";
|
|
310
323
|
}
|
|
311
324
|
// Check if this is a specially crafted report email where message/rfc822 content should not be inlined
|
|
@@ -316,7 +329,7 @@ class PostalMime {
|
|
|
316
329
|
let forceRfc822Attachments = false;
|
|
317
330
|
let walk = (node) => {
|
|
318
331
|
if (!node.contentType.multipart) {
|
|
319
|
-
if (["message/delivery-status", "message/feedback-report"].includes(node.contentType.parsed.value)) {
|
|
332
|
+
if (node.contentType.parsed && ["message/delivery-status", "message/feedback-report"].includes(node.contentType.parsed.value)) {
|
|
320
333
|
forceRfc822Attachments = true;
|
|
321
334
|
}
|
|
322
335
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postal-mime",
|
|
3
|
-
"version": "2.7.
|
|
4
|
-
"description": "Email parser for browser environments",
|
|
3
|
+
"version": "2.7.3",
|
|
4
|
+
"description": "Email parser for Node.js and browser environments",
|
|
5
5
|
"main": "./dist/postal-mime.cjs",
|
|
6
6
|
"module": "./src/postal-mime.js",
|
|
7
7
|
"exports": {
|
package/postal-mime.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export type RawEmail = string | ArrayBuffer | Uint8Array | Blob | Buffer | ReadableStream;
|
|
2
2
|
|
|
3
3
|
export type Header = {
|
|
4
|
+
/** Lowercase header name */
|
|
4
5
|
key: string;
|
|
5
|
-
|
|
6
|
+
/** Header value */
|
|
6
7
|
value: string;
|
|
7
8
|
};
|
|
8
9
|
|
|
@@ -35,7 +36,7 @@ export type Attachment = {
|
|
|
35
36
|
description?: string;
|
|
36
37
|
contentId?: string;
|
|
37
38
|
method?: string;
|
|
38
|
-
content: ArrayBuffer |
|
|
39
|
+
content: ArrayBuffer | string;
|
|
39
40
|
encoding?: "base64" | "utf8";
|
|
40
41
|
};
|
|
41
42
|
|
package/src/mime-node.js
CHANGED
|
@@ -30,8 +30,12 @@ export default class MimeNode {
|
|
|
30
30
|
this.headerLines = [];
|
|
31
31
|
this.headerSize = 0;
|
|
32
32
|
|
|
33
|
+
// RFC 2046 Section 5.1.5: multipart/digest defaults to message/rfc822
|
|
34
|
+
const parentMultipartType = this.options.parentMultipartType || null;
|
|
35
|
+
const defaultContentType = parentMultipartType === 'digest' ? 'message/rfc822' : 'text/plain';
|
|
36
|
+
|
|
33
37
|
this.contentType = {
|
|
34
|
-
value:
|
|
38
|
+
value: defaultContentType,
|
|
35
39
|
default: true
|
|
36
40
|
};
|
|
37
41
|
|
|
@@ -90,7 +94,61 @@ export default class MimeNode {
|
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
// Strip RFC 822 comments (parenthesized text) from structured header values
|
|
98
|
+
stripComments(str) {
|
|
99
|
+
let result = '';
|
|
100
|
+
let depth = 0;
|
|
101
|
+
let escaped = false;
|
|
102
|
+
let inQuote = false;
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < str.length; i++) {
|
|
105
|
+
const chr = str.charAt(i);
|
|
106
|
+
|
|
107
|
+
if (escaped) {
|
|
108
|
+
if (depth === 0) {
|
|
109
|
+
result += chr;
|
|
110
|
+
}
|
|
111
|
+
escaped = false;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (chr === '\\') {
|
|
116
|
+
escaped = true;
|
|
117
|
+
if (depth === 0) {
|
|
118
|
+
result += chr;
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (chr === '"' && depth === 0) {
|
|
124
|
+
inQuote = !inQuote;
|
|
125
|
+
result += chr;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!inQuote) {
|
|
130
|
+
if (chr === '(') {
|
|
131
|
+
depth++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (chr === ')' && depth > 0) {
|
|
135
|
+
depth--;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (depth === 0) {
|
|
141
|
+
result += chr;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
93
148
|
parseStructuredHeader(str) {
|
|
149
|
+
// Strip RFC 822 comments before parsing
|
|
150
|
+
str = this.stripComments(str);
|
|
151
|
+
|
|
94
152
|
let response = {
|
|
95
153
|
value: false,
|
|
96
154
|
params: {}
|
package/src/postal-mime.js
CHANGED
|
@@ -55,24 +55,43 @@ export default class PostalMime {
|
|
|
55
55
|
for (let i = boundaries.length - 1; i >= 0; i--) {
|
|
56
56
|
let boundary = boundaries[i];
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
// Line must be at least long enough for "--" + boundary
|
|
59
|
+
if (line.length < boundary.value.length + 2) {
|
|
59
60
|
continue;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// Check if boundary value matches
|
|
64
|
+
let boundaryMatches = true;
|
|
65
|
+
for (let j = 0; j < boundary.value.length; j++) {
|
|
66
|
+
if (line[j + 2] !== boundary.value[j]) {
|
|
67
|
+
boundaryMatches = false;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!boundaryMatches) {
|
|
65
72
|
continue;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
// Check for terminator (-- after boundary) and determine where boundary ends
|
|
76
|
+
let boundaryEnd = boundary.value.length + 2;
|
|
77
|
+
let isTerminator = false;
|
|
78
|
+
|
|
79
|
+
if (line.length >= boundary.value.length + 4 &&
|
|
80
|
+
line[boundary.value.length + 2] === 0x2d &&
|
|
81
|
+
line[boundary.value.length + 3] === 0x2d) {
|
|
82
|
+
isTerminator = true;
|
|
83
|
+
boundaryEnd = boundary.value.length + 4;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// RFC 2046: boundary line may have trailing whitespace (space/tab) before CRLF
|
|
87
|
+
let hasValidTrailing = true;
|
|
88
|
+
for (let j = boundaryEnd; j < line.length; j++) {
|
|
89
|
+
if (line[j] !== 0x20 && line[j] !== 0x09) {
|
|
90
|
+
hasValidTrailing = false;
|
|
72
91
|
break;
|
|
73
92
|
}
|
|
74
93
|
}
|
|
75
|
-
if (!
|
|
94
|
+
if (!hasValidTrailing) {
|
|
76
95
|
continue;
|
|
77
96
|
}
|
|
78
97
|
|
|
@@ -87,6 +106,7 @@ export default class PostalMime {
|
|
|
87
106
|
this.currentNode = new MimeNode({
|
|
88
107
|
postalMime: this,
|
|
89
108
|
parentNode: boundary.node,
|
|
109
|
+
parentMultipartType: boundary.node.contentType.multipart,
|
|
90
110
|
...this.mimeOptions
|
|
91
111
|
});
|
|
92
112
|
}
|
|
@@ -338,7 +358,7 @@ export default class PostalMime {
|
|
|
338
358
|
return false;
|
|
339
359
|
}
|
|
340
360
|
|
|
341
|
-
switch (node.contentType.parsed
|
|
361
|
+
switch (node.contentType.parsed?.value) {
|
|
342
362
|
case 'text/html':
|
|
343
363
|
case 'text/plain':
|
|
344
364
|
return true;
|
|
@@ -351,7 +371,7 @@ export default class PostalMime {
|
|
|
351
371
|
}
|
|
352
372
|
|
|
353
373
|
isInlineMessageRfc822(node) {
|
|
354
|
-
if (node.contentType.parsed
|
|
374
|
+
if (node.contentType.parsed?.value !== 'message/rfc822') {
|
|
355
375
|
return false;
|
|
356
376
|
}
|
|
357
377
|
let disposition =
|
|
@@ -368,7 +388,10 @@ export default class PostalMime {
|
|
|
368
388
|
let forceRfc822Attachments = false;
|
|
369
389
|
let walk = node => {
|
|
370
390
|
if (!node.contentType.multipart) {
|
|
371
|
-
if (
|
|
391
|
+
if (
|
|
392
|
+
node.contentType.parsed &&
|
|
393
|
+
['message/delivery-status', 'message/feedback-report'].includes(node.contentType.parsed.value)
|
|
394
|
+
) {
|
|
372
395
|
forceRfc822Attachments = true;
|
|
373
396
|
}
|
|
374
397
|
}
|