postal-mime 2.7.1 → 2.7.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/CHANGELOG.md +8 -0
- package/dist/mime-node.cjs +47 -1
- package/dist/postal-mime.cjs +27 -14
- package/package.json +1 -1
- package/src/mime-node.js +59 -1
- package/src/postal-mime.js +35 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.7.2](https://github.com/postalsys/postal-mime/compare/v2.7.1...v2.7.2) (2026-01-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* add null checks for contentType.parsed access ([ad8f4c6](https://github.com/postalsys/postal-mime/commit/ad8f4c62e0972fd0244859ee5a5184b2cac26395))
|
|
9
|
+
* improve RFC compliance for MIME parsing ([e004c3a](https://github.com/postalsys/postal-mime/commit/e004c3acb29d72ed7eaf1b0b66351cf8b82b970d))
|
|
10
|
+
|
|
3
11
|
## [2.7.1](https://github.com/postalsys/postal-mime/compare/v2.7.0...v2.7.1) (2025-12-22)
|
|
4
12
|
|
|
5
13
|
|
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
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
|
}
|