postal-mime 2.6.1 → 2.7.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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.7.1](https://github.com/postalsys/postal-mime/compare/v2.7.0...v2.7.1) (2025-12-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * Add null checks for contentDisposition.parsed access ([fd54c37](https://github.com/postalsys/postal-mime/commit/fd54c37093cc64737c6bb17986bc9d052d2d5add))
9
+
10
+ ## [2.7.0](https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.0) (2025-12-22)
11
+
12
+
13
+ ### Features
14
+
15
+ * add headerLines property exposing raw header lines ([c79a02a](https://github.com/postalsys/postal-mime/commit/c79a02ab05d9cac44e05e95a433752ff292aa5eb))
16
+
3
17
  ## [2.6.1](https://github.com/postalsys/postal-mime/compare/v2.6.0...v2.6.1) (2025-11-26)
4
18
 
5
19
 
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://kreata.ee/postal-mime/example/).
42
+ Try out a live demo using the [example page](https://postal-mime.postalsys.com/demo).
43
43
 
44
44
  ## Installation
45
45
 
@@ -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
- } else {
197
- line = line.replace(/\s+/g, " ");
198
- let sep = line.indexOf(":");
199
- let key = sep < 0 ? line.trim() : line.substr(0, sep).trim();
200
- let value = sep < 0 ? "" : line.substr(sep + 1).trim();
201
- this.headers.push({ key: key.toLowerCase(), originalKey: key, value });
202
- switch (key.toLowerCase()) {
203
- case "content-type":
204
- if (this.contentType.default) {
205
- this.contentType = { value, parsed: {} };
206
- }
207
- break;
208
- case "content-transfer-encoding":
209
- this.contentTransferEncoding = { value, parsed: {} };
210
- break;
211
- case "content-disposition":
212
- this.contentDisposition = { value, parsed: {} };
213
- break;
214
- case "content-id":
215
- this.contentId = value;
216
- break;
217
- case "content-description":
218
- this.contentDescription = value;
219
- break;
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);
@@ -134,6 +134,7 @@ class PostalMime {
134
134
  let textMap = this.textMap = /* @__PURE__ */ new Map();
135
135
  let forceRfc822Attachments = this.forceRfc822Attachments();
136
136
  let walk = async (node, alternative, related) => {
137
+ var _a, _b, _c, _d, _e;
137
138
  alternative = alternative || false;
138
139
  related = related || false;
139
140
  if (!node.contentType.multipart) {
@@ -173,11 +174,11 @@ class PostalMime {
173
174
  textEntry[textType].push({ type: "text", value: node.getTextContent() });
174
175
  textTypes.add(textType);
175
176
  } else if (node.content) {
176
- const filename = node.contentDisposition.parsed.params.filename || node.contentType.parsed.params.name || null;
177
+ 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
178
  const attachment = {
178
179
  filename: filename ? (0, import_decode_strings.decodeWords)(filename) : null,
179
180
  mimeType: node.contentType.parsed.value,
180
- disposition: node.contentDisposition.parsed.value || null
181
+ disposition: ((_e = (_d = node.contentDisposition) == null ? void 0 : _d.parsed) == null ? void 0 : _e.value) || null
181
182
  };
182
183
  if (related && node.contentId) {
183
184
  attachment.related = true;
@@ -285,7 +286,8 @@ class PostalMime {
285
286
  this.textContent = textContent;
286
287
  }
287
288
  isInlineTextNode(node) {
288
- if (node.contentDisposition.parsed.value === "attachment") {
289
+ var _a, _b;
290
+ if (((_b = (_a = node.contentDisposition) == null ? void 0 : _a.parsed) == null ? void 0 : _b.value) === "attachment") {
289
291
  return false;
290
292
  }
291
293
  switch (node.contentType.parsed.value) {
@@ -299,10 +301,11 @@ class PostalMime {
299
301
  }
300
302
  }
301
303
  isInlineMessageRfc822(node) {
304
+ var _a, _b;
302
305
  if (node.contentType.parsed.value !== "message/rfc822") {
303
306
  return false;
304
307
  }
305
- let disposition = node.contentDisposition.parsed.value || (this.options.rfc822Attachments ? "attachment" : "inline");
308
+ let disposition = ((_b = (_a = node.contentDisposition) == null ? void 0 : _a.parsed) == null ? void 0 : _b.value) || (this.options.rfc822Attachments ? "attachment" : "inline");
306
309
  return disposition === "inline";
307
310
  }
308
311
  // Check if this is a specially crafted report email where message/rfc822 content should not be inlined
@@ -426,6 +429,7 @@ class PostalMime {
426
429
  message.text = this.textContent.plain;
427
430
  }
428
431
  message.attachments = this.attachments;
432
+ message.headerLines = (this.root.rawHeaderLines || []).slice().reverse();
429
433
  switch (this.attachmentEncoding) {
430
434
  case "arraybuffer":
431
435
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postal-mime",
3
- "version": "2.6.1",
3
+ "version": "2.7.1",
4
4
  "description": "Email parser for browser environments",
5
5
  "main": "./dist/postal-mime.cjs",
6
6
  "module": "./src/postal-mime.js",
@@ -34,6 +34,7 @@
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": {
package/postal-mime.d.ts CHANGED
@@ -6,6 +6,13 @@ export type Header = {
6
6
  value: string;
7
7
  };
8
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
+
9
16
  export type Mailbox = {
10
17
  name: string;
11
18
  address: string;
@@ -34,6 +41,7 @@ export type Attachment = {
34
41
 
35
42
  export type Email = {
36
43
  headers: Header[];
44
+ headerLines: HeaderLine[];
37
45
  from?: Address;
38
46
  sender?: Address;
39
47
  replyTo?: Address[];
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
- } else {
217
- // remove folding and extra WS
218
- line = line.replace(/\s+/g, ' ');
219
- let sep = line.indexOf(':');
220
- let key = sep < 0 ? line.trim() : line.substr(0, sep).trim();
221
- let value = sep < 0 ? '' : line.substr(sep + 1).trim();
222
- this.headers.push({ key: key.toLowerCase(), originalKey: key, value });
223
-
224
- switch (key.toLowerCase()) {
225
- case 'content-type':
226
- if (this.contentType.default) {
227
- this.contentType = { value, parsed: {} };
228
- }
229
- break;
230
- case 'content-transfer-encoding':
231
- this.contentTransferEncoding = { value, parsed: {} };
232
- break;
233
- case 'content-disposition':
234
- this.contentDisposition = { value, parsed: {} };
235
- break;
236
- case 'content-id':
237
- this.contentId = value;
238
- break;
239
- case 'content-description':
240
- this.contentDescription = value;
241
- break;
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
 
@@ -200,11 +200,11 @@ export default class PostalMime {
200
200
  // is it an attachment
201
201
  else if (node.content) {
202
202
  const filename =
203
- node.contentDisposition.parsed.params.filename || node.contentType.parsed.params.name || null;
203
+ node.contentDisposition?.parsed?.params?.filename || node.contentType.parsed.params.name || null;
204
204
  const attachment = {
205
205
  filename: filename ? decodeWords(filename) : null,
206
206
  mimeType: node.contentType.parsed.value,
207
- disposition: node.contentDisposition.parsed.value || null
207
+ disposition: node.contentDisposition?.parsed?.value || null
208
208
  };
209
209
 
210
210
  if (related && node.contentId) {
@@ -333,7 +333,7 @@ export default class PostalMime {
333
333
  }
334
334
 
335
335
  isInlineTextNode(node) {
336
- if (node.contentDisposition.parsed.value === 'attachment') {
336
+ if (node.contentDisposition?.parsed?.value === 'attachment') {
337
337
  // no matter the type, this is an attachment
338
338
  return false;
339
339
  }
@@ -355,7 +355,7 @@ export default class PostalMime {
355
355
  return false;
356
356
  }
357
357
  let disposition =
358
- node.contentDisposition.parsed.value || (this.options.rfc822Attachments ? 'attachment' : 'inline');
358
+ node.contentDisposition?.parsed?.value || (this.options.rfc822Attachments ? 'attachment' : 'inline');
359
359
  return disposition === 'inline';
360
360
  }
361
361
 
@@ -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;