postal-mime 2.6.0 → 2.7.0
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 +1 -1
- package/dist/address-parser.cjs +8 -3
- package/dist/mime-node.cjs +34 -25
- package/dist/postal-mime.cjs +1 -0
- package/package.json +7 -6
- package/postal-mime.d.ts +10 -1
- package/src/address-parser.js +19 -3
- package/src/mime-node.js +48 -27
- package/src/postal-mime.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.7.0](https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.0) (2025-12-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add headerLines property exposing raw header lines ([c79a02a](https://github.com/postalsys/postal-mime/commit/c79a02ab05d9cac44e05e95a433752ff292aa5eb))
|
|
9
|
+
|
|
10
|
+
## [2.6.1](https://github.com/postalsys/postal-mime/compare/v2.6.0...v2.6.1) (2025-11-26)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* prevent DoS from deeply nested address groups ([f509eaf](https://github.com/postalsys/postal-mime/commit/f509eafec31bf448482133041c086f7aefa2b3fa))
|
|
16
|
+
* update TypeScript typings to match source code ([df5640a](https://github.com/postalsys/postal-mime/commit/df5640a3166b0a1fe3ca563e1364301bb4e6a025))
|
|
17
|
+
|
|
3
18
|
## [2.6.0](https://github.com/postalsys/postal-mime/compare/v2.5.0...v2.6.0) (2025-10-24)
|
|
4
19
|
|
|
5
20
|
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ The source code is available on [GitHub](https://github.com/postalsys/postal-mim
|
|
|
39
39
|
|
|
40
40
|
## Demo
|
|
41
41
|
|
|
42
|
-
Try out a live demo using the [example page](https://
|
|
42
|
+
Try out a live demo using the [example page](https://postal-mime.postalsys.com/demo).
|
|
43
43
|
|
|
44
44
|
## Installation
|
|
45
45
|
|
package/dist/address-parser.cjs
CHANGED
|
@@ -22,7 +22,7 @@ __export(address_parser_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(address_parser_exports);
|
|
24
24
|
var import_decode_strings = require("./decode-strings.cjs");
|
|
25
|
-
function _handleAddress(tokens) {
|
|
25
|
+
function _handleAddress(tokens, depth) {
|
|
26
26
|
let isGroup = false;
|
|
27
27
|
let state = "text";
|
|
28
28
|
let address;
|
|
@@ -90,7 +90,7 @@ function _handleAddress(tokens) {
|
|
|
90
90
|
data.text = data.text.join(" ");
|
|
91
91
|
let groupMembers = [];
|
|
92
92
|
if (data.group.length) {
|
|
93
|
-
let parsedGroup = addressParser(data.group.join(","));
|
|
93
|
+
let parsedGroup = addressParser(data.group.join(","), { _depth: depth + 1 });
|
|
94
94
|
parsedGroup.forEach((member) => {
|
|
95
95
|
if (member.group) {
|
|
96
96
|
groupMembers = groupMembers.concat(member.group);
|
|
@@ -258,8 +258,13 @@ class Tokenizer {
|
|
|
258
258
|
this.escaped = false;
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
|
+
const MAX_NESTED_GROUP_DEPTH = 50;
|
|
261
262
|
function addressParser(str, options) {
|
|
262
263
|
options = options || {};
|
|
264
|
+
let depth = options._depth || 0;
|
|
265
|
+
if (depth > MAX_NESTED_GROUP_DEPTH) {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
263
268
|
let tokenizer = new Tokenizer(str);
|
|
264
269
|
let tokens = tokenizer.tokenize();
|
|
265
270
|
let addresses = [];
|
|
@@ -279,7 +284,7 @@ function addressParser(str, options) {
|
|
|
279
284
|
addresses.push(address);
|
|
280
285
|
}
|
|
281
286
|
addresses.forEach((address2) => {
|
|
282
|
-
address2 = _handleAddress(address2);
|
|
287
|
+
address2 = _handleAddress(address2, depth);
|
|
283
288
|
if (address2.length) {
|
|
284
289
|
parsedAddresses = parsedAddresses.concat(address2);
|
|
285
290
|
}
|
package/dist/mime-node.cjs
CHANGED
|
@@ -193,31 +193,40 @@ class MimeNode {
|
|
|
193
193
|
if (i && /^\s/.test(line)) {
|
|
194
194
|
this.headerLines[i - 1] += "\n" + line;
|
|
195
195
|
this.headerLines.splice(i, 1);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
this.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
this.rawHeaderLines = [];
|
|
199
|
+
for (let i = this.headerLines.length - 1; i >= 0; i--) {
|
|
200
|
+
let rawLine = this.headerLines[i];
|
|
201
|
+
let sep = rawLine.indexOf(":");
|
|
202
|
+
let rawKey = sep < 0 ? rawLine.trim() : rawLine.substr(0, sep).trim();
|
|
203
|
+
this.rawHeaderLines.push({
|
|
204
|
+
key: rawKey.toLowerCase(),
|
|
205
|
+
line: rawLine
|
|
206
|
+
});
|
|
207
|
+
let normalizedLine = rawLine.replace(/\s+/g, " ");
|
|
208
|
+
sep = normalizedLine.indexOf(":");
|
|
209
|
+
let key = sep < 0 ? normalizedLine.trim() : normalizedLine.substr(0, sep).trim();
|
|
210
|
+
let value = sep < 0 ? "" : normalizedLine.substr(sep + 1).trim();
|
|
211
|
+
this.headers.push({ key: key.toLowerCase(), originalKey: key, value });
|
|
212
|
+
switch (key.toLowerCase()) {
|
|
213
|
+
case "content-type":
|
|
214
|
+
if (this.contentType.default) {
|
|
215
|
+
this.contentType = { value, parsed: {} };
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
case "content-transfer-encoding":
|
|
219
|
+
this.contentTransferEncoding = { value, parsed: {} };
|
|
220
|
+
break;
|
|
221
|
+
case "content-disposition":
|
|
222
|
+
this.contentDisposition = { value, parsed: {} };
|
|
223
|
+
break;
|
|
224
|
+
case "content-id":
|
|
225
|
+
this.contentId = value;
|
|
226
|
+
break;
|
|
227
|
+
case "content-description":
|
|
228
|
+
this.contentDescription = value;
|
|
229
|
+
break;
|
|
221
230
|
}
|
|
222
231
|
}
|
|
223
232
|
this.contentType.parsed = this.parseStructuredHeader(this.contentType.value);
|
package/dist/postal-mime.cjs
CHANGED
|
@@ -426,6 +426,7 @@ class PostalMime {
|
|
|
426
426
|
message.text = this.textContent.plain;
|
|
427
427
|
}
|
|
428
428
|
message.attachments = this.attachments;
|
|
429
|
+
message.headerLines = (this.root.rawHeaderLines || []).slice().reverse();
|
|
429
430
|
switch (this.attachmentEncoding) {
|
|
430
431
|
case "arraybuffer":
|
|
431
432
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postal-mime",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Email parser for browser environments",
|
|
5
5
|
"main": "./dist/postal-mime.cjs",
|
|
6
6
|
"module": "./src/postal-mime.js",
|
|
@@ -34,17 +34,18 @@
|
|
|
34
34
|
"bugs": {
|
|
35
35
|
"url": "https://github.com/postalsys/postal-mime/issues"
|
|
36
36
|
},
|
|
37
|
+
"homepage": "https://postal-mime.postalsys.com",
|
|
37
38
|
"author": "Andris Reinman",
|
|
38
39
|
"license": "MIT-0",
|
|
39
40
|
"devDependencies": {
|
|
40
|
-
"@types/node": "24.
|
|
41
|
+
"@types/node": "24.10.1",
|
|
41
42
|
"cross-blob": "3.0.2",
|
|
42
|
-
"cross-env": "
|
|
43
|
-
"esbuild": "
|
|
43
|
+
"cross-env": "10.1.0",
|
|
44
|
+
"esbuild": "0.27.0",
|
|
44
45
|
"eslint": "8.57.0",
|
|
45
46
|
"eslint-cli": "1.1.1",
|
|
46
47
|
"iframe-resizer": "4.3.6",
|
|
47
|
-
"prettier": "
|
|
48
|
-
"typescript": "
|
|
48
|
+
"prettier": "3.6.2",
|
|
49
|
+
"typescript": "5.9.3"
|
|
49
50
|
}
|
|
50
51
|
}
|
package/postal-mime.d.ts
CHANGED
|
@@ -2,9 +2,17 @@ export type RawEmail = string | ArrayBuffer | Uint8Array | Blob | Buffer | Reada
|
|
|
2
2
|
|
|
3
3
|
export type Header = {
|
|
4
4
|
key: string;
|
|
5
|
+
originalKey: string;
|
|
5
6
|
value: string;
|
|
6
7
|
};
|
|
7
8
|
|
|
9
|
+
export type HeaderLine = {
|
|
10
|
+
/** Lowercase header name */
|
|
11
|
+
key: string;
|
|
12
|
+
/** Complete raw header line including key and value (with folded lines merged) */
|
|
13
|
+
line: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
8
16
|
export type Mailbox = {
|
|
9
17
|
name: string;
|
|
10
18
|
address: string;
|
|
@@ -27,12 +35,13 @@ export type Attachment = {
|
|
|
27
35
|
description?: string;
|
|
28
36
|
contentId?: string;
|
|
29
37
|
method?: string;
|
|
30
|
-
content: ArrayBuffer | string;
|
|
38
|
+
content: ArrayBuffer | Uint8Array | string;
|
|
31
39
|
encoding?: "base64" | "utf8";
|
|
32
40
|
};
|
|
33
41
|
|
|
34
42
|
export type Email = {
|
|
35
43
|
headers: Header[];
|
|
44
|
+
headerLines: HeaderLine[];
|
|
36
45
|
from?: Address;
|
|
37
46
|
sender?: Address;
|
|
38
47
|
replyTo?: Address[];
|
package/src/address-parser.js
CHANGED
|
@@ -4,9 +4,10 @@ import { decodeWords } from './decode-strings.js';
|
|
|
4
4
|
* Converts tokens for a single address into an address object
|
|
5
5
|
*
|
|
6
6
|
* @param {Array} tokens Tokens object
|
|
7
|
+
* @param {Number} depth Current recursion depth for nested group protection
|
|
7
8
|
* @return {Object} Address object
|
|
8
9
|
*/
|
|
9
|
-
function _handleAddress(tokens) {
|
|
10
|
+
function _handleAddress(tokens, depth) {
|
|
10
11
|
let isGroup = false;
|
|
11
12
|
let state = 'text';
|
|
12
13
|
let address;
|
|
@@ -87,7 +88,7 @@ function _handleAddress(tokens) {
|
|
|
87
88
|
// Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting)
|
|
88
89
|
let groupMembers = [];
|
|
89
90
|
if (data.group.length) {
|
|
90
|
-
let parsedGroup = addressParser(data.group.join(','));
|
|
91
|
+
let parsedGroup = addressParser(data.group.join(','), { _depth: depth + 1 });
|
|
91
92
|
// Flatten: if any member is itself a group, extract its members into the sequence
|
|
92
93
|
parsedGroup.forEach(member => {
|
|
93
94
|
if (member.group) {
|
|
@@ -307,6 +308,13 @@ class Tokenizer {
|
|
|
307
308
|
}
|
|
308
309
|
}
|
|
309
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Maximum recursion depth for parsing nested groups.
|
|
313
|
+
* RFC 5322 doesn't allow nested groups, so this is a safeguard against
|
|
314
|
+
* malicious input that could cause stack overflow.
|
|
315
|
+
*/
|
|
316
|
+
const MAX_NESTED_GROUP_DEPTH = 50;
|
|
317
|
+
|
|
310
318
|
/**
|
|
311
319
|
* Parses structured e-mail addresses from an address field
|
|
312
320
|
*
|
|
@@ -319,10 +327,18 @@ class Tokenizer {
|
|
|
319
327
|
* [{name: 'Name', address: 'address@domain'}]
|
|
320
328
|
*
|
|
321
329
|
* @param {String} str Address field
|
|
330
|
+
* @param {Object} options Optional options object
|
|
331
|
+
* @param {Number} options._depth Internal recursion depth counter (do not set manually)
|
|
322
332
|
* @return {Array} An array of address objects
|
|
323
333
|
*/
|
|
324
334
|
function addressParser(str, options) {
|
|
325
335
|
options = options || {};
|
|
336
|
+
let depth = options._depth || 0;
|
|
337
|
+
|
|
338
|
+
// Prevent stack overflow from deeply nested groups (DoS protection)
|
|
339
|
+
if (depth > MAX_NESTED_GROUP_DEPTH) {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
326
342
|
|
|
327
343
|
let tokenizer = new Tokenizer(str);
|
|
328
344
|
let tokens = tokenizer.tokenize();
|
|
@@ -347,7 +363,7 @@ function addressParser(str, options) {
|
|
|
347
363
|
}
|
|
348
364
|
|
|
349
365
|
addresses.forEach(address => {
|
|
350
|
-
address = _handleAddress(address);
|
|
366
|
+
address = _handleAddress(address, depth);
|
|
351
367
|
if (address.length) {
|
|
352
368
|
parsedAddresses = parsedAddresses.concat(address);
|
|
353
369
|
}
|
package/src/mime-node.js
CHANGED
|
@@ -208,38 +208,59 @@ export default class MimeNode {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
processHeaders() {
|
|
211
|
+
// First pass: merge folded headers (backward iteration)
|
|
211
212
|
for (let i = this.headerLines.length - 1; i >= 0; i--) {
|
|
212
213
|
let line = this.headerLines[i];
|
|
213
214
|
if (i && /^\s/.test(line)) {
|
|
214
215
|
this.headerLines[i - 1] += '\n' + line;
|
|
215
216
|
this.headerLines.splice(i, 1);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Initialize rawHeaderLines to store unmodified lines
|
|
221
|
+
this.rawHeaderLines = [];
|
|
222
|
+
|
|
223
|
+
// Second pass: process headers (MUST be backward to maintain this.headers order)
|
|
224
|
+
// The existing code iterates backward and postal-mime.js calls .reverse()
|
|
225
|
+
// We must preserve this behavior to avoid breaking changes
|
|
226
|
+
for (let i = this.headerLines.length - 1; i >= 0; i--) {
|
|
227
|
+
let rawLine = this.headerLines[i];
|
|
228
|
+
|
|
229
|
+
// Extract key from raw line for rawHeaderLines
|
|
230
|
+
let sep = rawLine.indexOf(':');
|
|
231
|
+
let rawKey = sep < 0 ? rawLine.trim() : rawLine.substr(0, sep).trim();
|
|
232
|
+
|
|
233
|
+
// Store raw line with lowercase key
|
|
234
|
+
this.rawHeaderLines.push({
|
|
235
|
+
key: rawKey.toLowerCase(),
|
|
236
|
+
line: rawLine
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Normalize for this.headers (existing behavior - order preserved)
|
|
240
|
+
let normalizedLine = rawLine.replace(/\s+/g, ' ');
|
|
241
|
+
sep = normalizedLine.indexOf(':');
|
|
242
|
+
let key = sep < 0 ? normalizedLine.trim() : normalizedLine.substr(0, sep).trim();
|
|
243
|
+
let value = sep < 0 ? '' : normalizedLine.substr(sep + 1).trim();
|
|
244
|
+
this.headers.push({ key: key.toLowerCase(), originalKey: key, value });
|
|
245
|
+
|
|
246
|
+
switch (key.toLowerCase()) {
|
|
247
|
+
case 'content-type':
|
|
248
|
+
if (this.contentType.default) {
|
|
249
|
+
this.contentType = { value, parsed: {} };
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
case 'content-transfer-encoding':
|
|
253
|
+
this.contentTransferEncoding = { value, parsed: {} };
|
|
254
|
+
break;
|
|
255
|
+
case 'content-disposition':
|
|
256
|
+
this.contentDisposition = { value, parsed: {} };
|
|
257
|
+
break;
|
|
258
|
+
case 'content-id':
|
|
259
|
+
this.contentId = value;
|
|
260
|
+
break;
|
|
261
|
+
case 'content-description':
|
|
262
|
+
this.contentDescription = value;
|
|
263
|
+
break;
|
|
243
264
|
}
|
|
244
265
|
}
|
|
245
266
|
|
package/src/postal-mime.js
CHANGED
|
@@ -517,6 +517,9 @@ export default class PostalMime {
|
|
|
517
517
|
|
|
518
518
|
message.attachments = this.attachments;
|
|
519
519
|
|
|
520
|
+
// Expose raw header lines (reversed to match headers array order)
|
|
521
|
+
message.headerLines = (this.root.rawHeaderLines || []).slice().reverse();
|
|
522
|
+
|
|
520
523
|
switch (this.attachmentEncoding) {
|
|
521
524
|
case 'arraybuffer':
|
|
522
525
|
break;
|