postal-mime 2.3.2 → 2.4.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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.4.0](https://github.com/postalsys/postal-mime/compare/v2.3.2...v2.4.0) (2025-01-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * **attachments:** Added new option 'attachmentEncoding' to return attachment content as a string, not arraybuffer ([0f7e9df](https://github.com/postalsys/postal-mime/commit/0f7e9df855c9c8f99ed0b7c517d6653169c53405))
9
+
3
10
  ## [2.3.2](https://github.com/postalsys/postal-mime/compare/v2.3.1...v2.3.2) (2024-09-23)
4
11
 
5
12
 
package/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2021-2024 Andris Reinman
1
+ Copyright (c) 2021-2025 Andris Reinman
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,11 +1,27 @@
1
1
  # postal-mime
2
2
 
3
- Email parser for browser and serverless environments.
3
+ **postal-mime** is an email parsing library that runs in browser environments (including Web Workers) and serverless functions (like Cloudflare Email Workers). It takes in a raw email message (RFC822 format) and outputs a structured object containing headers, recipients, attachments, and more.
4
4
 
5
- PostalMime can be run in the main web thread or from Web Workers. It can also be used in serverless functions like Cloudflare Email Workers.
5
+ > **Tip**
6
+ > PostalMime is developed by the makers of [EmailEngine](https://emailengine.app/?utm_source=github&utm_campaign=imapflow&utm_medium=readme-link)—a self-hosted email gateway that provides a REST API for IMAP and SMTP servers and sends webhooks whenever something changes in registered accounts.
6
7
 
7
- > [!TIP]
8
- > PostalMime is developed by the makers of **[EmailEngine](https://emailengine.app/?utm_source=github&utm_campaign=imapflow&utm_medium=readme-link)** – a self-hosted email gateway that allows making **REST requests against IMAP and SMTP servers**. EmailEngine also sends webhooks whenever something changes on the registered accounts.
8
+ ## Table of Contents
9
+
10
+ - [Source](#source)
11
+ - [Demo](#demo)
12
+ - [Installation](#installation)
13
+ - [Usage](#usage)
14
+ - [Browser](#browser)
15
+ - [Node.js](#nodejs)
16
+ - [Cloudflare Email Workers](#cloudflare-email-workers)
17
+ - [API](#api)
18
+ - [PostalMime.parse()](#postalmimeparse)
19
+ - [Utility Functions](#utility-functions)
20
+ - [addressParser()](#addressparser)
21
+ - [decodeWords()](#decodewords)
22
+ - [License](#license)
23
+
24
+ ---
9
25
 
10
26
  ## Source
11
27
 
@@ -13,33 +29,23 @@ The source code is available on [GitHub](https://github.com/postalsys/postal-mim
13
29
 
14
30
  ## Demo
15
31
 
16
- See this [example](https://kreata.ee/postal-mime/example/).
17
-
18
- ## Usage
32
+ Try out a live demo using the [example page](https://kreata.ee/postal-mime/example/).
19
33
 
20
- First, install the module from npm:
21
-
22
- ```
23
- $ npm install postal-mime
24
- ```
34
+ ## Installation
25
35
 
26
- Next, import the PostalMime class into your script:
36
+ Install the module from npm:
27
37
 
28
- ```js
29
- import PostalMime from './node_modules/postal-mime/src/postal-mime.js';
38
+ ```bash
39
+ npm install postal-mime
30
40
  ```
31
41
 
32
- Or when using it from a Node.js app or in a serverless function:
33
-
34
- ```js
35
- import PostalMime from 'postal-mime';
36
- ```
42
+ ## Usage
37
43
 
38
- ### Promises
44
+ You can import the `PostalMime` class differently depending on your environment:
39
45
 
40
- PostalMime methods use Promises, so you need to wait using `await` or the `then()` method to get the response.
46
+ ### Browser
41
47
 
42
- #### Browser
48
+ To use PostalMime in the browser (including Web Workers), import it from the `src` folder:
43
49
 
44
50
  ```js
45
51
  import PostalMime from './node_modules/postal-mime/src/postal-mime.js';
@@ -49,12 +55,12 @@ Content-Type: text/html; charset=utf-8
49
55
 
50
56
  <p>Hello world 😵‍💫</p>`);
51
57
 
52
- console.log(email.subject);
58
+ console.log(email.subject); // "My awesome email 🤓"
53
59
  ```
54
60
 
55
- #### Node.js
61
+ ### Node.js
56
62
 
57
- It is pretty much the same as in the browser.
63
+ In Node.js (including serverless functions), import it directly from `postal-mime`:
58
64
 
59
65
  ```js
60
66
  import PostalMime from 'postal-mime';
@@ -65,12 +71,13 @@ Content-Type: text/html; charset=utf-8
65
71
 
66
72
  <p>Hello world 😵‍💫</p>`);
67
73
 
74
+ // Use 'util.inspect' for pretty-printing
68
75
  console.log(util.inspect(email, false, 22, true));
69
76
  ```
70
77
 
71
- #### Cloudflare [Email Workers](https://developers.cloudflare.com/email-routing/email-workers/)
78
+ ### Cloudflare Email Workers
72
79
 
73
- Pretty much the same as in Node.js. Use `message.raw` as the raw message for parsing.
80
+ Use the `message.raw` as the raw email data for parsing:
74
81
 
75
82
  ```js
76
83
  import PostalMime from 'postal-mime';
@@ -79,107 +86,106 @@ export default {
79
86
  async email(message, env, ctx) {
80
87
  const email = await PostalMime.parse(message.raw);
81
88
 
82
- console.log('Subject: ', email.subject);
83
- console.log('HTML: ', email.html);
84
- console.log('Text: ', email.text);
89
+ console.log('Subject:', email.subject);
90
+ console.log('HTML:', email.html);
91
+ console.log('Text:', email.text);
85
92
  }
86
93
  };
87
94
  ```
88
95
 
89
- #### PostalMime.parse()
96
+ ---
90
97
 
91
- `parse(email, options)` is a static class method used to parse emails.
98
+ ## API
99
+
100
+ ### PostalMime.parse()
92
101
 
93
102
  ```js
94
- PostalMime.parse(email, options) -> Promise
103
+ PostalMime.parse(email, options) -> Promise<ParsedEmail>
95
104
  ```
96
105
 
97
- Where:
98
-
99
- - **email**: The RFC822 formatted email. This can be a string, an ArrayBuffer/Uint8Array, a Blob object, a Node.js Buffer, or a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
100
- - **options**: An optional object containing configuration options.
101
- - **rfc822Attachments**: A boolean (defaults to `false`). If set to `true`, then treats `message/rfc822` attachments without a Content-Disposition declaration as attachments. By default, these messages are treated as inline values.
102
- - **forceRfc822Attachments**: A boolean (defaults to `false`). If set to `true`, then treats all `message/rfc822` nodes as attachments.
103
-
104
- This method parses an email message into a structured object with the following properties:
105
-
106
- - **headers**: An array of headers in the order they appear in the message (topmost headers first).
107
- - **headers[].key**: The lowercase key of the header line, e.g., `"dkim-signature"`.
108
- - **headers[].value**: The unprocessed value of the header line.
109
- - **from**, **sender**: Includes a processed object for the corresponding headers.
110
- - **from.name**: The decoded name (empty string if not set).
111
- - **from.address**: The email address.
112
- - **deliveredTo**, **returnPath**: The email address from the corresponding header.
113
- - **to**, **cc**, **bcc**, **replyTo**: An array of processed objects for the corresponding headers.
114
- - **to[].name**: The decoded name (empty string if not set).
115
- - **to[].address**: The email address.
116
- - **subject**: The email subject line.
117
- - **messageId**, **inReplyTo**, **references**: The value as found in the corresponding header without any processing.
118
- - **date**: The email sending time formatted as an ISO date string (unless parsing failed, in which case the original value is used).
119
- - **html**: The HTML content of the message as a string.
120
- - **text**: The plaintext content of the message as a string.
121
- - **attachments**: An array that includes the message attachments.
122
- - **attachments[].filename**: The file name if provided.
123
- - **attachments[].mimeType**: The MIME type of the attachment.
124
- - **attachments[].disposition**: Either "attachment", "inline", or `null` if disposition was not provided.
125
- - **attachments[].related**: A boolean value indicating if this attachment should be treated as an embedded image.
126
- - **attachments[].contentId**: The ID from the Content-ID header.
127
- - **attachments[].content**: A Uint8Array value that contains the attachment file.
106
+ - **email**: An RFC822 formatted email. This can be a `string`, `ArrayBuffer/Uint8Array`, `Blob` (browser only), `Buffer` (Node.js), or a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
107
+ - **options**: Optional configuration object:
108
+ - **rfc822Attachments** (boolean, default: `false`): Treat `message/rfc822` attachments without a Content-Disposition as attachments.
109
+ - **forceRfc822Attachments** (boolean, default: `false`): Treat _all_ `message/rfc822` parts as attachments.
110
+ - **attachmentEncoding** (string): Determines how attachment content is decoded in the parsed email:
111
+ - `"base64"`
112
+ - `"utf8"`
113
+ - `"arraybuffer"` (no decoding, returns `ArrayBuffer`)
114
+
115
+ **Returns**: A Promise that resolves to a structured object with the following properties:
116
+
117
+ - **headers**: An array of header objects, each containing:
118
+ - `key`: Lowercase header name (e.g., `"dkim-signature"`).
119
+ - `value`: Unprocessed header value as a string.
120
+ - **from**, **sender**: Processed address objects:
121
+ - `name`: Decoded display name, or an empty string if not set.
122
+ - `address`: Email address.
123
+ - **deliveredTo**, **returnPath**: Single email addresses as strings.
124
+ - **to**, **cc**, **bcc**, **replyTo**: Arrays of processed address objects (same structure as `from`).
125
+ - **subject**: Subject line of the email.
126
+ - **messageId**, **inReplyTo**, **references**: Values from their corresponding headers.
127
+ - **date**: The email’s sending time in ISO 8601 format (or the original string if parsing fails).
128
+ - **html**: String containing the HTML content of the email.
129
+ - **text**: String containing the plain text content of the email.
130
+ - **attachments**: Array of attachment objects:
131
+ - `filename`
132
+ - `mimeType`
133
+ - `disposition` (e.g., `"attachment"`, `"inline"`, or `null`)
134
+ - `related` (boolean, `true` if it’s an inline image)
135
+ - `contentId`
136
+ - `content` (array buffer or string, depending on `attachmentEncoding`)
137
+ - `encoding` (e.g., `"base64"`)
138
+
139
+ ---
128
140
 
129
141
  ### Utility Functions
130
142
 
131
- #### addressParser
132
-
133
- Parse email address strings.
143
+ #### addressParser()
134
144
 
135
145
  ```js
146
+ import { addressParser } from 'postal-mime';
147
+
136
148
  addressParser(addressStr, opts) -> Array
137
149
  ```
138
150
 
139
- Where:
151
+ - **addressStr**: A raw address header string.
152
+ - **opts**: Optional configuration:
153
+ - **flatten** (boolean, default: `false`): If `true`, ignores address groups and returns a flat array of addresses.
140
154
 
141
- - **addressStr**: The header value for an address header.
142
- - **opts**: An optional object containing configuration options.
143
- - **flatten**: A boolean value. If set to `true`, it ignores address groups and returns a flat array of addresses. By default (`flatten` is `false`), the result might include nested groups.
155
+ **Returns**: An array of address objects, which can be nested if address groups are present.
144
156
 
145
- The result is an array of objects:
146
-
147
- - **name**: The name string. An empty string is used if the name value is not set.
148
- - **address**: The email address value.
149
- - **group**: An array of nested address objects. This is used when `flatten` is `false` (the default) and the address string contains address group syntax.
157
+ **Example**:
150
158
 
151
159
  ```js
152
- import { addressParser } from 'postal-mime';
153
-
154
160
  const addressStr = '=?utf-8?B?44Ko44Od44K544Kr44O844OJ?= <support@example.com>';
155
161
  console.log(addressParser(addressStr));
156
162
  // [ { name: 'エポスカード', address: 'support@example.com' } ]
157
163
  ```
158
164
 
159
- #### decodeWords
160
-
161
- Decode MIME encoded-words.
165
+ #### decodeWords()
162
166
 
163
167
  ```js
164
- decodeWords(encodedStr) -> String
168
+ import { decodeWords } from 'postal-mime';
169
+
170
+ decodeWords(encodedStr) -> string
165
171
  ```
166
172
 
167
- Where:
173
+ - **encodedStr**: A string that may contain MIME encoded-words.
168
174
 
169
- - **encodedStr**: A string value that _may_ include MIME encoded-words.
175
+ **Returns**: A Unicode string with all encoded-words decoded.
170
176
 
171
- The result is a Unicode string.
177
+ **Example**:
172
178
 
173
179
  ```js
174
- import { decodeWords } from 'postal-mime';
175
-
176
180
  const encodedStr = 'Hello, =?utf-8?B?44Ko44Od44K544Kr44O844OJ?=';
177
181
  console.log(decodeWords(encodedStr));
178
182
  // Hello, エポスカード
179
183
  ```
180
184
 
185
+ ---
186
+
181
187
  ## License
182
188
 
183
- &copy; 2021-2024 Andris Reinman
189
+ &copy; 2021–2025 Andris Reinman
184
190
 
185
- `postal-mime` is licensed under the **MIT No Attribution license**
191
+ `postal-mime` is licensed under the **MIT No Attribution license**.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postal-mime",
3
- "version": "2.3.2",
3
+ "version": "2.4.0",
4
4
  "description": "Email parser for browser environments",
5
5
  "main": "./src/postal-mime.js",
6
6
  "exports": {
package/postal-mime.d.ts CHANGED
@@ -16,7 +16,8 @@ export type Attachment = {
16
16
  description?: string;
17
17
  contentId?: string;
18
18
  method?: string;
19
- content: ArrayBuffer;
19
+ content: ArrayBuffer | string;
20
+ encoding?: "base64" | "utf8";
20
21
  };
21
22
 
22
23
  export type Email = {
@@ -54,7 +55,8 @@ declare function decodeWords (
54
55
 
55
56
  declare type PostalMimeOptions = {
56
57
  rfc822Attachments?: boolean,
57
- forceRfc822Attachments?: boolean
58
+ forceRfc822Attachments?: boolean,
59
+ attachmentEncoding?: "base64" | "utf8"
58
60
  }
59
61
 
60
62
  declare class PostalMime {
@@ -0,0 +1,69 @@
1
+ // Code from: https://gist.githubusercontent.com/jonleighton/958841/raw/fb05a8632efb75d85d43deb593df04367ce48371/base64ArrayBuffer.js
2
+
3
+ // Converts an ArrayBuffer directly to base64, without any intermediate 'convert to string then
4
+ // use window.btoa' step. According to my tests, this appears to be a faster approach:
5
+ // http://jsperf.com/encoding-xhr-image-data/5
6
+
7
+ /*
8
+ MIT LICENSE
9
+
10
+ Copyright 2011 Jon Leighton
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ */
18
+
19
+ export function base64ArrayBuffer(arrayBuffer) {
20
+ var base64 = '';
21
+ var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
22
+
23
+ var bytes = new Uint8Array(arrayBuffer);
24
+ var byteLength = bytes.byteLength;
25
+ var byteRemainder = byteLength % 3;
26
+ var mainLength = byteLength - byteRemainder;
27
+
28
+ var a, b, c, d;
29
+ var chunk;
30
+
31
+ // Main loop deals with bytes in chunks of 3
32
+ for (var i = 0; i < mainLength; i = i + 3) {
33
+ // Combine the three bytes into a single integer
34
+ chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
35
+
36
+ // Use bitmasks to extract 6-bit segments from the triplet
37
+ a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
38
+ b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
39
+ c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
40
+ d = chunk & 63; // 63 = 2^6 - 1
41
+
42
+ // Convert the raw binary segments to the appropriate ASCII encoding
43
+ base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
44
+ }
45
+
46
+ // Deal with the remaining bytes and padding
47
+ if (byteRemainder == 1) {
48
+ chunk = bytes[mainLength];
49
+
50
+ a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
51
+
52
+ // Set the 4 least significant bits to zero
53
+ b = (chunk & 3) << 4; // 3 = 2^2 - 1
54
+
55
+ base64 += encodings[a] + encodings[b] + '==';
56
+ } else if (byteRemainder == 2) {
57
+ chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
58
+
59
+ a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
60
+ b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
61
+
62
+ // Set the 2 least significant bits to zero
63
+ c = (chunk & 15) << 2; // 15 = 2^4 - 1
64
+
65
+ base64 += encodings[a] + encodings[b] + encodings[c] + '=';
66
+ }
67
+
68
+ return base64;
69
+ }
@@ -2,6 +2,7 @@ import MimeNode from './mime-node.js';
2
2
  import { textToHtml, htmlToText, formatTextHeader, formatHtmlHeader } from './text-format.js';
3
3
  import addressParser from './address-parser.js';
4
4
  import { decodeWords, textEncoder, blobToArrayBuffer } from './decode-strings.js';
5
+ import { base64ArrayBuffer } from './base64-encoder.js';
5
6
 
6
7
  export { addressParser, decodeWords };
7
8
 
@@ -22,6 +23,13 @@ export default class PostalMime {
22
23
  this.textContent = {};
23
24
  this.attachments = [];
24
25
 
26
+ this.attachmentEncoding =
27
+ (this.options.attachmentEncoding || '')
28
+ .toString()
29
+ .replace(/[-_\s]/g, '')
30
+ .trim()
31
+ .toLowerCase() || 'arraybuffer';
32
+
25
33
  this.started = false;
26
34
  }
27
35
 
@@ -495,6 +503,33 @@ export default class PostalMime {
495
503
 
496
504
  message.attachments = this.attachments;
497
505
 
506
+ switch (this.attachmentEncoding) {
507
+ case 'arraybuffer':
508
+ break;
509
+
510
+ case 'base64':
511
+ for (let attachment of message.attachments || []) {
512
+ if (attachment?.content) {
513
+ attachment.content = base64ArrayBuffer(attachment.content);
514
+ attachment.encoding = 'base64';
515
+ }
516
+ }
517
+ break;
518
+
519
+ case 'utf8':
520
+ let attachmentDecoder = new TextDecoder('utf8');
521
+ for (let attachment of message.attachments || []) {
522
+ if (attachment?.content) {
523
+ attachment.content = attachmentDecoder.decode(attachment.content);
524
+ attachment.encoding = 'utf8';
525
+ }
526
+ }
527
+ break;
528
+
529
+ default:
530
+ throw new Error('Unknwon attachment encoding');
531
+ }
532
+
498
533
  return message;
499
534
  }
500
535
  }