postal-mime 2.6.0 → 2.6.1

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.6.1](https://github.com/postalsys/postal-mime/compare/v2.6.0...v2.6.1) (2025-11-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * prevent DoS from deeply nested address groups ([f509eaf](https://github.com/postalsys/postal-mime/commit/f509eafec31bf448482133041c086f7aefa2b3fa))
9
+ * update TypeScript typings to match source code ([df5640a](https://github.com/postalsys/postal-mime/commit/df5640a3166b0a1fe3ca563e1364301bb4e6a025))
10
+
3
11
  ## [2.6.0](https://github.com/postalsys/postal-mime/compare/v2.5.0...v2.6.0) (2025-10-24)
4
12
 
5
13
 
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postal-mime",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "description": "Email parser for browser environments",
5
5
  "main": "./dist/postal-mime.cjs",
6
6
  "module": "./src/postal-mime.js",
@@ -37,14 +37,14 @@
37
37
  "author": "Andris Reinman",
38
38
  "license": "MIT-0",
39
39
  "devDependencies": {
40
- "@types/node": "24.0.4",
40
+ "@types/node": "24.10.1",
41
41
  "cross-blob": "3.0.2",
42
- "cross-env": "7.0.3",
43
- "esbuild": "^0.25.11",
42
+ "cross-env": "10.1.0",
43
+ "esbuild": "0.27.0",
44
44
  "eslint": "8.57.0",
45
45
  "eslint-cli": "1.1.1",
46
46
  "iframe-resizer": "4.3.6",
47
- "prettier": "^3.6.2",
48
- "typescript": "^5.9.3"
47
+ "prettier": "3.6.2",
48
+ "typescript": "5.9.3"
49
49
  }
50
50
  }
package/postal-mime.d.ts CHANGED
@@ -2,6 +2,7 @@ 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
 
@@ -27,7 +28,7 @@ export type Attachment = {
27
28
  description?: string;
28
29
  contentId?: string;
29
30
  method?: string;
30
- content: ArrayBuffer | string;
31
+ content: ArrayBuffer | Uint8Array | string;
31
32
  encoding?: "base64" | "utf8";
32
33
  };
33
34
 
@@ -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
  }