postal-mime 2.7.0 → 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 +15 -0
- package/dist/mime-node.cjs +47 -1
- package/dist/postal-mime.cjs +31 -15
- package/package.json +1 -1
- package/src/mime-node.js +59 -1
- package/src/postal-mime.js +39 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
11
|
+
## [2.7.1](https://github.com/postalsys/postal-mime/compare/v2.7.0...v2.7.1) (2025-12-22)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* Add null checks for contentDisposition.parsed access ([fd54c37](https://github.com/postalsys/postal-mime/commit/fd54c37093cc64737c6bb17986bc9d052d2d5add))
|
|
17
|
+
|
|
3
18
|
## [2.7.0](https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.0) (2025-12-22)
|
|
4
19
|
|
|
5
20
|
|
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
|
}
|
|
@@ -134,6 +147,7 @@ class PostalMime {
|
|
|
134
147
|
let textMap = this.textMap = /* @__PURE__ */ new Map();
|
|
135
148
|
let forceRfc822Attachments = this.forceRfc822Attachments();
|
|
136
149
|
let walk = async (node, alternative, related) => {
|
|
150
|
+
var _a, _b, _c, _d, _e;
|
|
137
151
|
alternative = alternative || false;
|
|
138
152
|
related = related || false;
|
|
139
153
|
if (!node.contentType.multipart) {
|
|
@@ -173,11 +187,11 @@ class PostalMime {
|
|
|
173
187
|
textEntry[textType].push({ type: "text", value: node.getTextContent() });
|
|
174
188
|
textTypes.add(textType);
|
|
175
189
|
} else if (node.content) {
|
|
176
|
-
const filename = node.contentDisposition.parsed.params.filename || node.contentType.parsed.params.name || null;
|
|
190
|
+
const filename = ((_c = (_b = (_a = node.contentDisposition) == null ? void 0 : _a.parsed) == null ? void 0 : _b.params) == null ? void 0 : _c.filename) || node.contentType.parsed.params.name || null;
|
|
177
191
|
const attachment = {
|
|
178
192
|
filename: filename ? (0, import_decode_strings.decodeWords)(filename) : null,
|
|
179
193
|
mimeType: node.contentType.parsed.value,
|
|
180
|
-
disposition: node.contentDisposition.parsed.value || null
|
|
194
|
+
disposition: ((_e = (_d = node.contentDisposition) == null ? void 0 : _d.parsed) == null ? void 0 : _e.value) || null
|
|
181
195
|
};
|
|
182
196
|
if (related && node.contentId) {
|
|
183
197
|
attachment.related = true;
|
|
@@ -285,10 +299,11 @@ class PostalMime {
|
|
|
285
299
|
this.textContent = textContent;
|
|
286
300
|
}
|
|
287
301
|
isInlineTextNode(node) {
|
|
288
|
-
|
|
302
|
+
var _a, _b, _c;
|
|
303
|
+
if (((_b = (_a = node.contentDisposition) == null ? void 0 : _a.parsed) == null ? void 0 : _b.value) === "attachment") {
|
|
289
304
|
return false;
|
|
290
305
|
}
|
|
291
|
-
switch (node.contentType.parsed.value) {
|
|
306
|
+
switch ((_c = node.contentType.parsed) == null ? void 0 : _c.value) {
|
|
292
307
|
case "text/html":
|
|
293
308
|
case "text/plain":
|
|
294
309
|
return true;
|
|
@@ -299,10 +314,11 @@ class PostalMime {
|
|
|
299
314
|
}
|
|
300
315
|
}
|
|
301
316
|
isInlineMessageRfc822(node) {
|
|
302
|
-
|
|
317
|
+
var _a, _b, _c;
|
|
318
|
+
if (((_a = node.contentType.parsed) == null ? void 0 : _a.value) !== "message/rfc822") {
|
|
303
319
|
return false;
|
|
304
320
|
}
|
|
305
|
-
let disposition = node.contentDisposition.parsed.value || (this.options.rfc822Attachments ? "attachment" : "inline");
|
|
321
|
+
let disposition = ((_c = (_b = node.contentDisposition) == null ? void 0 : _b.parsed) == null ? void 0 : _c.value) || (this.options.rfc822Attachments ? "attachment" : "inline");
|
|
306
322
|
return disposition === "inline";
|
|
307
323
|
}
|
|
308
324
|
// Check if this is a specially crafted report email where message/rfc822 content should not be inlined
|
|
@@ -313,7 +329,7 @@ class PostalMime {
|
|
|
313
329
|
let forceRfc822Attachments = false;
|
|
314
330
|
let walk = (node) => {
|
|
315
331
|
if (!node.contentType.multipart) {
|
|
316
|
-
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)) {
|
|
317
333
|
forceRfc822Attachments = true;
|
|
318
334
|
}
|
|
319
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
|
}
|
|
@@ -200,11 +220,11 @@ export default class PostalMime {
|
|
|
200
220
|
// is it an attachment
|
|
201
221
|
else if (node.content) {
|
|
202
222
|
const filename =
|
|
203
|
-
node.contentDisposition
|
|
223
|
+
node.contentDisposition?.parsed?.params?.filename || node.contentType.parsed.params.name || null;
|
|
204
224
|
const attachment = {
|
|
205
225
|
filename: filename ? decodeWords(filename) : null,
|
|
206
226
|
mimeType: node.contentType.parsed.value,
|
|
207
|
-
disposition: node.contentDisposition
|
|
227
|
+
disposition: node.contentDisposition?.parsed?.value || null
|
|
208
228
|
};
|
|
209
229
|
|
|
210
230
|
if (related && node.contentId) {
|
|
@@ -333,12 +353,12 @@ export default class PostalMime {
|
|
|
333
353
|
}
|
|
334
354
|
|
|
335
355
|
isInlineTextNode(node) {
|
|
336
|
-
if (node.contentDisposition
|
|
356
|
+
if (node.contentDisposition?.parsed?.value === 'attachment') {
|
|
337
357
|
// no matter the type, this is an attachment
|
|
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,11 +371,11 @@ 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 =
|
|
358
|
-
node.contentDisposition
|
|
378
|
+
node.contentDisposition?.parsed?.value || (this.options.rfc822Attachments ? 'attachment' : 'inline');
|
|
359
379
|
return disposition === 'inline';
|
|
360
380
|
}
|
|
361
381
|
|
|
@@ -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
|
}
|