postal-mime 2.7.3 → 2.7.4

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.7.4](https://github.com/postalsys/postal-mime/compare/v2.7.3...v2.7.4) (2026-03-17)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * add missing originalKey to Header type and Uint8Array to Attachment content ([92cc91c](https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0))
9
+ * include originalKey in parsed headers output ([83521c8](https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347))
10
+ * preserve __esModule and .default in CJS build for bundler interop ([1466910](https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048))
11
+ * prevent RFC 2047 encoded-word address fabrication ([844f920](https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3))
12
+
3
13
  ## [2.7.3](https://github.com/postalsys/postal-mime/compare/v2.7.2...v2.7.3) (2026-01-09)
4
14
 
5
15
 
package/README.md CHANGED
@@ -14,6 +14,9 @@
14
14
  - **Handles complex MIME structures** - Multipart messages, nested parts, attachments
15
15
  - **Security limits** - Built-in protection against deeply nested messages and oversized headers
16
16
 
17
+ > [!NOTE]
18
+ > Full documentation is available at [postal-mime.postalsys.com](https://postal-mime.postalsys.com/).
19
+
17
20
  ## Table of Contents
18
21
 
19
22
  - [Source](#source)
@@ -141,27 +141,27 @@ function _handleAddress(tokens, depth) {
141
141
  data.text = data.text.join(" ");
142
142
  data.address = data.address.join(" ");
143
143
  if (!data.address && /^=\?[^=]+?=$/.test(data.text.trim())) {
144
- const parsedSubAddresses = addressParser((0, import_decode_strings.decodeWords)(data.text));
145
- if (parsedSubAddresses && parsedSubAddresses.length) {
146
- return parsedSubAddresses;
144
+ const decodedText = (0, import_decode_strings.decodeWords)(data.text);
145
+ if (/<[^<>]+@[^<>]+>/.test(decodedText)) {
146
+ const parsedSubAddresses = addressParser(decodedText);
147
+ if (parsedSubAddresses && parsedSubAddresses.length) {
148
+ return parsedSubAddresses;
149
+ }
147
150
  }
151
+ return [{ address: "", name: decodedText }];
148
152
  }
149
- if (!data.address && isGroup) {
150
- return [];
151
- } else {
152
- address = {
153
- address: data.address || data.text || "",
154
- name: (0, import_decode_strings.decodeWords)(data.text || data.address || "")
155
- };
156
- if (address.address === address.name) {
157
- if ((address.address || "").match(/@/)) {
158
- address.name = "";
159
- } else {
160
- address.address = "";
161
- }
153
+ address = {
154
+ address: data.address || data.text || "",
155
+ name: (0, import_decode_strings.decodeWords)(data.text || data.address || "")
156
+ };
157
+ if (address.address === address.name) {
158
+ if ((address.address || "").match(/@/)) {
159
+ address.name = "";
160
+ } else {
161
+ address.address = "";
162
162
  }
163
- addresses.push(address);
164
163
  }
164
+ addresses.push(address);
165
165
  }
166
166
  return addresses;
167
167
  }
@@ -238,7 +238,7 @@ class Tokenizer {
238
238
  this.operatorExpecting = this.operators[chr];
239
239
  this.escaped = false;
240
240
  return;
241
- } else if (['"', "'"].includes(this.operatorExpecting) && chr === "\\") {
241
+ } else if (this.operatorExpecting === '"' && chr === "\\") {
242
242
  this.escaped = true;
243
243
  return;
244
244
  }
@@ -312,11 +312,14 @@ if (module.exports.default) {
312
312
  var defaultExport = module.exports.default;
313
313
  var namedExports = {};
314
314
  for (var key in module.exports) {
315
- if (key !== 'default') {
315
+ if (key !== 'default' && key !== '__esModule') {
316
316
  namedExports[key] = module.exports[key];
317
317
  }
318
318
  }
319
319
  module.exports = defaultExport;
320
320
  Object.assign(module.exports, namedExports);
321
+ // Preserve __esModule and .default for bundler/transpiler interop
322
+ Object.defineProperty(module.exports, '__esModule', { value: true });
323
+ module.exports.default = defaultExport;
321
324
  }
322
325
 
@@ -32,9 +32,7 @@ class Base64Decoder {
32
32
  }
33
33
  update(buffer) {
34
34
  let str = this.decoder.decode(buffer);
35
- if (/[^a-zA-Z0-9+\/]/.test(str)) {
36
- str = str.replace(/[^a-zA-Z0-9+\/]+/g, "");
37
- }
35
+ str = str.replace(/[^a-zA-Z0-9+\/]+/g, "");
38
36
  this.remainder += str;
39
37
  if (this.remainder.length >= this.maxChunkSize) {
40
38
  let allowedBytes = Math.floor(this.remainder.length / 4) * 4;
@@ -64,11 +62,14 @@ if (module.exports.default) {
64
62
  var defaultExport = module.exports.default;
65
63
  var namedExports = {};
66
64
  for (var key in module.exports) {
67
- if (key !== 'default') {
65
+ if (key !== 'default' && key !== '__esModule') {
68
66
  namedExports[key] = module.exports[key];
69
67
  }
70
68
  }
71
69
  module.exports = defaultExport;
72
70
  Object.assign(module.exports, namedExports);
71
+ // Preserve __esModule and .default for bundler/transpiler interop
72
+ Object.defineProperty(module.exports, '__esModule', { value: true });
73
+ module.exports.default = defaultExport;
73
74
  }
74
75
 
@@ -62,11 +62,14 @@ if (module.exports.default) {
62
62
  var defaultExport = module.exports.default;
63
63
  var namedExports = {};
64
64
  for (var key in module.exports) {
65
- if (key !== 'default') {
65
+ if (key !== 'default' && key !== '__esModule') {
66
66
  namedExports[key] = module.exports[key];
67
67
  }
68
68
  }
69
69
  module.exports = defaultExport;
70
70
  Object.assign(module.exports, namedExports);
71
+ // Preserve __esModule and .default for bundler/transpiler interop
72
+ Object.defineProperty(module.exports, '__esModule', { value: true });
73
+ module.exports.default = defaultExport;
71
74
  }
72
75
 
@@ -32,7 +32,7 @@ module.exports = __toCommonJS(decode_strings_exports);
32
32
  const textEncoder = new TextEncoder();
33
33
  const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
34
34
  const base64Lookup = new Uint8Array(256);
35
- for (var i = 0; i < base64Chars.length; i++) {
35
+ for (let i = 0; i < base64Chars.length; i++) {
36
36
  base64Lookup[base64Chars.charCodeAt(i)] = i;
37
37
  }
38
38
  function decodeBase64(base64) {
@@ -244,11 +244,14 @@ if (module.exports.default) {
244
244
  var defaultExport = module.exports.default;
245
245
  var namedExports = {};
246
246
  for (var key in module.exports) {
247
- if (key !== 'default') {
247
+ if (key !== 'default' && key !== '__esModule') {
248
248
  namedExports[key] = module.exports[key];
249
249
  }
250
250
  }
251
251
  module.exports = defaultExport;
252
252
  Object.assign(module.exports, namedExports);
253
+ // Preserve __esModule and .default for bundler/transpiler interop
254
+ Object.defineProperty(module.exports, '__esModule', { value: true });
255
+ module.exports.default = defaultExport;
253
256
  }
254
257
 
@@ -2266,11 +2266,14 @@ if (module.exports.default) {
2266
2266
  var defaultExport = module.exports.default;
2267
2267
  var namedExports = {};
2268
2268
  for (var key in module.exports) {
2269
- if (key !== 'default') {
2269
+ if (key !== 'default' && key !== '__esModule') {
2270
2270
  namedExports[key] = module.exports[key];
2271
2271
  }
2272
2272
  }
2273
2273
  module.exports = defaultExport;
2274
2274
  Object.assign(module.exports, namedExports);
2275
+ // Preserve __esModule and .default for bundler/transpiler interop
2276
+ Object.defineProperty(module.exports, '__esModule', { value: true });
2277
+ module.exports.default = defaultExport;
2275
2278
  }
2276
2279
 
@@ -35,6 +35,7 @@ var import_decode_strings = require("./decode-strings.cjs");
35
35
  var import_pass_through_decoder = __toESM(require("./pass-through-decoder.cjs"), 1);
36
36
  var import_base64_decoder = __toESM(require("./base64-decoder.cjs"), 1);
37
37
  var import_qp_decoder = __toESM(require("./qp-decoder.cjs"), 1);
38
+ const defaultDecoder = (0, import_decode_strings.getDecoder)();
38
39
  class MimeNode {
39
40
  constructor(options) {
40
41
  this.options = options || {};
@@ -212,7 +213,7 @@ class MimeNode {
212
213
  }
213
214
  decodeFlowedText(str, delSp) {
214
215
  return str.split(/\r?\n/).reduce((previousValue, currentValue) => {
215
- if (/ $/.test(previousValue) && !/(^|\n)-- $/.test(previousValue)) {
216
+ if (previousValue.endsWith(" ") && previousValue !== "-- " && !previousValue.endsWith("\n-- ")) {
216
217
  if (delSp) {
217
218
  return previousValue.slice(0, -1) + currentValue;
218
219
  } else {
@@ -299,7 +300,7 @@ class MimeNode {
299
300
  let error = new Error(`Maximum header size of ${this.options.maxHeadersSize} bytes exceeded`);
300
301
  throw error;
301
302
  }
302
- this.headerLines.push((0, import_decode_strings.getDecoder)().decode(line));
303
+ this.headerLines.push(defaultDecoder.decode(line));
303
304
  break;
304
305
  case "body": {
305
306
  this.contentDecoder.update(line);
@@ -313,11 +314,14 @@ if (module.exports.default) {
313
314
  var defaultExport = module.exports.default;
314
315
  var namedExports = {};
315
316
  for (var key in module.exports) {
316
- if (key !== 'default') {
317
+ if (key !== 'default' && key !== '__esModule') {
317
318
  namedExports[key] = module.exports[key];
318
319
  }
319
320
  }
320
321
  module.exports = defaultExport;
321
322
  Object.assign(module.exports, namedExports);
323
+ // Preserve __esModule and .default for bundler/transpiler interop
324
+ Object.defineProperty(module.exports, '__esModule', { value: true });
325
+ module.exports.default = defaultExport;
322
326
  }
323
327
 
@@ -40,11 +40,14 @@ if (module.exports.default) {
40
40
  var defaultExport = module.exports.default;
41
41
  var namedExports = {};
42
42
  for (var key in module.exports) {
43
- if (key !== 'default') {
43
+ if (key !== 'default' && key !== '__esModule') {
44
44
  namedExports[key] = module.exports[key];
45
45
  }
46
46
  }
47
47
  module.exports = defaultExport;
48
48
  Object.assign(module.exports, namedExports);
49
+ // Preserve __esModule and .default for bundler/transpiler interop
50
+ Object.defineProperty(module.exports, '__esModule', { value: true });
51
+ module.exports.default = defaultExport;
49
52
  }
50
53
 
@@ -40,6 +40,9 @@ var import_decode_strings = require("./decode-strings.cjs");
40
40
  var import_base64_encoder = require("./base64-encoder.cjs");
41
41
  const MAX_NESTING_DEPTH = 256;
42
42
  const MAX_HEADERS_SIZE = 2 * 1024 * 1024;
43
+ function toCamelCase(key) {
44
+ return key.replace(/-(.)/g, (o, c) => c.toUpperCase());
45
+ }
43
46
  class PostalMime {
44
47
  static parse(buf, options) {
45
48
  const parser = new PostalMime(options);
@@ -124,22 +127,22 @@ class PostalMime {
124
127
  readLine() {
125
128
  let startPos = this.readPos;
126
129
  let endPos = this.readPos;
127
- let res = () => {
128
- return {
129
- bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
130
- done: this.readPos >= this.av.length
131
- };
132
- };
133
130
  while (this.readPos < this.av.length) {
134
131
  const c = this.av[this.readPos++];
135
132
  if (c !== 13 && c !== 10) {
136
133
  endPos = this.readPos;
137
134
  }
138
135
  if (c === 10) {
139
- return res();
136
+ return {
137
+ bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
138
+ done: this.readPos >= this.av.length
139
+ };
140
140
  }
141
141
  }
142
- return res();
142
+ return {
143
+ bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
144
+ done: this.readPos >= this.av.length
145
+ };
143
146
  }
144
147
  async processNodeTree() {
145
148
  let textContent = {};
@@ -228,7 +231,7 @@ class PostalMime {
228
231
  await walk(childNode, alternative, related);
229
232
  }
230
233
  };
231
- await walk(this.root, false, []);
234
+ await walk(this.root, false, false);
232
235
  textMap.forEach((mapEntry) => {
233
236
  textTypes.forEach((textType) => {
234
237
  if (!textContent[textType]) {
@@ -388,7 +391,7 @@ class PostalMime {
388
391
  }
389
392
  await this.processNodeTree();
390
393
  const message = {
391
- headers: this.root.headers.map((entry) => ({ key: entry.key, value: entry.value })).reverse()
394
+ headers: this.root.headers.map((entry) => ({ key: entry.key, originalKey: entry.originalKey, value: entry.value })).reverse()
392
395
  };
393
396
  for (const key of ["from", "sender"]) {
394
397
  const addressHeader = this.root.headers.find((line) => line.key === key);
@@ -404,7 +407,7 @@ class PostalMime {
404
407
  if (addressHeader && addressHeader.value) {
405
408
  const addresses = (0, import_address_parser.default)(addressHeader.value);
406
409
  if (addresses && addresses.length && addresses[0].address) {
407
- const camelKey = key.replace(/\-(.)/g, (o, c) => c.toUpperCase());
410
+ const camelKey = toCamelCase(key);
408
411
  message[camelKey] = addresses[0].address;
409
412
  }
410
413
  }
@@ -414,21 +417,21 @@ class PostalMime {
414
417
  let addresses = [];
415
418
  addressHeaders.filter((entry) => entry && entry.value).map((entry) => (0, import_address_parser.default)(entry.value)).forEach((parsed) => addresses = addresses.concat(parsed || []));
416
419
  if (addresses && addresses.length) {
417
- const camelKey = key.replace(/\-(.)/g, (o, c) => c.toUpperCase());
420
+ const camelKey = toCamelCase(key);
418
421
  message[camelKey] = addresses;
419
422
  }
420
423
  }
421
424
  for (const key of ["subject", "message-id", "in-reply-to", "references"]) {
422
425
  const header = this.root.headers.find((line) => line.key === key);
423
426
  if (header && header.value) {
424
- const camelKey = key.replace(/\-(.)/g, (o, c) => c.toUpperCase());
427
+ const camelKey = toCamelCase(key);
425
428
  message[camelKey] = (0, import_decode_strings.decodeWords)(header.value);
426
429
  }
427
430
  }
428
431
  let dateHeader = this.root.headers.find((line) => line.key === "date");
429
432
  if (dateHeader) {
430
433
  let date = new Date(dateHeader.value);
431
- if (!date || date.toString() === "Invalid Date") {
434
+ if (date.toString() === "Invalid Date") {
432
435
  date = dateHeader.value;
433
436
  } else {
434
437
  date = date.toISOString();
@@ -464,7 +467,7 @@ class PostalMime {
464
467
  }
465
468
  break;
466
469
  default:
467
- throw new Error("Unknwon attachment encoding");
470
+ throw new Error("Unknown attachment encoding");
468
471
  }
469
472
  return message;
470
473
  }
@@ -480,11 +483,14 @@ if (module.exports.default) {
480
483
  var defaultExport = module.exports.default;
481
484
  var namedExports = {};
482
485
  for (var key in module.exports) {
483
- if (key !== 'default') {
486
+ if (key !== 'default' && key !== '__esModule') {
484
487
  namedExports[key] = module.exports[key];
485
488
  }
486
489
  }
487
490
  module.exports = defaultExport;
488
491
  Object.assign(module.exports, namedExports);
492
+ // Preserve __esModule and .default for bundler/transpiler interop
493
+ Object.defineProperty(module.exports, '__esModule', { value: true });
494
+ module.exports.default = defaultExport;
489
495
  }
490
496
 
@@ -86,7 +86,6 @@ class QPDecoder {
86
86
  }
87
87
  if (encodedBytes.length) {
88
88
  this.chunks.push(this.decodeQPBytes(encodedBytes));
89
- encodedBytes = [];
90
89
  }
91
90
  }
92
91
  update(buffer) {
@@ -122,11 +121,14 @@ if (module.exports.default) {
122
121
  var defaultExport = module.exports.default;
123
122
  var namedExports = {};
124
123
  for (var key in module.exports) {
125
- if (key !== 'default') {
124
+ if (key !== 'default' && key !== '__esModule') {
126
125
  namedExports[key] = module.exports[key];
127
126
  }
128
127
  }
129
128
  module.exports = defaultExport;
130
129
  Object.assign(module.exports, namedExports);
130
+ // Preserve __esModule and .default for bundler/transpiler interop
131
+ Object.defineProperty(module.exports, '__esModule', { value: true });
132
+ module.exports.default = defaultExport;
131
133
  }
132
134
 
@@ -51,7 +51,7 @@ function decodeHTMLEntities(str) {
51
51
  } else {
52
52
  codePoint = parseInt(entity.substr(1), 10);
53
53
  }
54
- var output = "";
54
+ let output = "";
55
55
  if (codePoint >= 55296 && codePoint <= 57343 || codePoint > 1114111) {
56
56
  return "\uFFFD";
57
57
  }
@@ -271,11 +271,14 @@ if (module.exports.default) {
271
271
  var defaultExport = module.exports.default;
272
272
  var namedExports = {};
273
273
  for (var key in module.exports) {
274
- if (key !== 'default') {
274
+ if (key !== 'default' && key !== '__esModule') {
275
275
  namedExports[key] = module.exports[key];
276
276
  }
277
277
  }
278
278
  module.exports = defaultExport;
279
279
  Object.assign(module.exports, namedExports);
280
+ // Preserve __esModule and .default for bundler/transpiler interop
281
+ Object.defineProperty(module.exports, '__esModule', { value: true });
282
+ module.exports.default = defaultExport;
280
283
  }
281
284
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postal-mime",
3
- "version": "2.7.3",
3
+ "version": "2.7.4",
4
4
  "description": "Email parser for Node.js and browser environments",
5
5
  "main": "./dist/postal-mime.cjs",
6
6
  "module": "./src/postal-mime.js",
@@ -38,14 +38,14 @@
38
38
  "author": "Andris Reinman",
39
39
  "license": "MIT-0",
40
40
  "devDependencies": {
41
- "@types/node": "24.10.1",
41
+ "@types/node": "25.5.0",
42
42
  "cross-blob": "3.0.2",
43
43
  "cross-env": "10.1.0",
44
- "esbuild": "0.27.0",
44
+ "esbuild": "0.27.4",
45
45
  "eslint": "8.57.0",
46
46
  "eslint-cli": "1.1.1",
47
47
  "iframe-resizer": "4.3.6",
48
- "prettier": "3.6.2",
48
+ "prettier": "3.8.1",
49
49
  "typescript": "5.9.3"
50
50
  }
51
51
  }
package/postal-mime.d.ts CHANGED
@@ -3,6 +3,8 @@ export type RawEmail = string | ArrayBuffer | Uint8Array | Blob | Buffer | Reada
3
3
  export type Header = {
4
4
  /** Lowercase header name */
5
5
  key: string;
6
+ /** Original header name preserving case */
7
+ originalKey: string;
6
8
  /** Header value */
7
9
  value: string;
8
10
  };
@@ -36,7 +38,7 @@ export type Attachment = {
36
38
  description?: string;
37
39
  contentId?: string;
38
40
  method?: string;
39
- content: ArrayBuffer | string;
41
+ content: ArrayBuffer | Uint8Array | string;
40
42
  encoding?: "base64" | "utf8";
41
43
  };
42
44
 
@@ -142,13 +142,13 @@ function _handleAddress(tokens, depth) {
142
142
  }
143
143
  }
144
144
 
145
- // If there's still is no text but a comment exixts, replace the two
145
+ // If there's still no text but a comment exists, replace the two
146
146
  if (!data.text.length && data.comment.length) {
147
147
  data.text = data.comment;
148
148
  data.comment = [];
149
149
  }
150
150
 
151
- // Keep only the first address occurence, push others to regular text
151
+ // Keep only the first address occurrence, push others to regular text
152
152
  if (data.address.length > 1) {
153
153
  data.text = data.text.concat(data.address.splice(1));
154
154
  }
@@ -159,30 +159,34 @@ function _handleAddress(tokens, depth) {
159
159
 
160
160
  if (!data.address && /^=\?[^=]+?=$/.test(data.text.trim())) {
161
161
  // try to extract words from text content
162
- const parsedSubAddresses = addressParser(decodeWords(data.text));
163
- if (parsedSubAddresses && parsedSubAddresses.length) {
164
- return parsedSubAddresses;
162
+ const decodedText = decodeWords(data.text);
163
+ // Security: only re-parse if decoded text contains angle-bracket addresses.
164
+ // Without this, a bare encoded email (e.g. =?utf-8?B?dGVzdEBldmlsLmNv?=)
165
+ // would be fabricated into an address from attacker-controlled input.
166
+ if (/<[^<>]+@[^<>]+>/.test(decodedText)) {
167
+ const parsedSubAddresses = addressParser(decodedText);
168
+ if (parsedSubAddresses && parsedSubAddresses.length) {
169
+ return parsedSubAddresses;
170
+ }
165
171
  }
172
+ // No usable address found - treat decoded text as display name only
173
+ return [{ address: '', name: decodedText }];
166
174
  }
167
175
 
168
- if (!data.address && isGroup) {
169
- return [];
170
- } else {
171
- address = {
172
- address: data.address || data.text || '',
173
- name: decodeWords(data.text || data.address || '')
174
- };
176
+ address = {
177
+ address: data.address || data.text || '',
178
+ name: decodeWords(data.text || data.address || '')
179
+ };
175
180
 
176
- if (address.address === address.name) {
177
- if ((address.address || '').match(/@/)) {
178
- address.name = '';
179
- } else {
180
- address.address = '';
181
- }
181
+ if (address.address === address.name) {
182
+ if ((address.address || '').match(/@/)) {
183
+ address.name = '';
184
+ } else {
185
+ address.address = '';
182
186
  }
183
-
184
- addresses.push(address);
185
187
  }
188
+
189
+ addresses.push(address);
186
190
  }
187
191
 
188
192
  return addresses;
@@ -280,7 +284,7 @@ class Tokenizer {
280
284
  this.operatorExpecting = this.operators[chr];
281
285
  this.escaped = false;
282
286
  return;
283
- } else if (['"', "'"].includes(this.operatorExpecting) && chr === '\\') {
287
+ } else if (this.operatorExpecting === '"' && chr === '\\') {
284
288
  this.escaped = true;
285
289
  return;
286
290
  }
@@ -16,9 +16,7 @@ export default class Base64Decoder {
16
16
  update(buffer) {
17
17
  let str = this.decoder.decode(buffer);
18
18
 
19
- if (/[^a-zA-Z0-9+\/]/.test(str)) {
20
- str = str.replace(/[^a-zA-Z0-9+\/]+/g, '');
21
- }
19
+ str = str.replace(/[^a-zA-Z0-9+\/]+/g, '');
22
20
 
23
21
  this.remainder += str;
24
22
 
@@ -4,7 +4,7 @@ const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456
4
4
 
5
5
  // Use a lookup table to find the index.
6
6
  const base64Lookup = new Uint8Array(256);
7
- for (var i = 0; i < base64Chars.length; i++) {
7
+ for (let i = 0; i < base64Chars.length; i++) {
8
8
  base64Lookup[base64Chars.charCodeAt(i)] = i;
9
9
  }
10
10
 
package/src/mime-node.js CHANGED
@@ -3,6 +3,8 @@ import PassThroughDecoder from './pass-through-decoder.js';
3
3
  import Base64Decoder from './base64-decoder.js';
4
4
  import QPDecoder from './qp-decoder.js';
5
5
 
6
+ const defaultDecoder = getDecoder();
7
+
6
8
  export default class MimeNode {
7
9
  constructor(options) {
8
10
  this.options = options || {};
@@ -233,7 +235,7 @@ export default class MimeNode {
233
235
  // remove soft linebreaks
234
236
  // soft linebreaks are added after space symbols
235
237
  .reduce((previousValue, currentValue) => {
236
- if (/ $/.test(previousValue) && !/(^|\n)-- $/.test(previousValue)) {
238
+ if (previousValue.endsWith(' ') && previousValue !== '-- ' && !previousValue.endsWith('\n-- ')) {
237
239
  if (delSp) {
238
240
  // delsp adds space to text to be able to fold it
239
241
  // these spaces can be removed once the text is unfolded
@@ -360,7 +362,7 @@ export default class MimeNode {
360
362
  throw error;
361
363
  }
362
364
 
363
- this.headerLines.push(getDecoder().decode(line));
365
+ this.headerLines.push(defaultDecoder.decode(line));
364
366
  break;
365
367
  case 'body': {
366
368
  // add line to body
@@ -9,6 +9,10 @@ export { addressParser, decodeWords };
9
9
  const MAX_NESTING_DEPTH = 256;
10
10
  const MAX_HEADERS_SIZE = 2 * 1024 * 1024;
11
11
 
12
+ function toCamelCase(key) {
13
+ return key.replace(/-(.)/g, (o, c) => c.toUpperCase());
14
+ }
15
+
12
16
  export default class PostalMime {
13
17
  static parse(buf, options) {
14
18
  const parser = new PostalMime(options);
@@ -76,9 +80,11 @@ export default class PostalMime {
76
80
  let boundaryEnd = boundary.value.length + 2;
77
81
  let isTerminator = false;
78
82
 
79
- if (line.length >= boundary.value.length + 4 &&
83
+ if (
84
+ line.length >= boundary.value.length + 4 &&
80
85
  line[boundary.value.length + 2] === 0x2d &&
81
- line[boundary.value.length + 3] === 0x2d) {
86
+ line[boundary.value.length + 3] === 0x2d
87
+ ) {
82
88
  isTerminator = true;
83
89
  boundaryEnd = boundary.value.length + 4;
84
90
  }
@@ -130,13 +136,6 @@ export default class PostalMime {
130
136
  let startPos = this.readPos;
131
137
  let endPos = this.readPos;
132
138
 
133
- let res = () => {
134
- return {
135
- bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
136
- done: this.readPos >= this.av.length
137
- };
138
- };
139
-
140
139
  while (this.readPos < this.av.length) {
141
140
  const c = this.av[this.readPos++];
142
141
 
@@ -145,11 +144,17 @@ export default class PostalMime {
145
144
  }
146
145
 
147
146
  if (c === 0x0a) {
148
- return res();
147
+ return {
148
+ bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
149
+ done: this.readPos >= this.av.length
150
+ };
149
151
  }
150
152
  }
151
153
 
152
- return res();
154
+ return {
155
+ bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
156
+ done: this.readPos >= this.av.length
157
+ };
153
158
  }
154
159
 
155
160
  async processNodeTree() {
@@ -220,7 +225,9 @@ export default class PostalMime {
220
225
  // is it an attachment
221
226
  else if (node.content) {
222
227
  const filename =
223
- node.contentDisposition?.parsed?.params?.filename || node.contentType.parsed.params.name || null;
228
+ node.contentDisposition?.parsed?.params?.filename ||
229
+ node.contentType.parsed.params.name ||
230
+ null;
224
231
  const attachment = {
225
232
  filename: filename ? decodeWords(filename) : null,
226
233
  mimeType: node.contentType.parsed.value,
@@ -274,7 +281,7 @@ export default class PostalMime {
274
281
  }
275
282
  };
276
283
 
277
- await walk(this.root, false, []);
284
+ await walk(this.root, false, false);
278
285
 
279
286
  textMap.forEach(mapEntry => {
280
287
  textTypes.forEach(textType => {
@@ -471,7 +478,9 @@ export default class PostalMime {
471
478
  await this.processNodeTree();
472
479
 
473
480
  const message = {
474
- headers: this.root.headers.map(entry => ({ key: entry.key, value: entry.value })).reverse()
481
+ headers: this.root.headers
482
+ .map(entry => ({ key: entry.key, originalKey: entry.originalKey, value: entry.value }))
483
+ .reverse()
475
484
  };
476
485
 
477
486
  for (const key of ['from', 'sender']) {
@@ -489,7 +498,7 @@ export default class PostalMime {
489
498
  if (addressHeader && addressHeader.value) {
490
499
  const addresses = addressParser(addressHeader.value);
491
500
  if (addresses && addresses.length && addresses[0].address) {
492
- const camelKey = key.replace(/\-(.)/g, (o, c) => c.toUpperCase());
501
+ const camelKey = toCamelCase(key);
493
502
  message[camelKey] = addresses[0].address;
494
503
  }
495
504
  }
@@ -505,7 +514,7 @@ export default class PostalMime {
505
514
  .forEach(parsed => (addresses = addresses.concat(parsed || [])));
506
515
 
507
516
  if (addresses && addresses.length) {
508
- const camelKey = key.replace(/\-(.)/g, (o, c) => c.toUpperCase());
517
+ const camelKey = toCamelCase(key);
509
518
  message[camelKey] = addresses;
510
519
  }
511
520
  }
@@ -513,7 +522,7 @@ export default class PostalMime {
513
522
  for (const key of ['subject', 'message-id', 'in-reply-to', 'references']) {
514
523
  const header = this.root.headers.find(line => line.key === key);
515
524
  if (header && header.value) {
516
- const camelKey = key.replace(/\-(.)/g, (o, c) => c.toUpperCase());
525
+ const camelKey = toCamelCase(key);
517
526
  message[camelKey] = decodeWords(header.value);
518
527
  }
519
528
  }
@@ -521,7 +530,7 @@ export default class PostalMime {
521
530
  let dateHeader = this.root.headers.find(line => line.key === 'date');
522
531
  if (dateHeader) {
523
532
  let date = new Date(dateHeader.value);
524
- if (!date || date.toString() === 'Invalid Date') {
533
+ if (date.toString() === 'Invalid Date') {
525
534
  date = dateHeader.value;
526
535
  } else {
527
536
  // enforce ISO format if seems to be a valid date
@@ -567,7 +576,7 @@ export default class PostalMime {
567
576
  break;
568
577
 
569
578
  default:
570
- throw new Error('Unknwon attachment encoding');
579
+ throw new Error('Unknown attachment encoding');
571
580
  }
572
581
 
573
582
  return message;
package/src/qp-decoder.js CHANGED
@@ -81,7 +81,6 @@ export default class QPDecoder {
81
81
  }
82
82
  if (encodedBytes.length) {
83
83
  this.chunks.push(this.decodeQPBytes(encodedBytes));
84
- encodedBytes = [];
85
84
  }
86
85
  }
87
86
 
@@ -20,7 +20,7 @@ export function decodeHTMLEntities(str) {
20
20
  codePoint = parseInt(entity.substr(1), 10);
21
21
  }
22
22
 
23
- var output = '';
23
+ let output = '';
24
24
 
25
25
  if ((codePoint >= 0xd800 && codePoint <= 0xdfff) || codePoint > 0x10ffff) {
26
26
  // Invalid range, return a replacement character instead