diff --git a/LICENSE.txt b/LICENSE.txt
index 8243b3d..d03231f 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2021-2024 Andris Reinman
+Copyright (c) 2021-2025 Andris Reinman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 0d6dab9..840f0c1 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,27 @@
# postal-mime
-Email parser for browser and serverless environments.
+**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.
-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.
+> **Tip**
+> 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.
-> [!TIP]
-> 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.
+## Table of Contents
+
+- [Source](#source)
+- [Demo](#demo)
+- [Installation](#installation)
+- [Usage](#usage)
+ - [Browser](#browser)
+ - [Node.js](#nodejs)
+ - [Cloudflare Email Workers](#cloudflare-email-workers)
+- [API](#api)
+ - [PostalMime.parse()](#postalmimeparse)
+ - [Utility Functions](#utility-functions)
+ - [addressParser()](#addressparser)
+ - [decodeWords()](#decodewords)
+- [License](#license)
+
+---
## Source
@@ -13,33 +29,23 @@ The source code is available on [GitHub](https://github.com/postalsys/postal-mim
## Demo
-See this [example](https://kreata.ee/postal-mime/example/).
-
-## Usage
+Try out a live demo using the [example page](https://kreata.ee/postal-mime/example/).
-First, install the module from npm:
-
-```
-$ npm install postal-mime
-```
+## Installation
-Next, import the PostalMime class into your script:
+Install the module from npm:
-```js
-import PostalMime from './node_modules/postal-mime/src/postal-mime.js';
+```bash
+npm install postal-mime
```
-Or when using it from a Node.js app or in a serverless function:
-
-```js
-import PostalMime from 'postal-mime';
-```
+## Usage
-### Promises
+You can import the `PostalMime` class differently depending on your environment:
-PostalMime methods use Promises, so you need to wait using `await` or the `then()` method to get the response.
+### Browser
-#### Browser
+To use PostalMime in the browser (including Web Workers), import it from the `src` folder:
```js
import PostalMime from './node_modules/postal-mime/src/postal-mime.js';
@@ -49,12 +55,12 @@ Content-Type: text/html; charset=utf-8
Hello world π΅βπ«
`);
-console.log(email.subject);
+console.log(email.subject); // "My awesome email π€"
```
-#### Node.js
+### Node.js
-It is pretty much the same as in the browser.
+In Node.js (including serverless functions), import it directly from `postal-mime`:
```js
import PostalMime from 'postal-mime';
@@ -65,12 +71,13 @@ Content-Type: text/html; charset=utf-8
Hello world π΅βπ«
`);
+// Use 'util.inspect' for pretty-printing
console.log(util.inspect(email, false, 22, true));
```
-#### Cloudflare [Email Workers](https://developers.cloudflare.com/email-routing/email-workers/)
+### Cloudflare Email Workers
-Pretty much the same as in Node.js. Use `message.raw` as the raw message for parsing.
+Use the `message.raw` as the raw email data for parsing:
```js
import PostalMime from 'postal-mime';
@@ -79,107 +86,106 @@ export default {
async email(message, env, ctx) {
const email = await PostalMime.parse(message.raw);
- console.log('Subject: ', email.subject);
- console.log('HTML: ', email.html);
- console.log('Text: ', email.text);
+ console.log('Subject:', email.subject);
+ console.log('HTML:', email.html);
+ console.log('Text:', email.text);
}
};
```
-#### PostalMime.parse()
+---
-`parse(email, options)` is a static class method used to parse emails.
+## API
+
+### PostalMime.parse()
```js
-PostalMime.parse(email, options) -> Promise
+PostalMime.parse(email, options) -> Promise
```
-Where:
-
-- **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).
-- **options**: An optional object containing configuration options.
- - **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.
- - **forceRfc822Attachments**: A boolean (defaults to `false`). If set to `true`, then treats all `message/rfc822` nodes as attachments.
-
-This method parses an email message into a structured object with the following properties:
-
-- **headers**: An array of headers in the order they appear in the message (topmost headers first).
- - **headers[].key**: The lowercase key of the header line, e.g., `"dkim-signature"`.
- - **headers[].value**: The unprocessed value of the header line.
-- **from**, **sender**: Includes a processed object for the corresponding headers.
- - **from.name**: The decoded name (empty string if not set).
- - **from.address**: The email address.
-- **deliveredTo**, **returnPath**: The email address from the corresponding header.
-- **to**, **cc**, **bcc**, **replyTo**: An array of processed objects for the corresponding headers.
- - **to[].name**: The decoded name (empty string if not set).
- - **to[].address**: The email address.
-- **subject**: The email subject line.
-- **messageId**, **inReplyTo**, **references**: The value as found in the corresponding header without any processing.
-- **date**: The email sending time formatted as an ISO date string (unless parsing failed, in which case the original value is used).
-- **html**: The HTML content of the message as a string.
-- **text**: The plaintext content of the message as a string.
-- **attachments**: An array that includes the message attachments.
- - **attachments[].filename**: The file name if provided.
- - **attachments[].mimeType**: The MIME type of the attachment.
- - **attachments[].disposition**: Either "attachment", "inline", or `null` if disposition was not provided.
- - **attachments[].related**: A boolean value indicating if this attachment should be treated as an embedded image.
- - **attachments[].contentId**: The ID from the Content-ID header.
- - **attachments[].content**: An ArrayBuffer value that contains the attachment file.
+- **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).
+- **options**: Optional configuration object:
+ - **rfc822Attachments** (boolean, default: `false`): Treat `message/rfc822` attachments without a Content-Disposition as attachments.
+ - **forceRfc822Attachments** (boolean, default: `false`): Treat _all_ `message/rfc822` parts as attachments.
+ - **attachmentEncoding** (string): Determines how attachment content is decoded in the parsed email:
+ - `"base64"`
+ - `"utf8"`
+ - `"arraybuffer"` (no decoding, returns `ArrayBuffer`)
+
+**Returns**: A Promise that resolves to a structured object with the following properties:
+
+- **headers**: An array of header objects, each containing:
+ - `key`: Lowercase header name (e.g., `"dkim-signature"`).
+ - `value`: Unprocessed header value as a string.
+- **from**, **sender**: Processed address objects:
+ - `name`: Decoded display name, or an empty string if not set.
+ - `address`: Email address.
+- **deliveredTo**, **returnPath**: Single email addresses as strings.
+- **to**, **cc**, **bcc**, **replyTo**: Arrays of processed address objects (same structure as `from`).
+- **subject**: Subject line of the email.
+- **messageId**, **inReplyTo**, **references**: Values from their corresponding headers.
+- **date**: The emailβs sending time in ISO 8601 format (or the original string if parsing fails).
+- **html**: String containing the HTML content of the email.
+- **text**: String containing the plain text content of the email.
+- **attachments**: Array of attachment objects:
+ - `filename`
+ - `mimeType`
+ - `disposition` (e.g., `"attachment"`, `"inline"`, or `null`)
+ - `related` (boolean, `true` if itβs an inline image)
+ - `contentId`
+ - `content` (array buffer or string, depending on `attachmentEncoding`)
+ - `encoding` (e.g., `"base64"`)
+
+---
### Utility Functions
-#### addressParser
-
-Parse email address strings.
+#### addressParser()
```js
+import { addressParser } from 'postal-mime';
+
addressParser(addressStr, opts) -> Array
```
-Where:
+- **addressStr**: A raw address header string.
+- **opts**: Optional configuration:
+ - **flatten** (boolean, default: `false`): If `true`, ignores address groups and returns a flat array of addresses.
-- **addressStr**: The header value for an address header.
-- **opts**: An optional object containing configuration options.
- - **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.
+**Returns**: An array of address objects, which can be nested if address groups are present.
-The result is an array of objects:
-
-- **name**: The name string. An empty string is used if the name value is not set.
-- **address**: The email address value.
-- **group**: An array of nested address objects. This is used when `flatten` is `false` (the default) and the address string contains address group syntax.
+**Example**:
```js
-import { addressParser } from 'postal-mime';
-
const addressStr = '=?utf-8?B?44Ko44Od44K544Kr44O844OJ?= ';
console.log(addressParser(addressStr));
// [ { name: 'γ¨γγΉγ«γΌγ', address: 'support@example.com' } ]
```
-#### decodeWords
-
-Decode MIME encoded-words.
+#### decodeWords()
```js
-decodeWords(encodedStr) -> String
+import { decodeWords } from 'postal-mime';
+
+decodeWords(encodedStr) -> string
```
-Where:
+- **encodedStr**: A string that may contain MIME encoded-words.
-- **encodedStr**: A string value that _may_ include MIME encoded-words.
+**Returns**: A Unicode string with all encoded-words decoded.
-The result is a Unicode string.
+**Example**:
```js
-import { decodeWords } from 'postal-mime';
-
const encodedStr = 'Hello, =?utf-8?B?44Ko44Od44K544Kr44O844OJ?=';
console.log(decodeWords(encodedStr));
// Hello, γ¨γγΉγ«γΌγ
```
+---
+
## License
-© 2021-2024 Andris Reinman
+© 2021β2025 Andris Reinman
-`postal-mime` is licensed under the **MIT No Attribution license**
+`postal-mime` is licensed under the **MIT No Attribution license**.
diff --git a/example/example.js b/example/example.js
index 831c7a2..c9853dc 100644
--- a/example/example.js
+++ b/example/example.js
@@ -223,9 +223,9 @@ function renderEmail(email) {
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('open-file').addEventListener('click', () => {
browseFileContents()
- .then(file => PostalMime.parse(file))
+ .then(file => PostalMime.parse(file, { attachmentEncoding: 'utf8' }))
.then(email => {
- console.log(JSON.stringify(email, false, 2));
+ console.log(email);
renderEmail(email);
})
.catch(err => console.error(err));
diff --git a/postal-mime.d.ts b/postal-mime.d.ts
index b202190..d574a6e 100644
--- a/postal-mime.d.ts
+++ b/postal-mime.d.ts
@@ -16,7 +16,8 @@ export type Attachment = {
description?: string;
contentId?: string;
method?: string;
- content: ArrayBuffer;
+ content: ArrayBuffer | string;
+ encoding?: "base64" | "utf8";
};
export type Email = {
@@ -54,7 +55,8 @@ declare function decodeWords (
declare type PostalMimeOptions = {
rfc822Attachments?: boolean,
- forceRfc822Attachments?: boolean
+ forceRfc822Attachments?: boolean,
+ attachmentEncoding?: "base64" | "utf8"
}
declare class PostalMime {
diff --git a/src/base64-encoder.js b/src/base64-encoder.js
new file mode 100644
index 0000000..23207d3
--- /dev/null
+++ b/src/base64-encoder.js
@@ -0,0 +1,69 @@
+// Code from: https://gist.githubusercontent.com/jonleighton/958841/raw/fb05a8632efb75d85d43deb593df04367ce48371/base64ArrayBuffer.js
+
+// Converts an ArrayBuffer directly to base64, without any intermediate 'convert to string then
+// use window.btoa' step. According to my tests, this appears to be a faster approach:
+// http://jsperf.com/encoding-xhr-image-data/5
+
+/*
+MIT LICENSE
+
+Copyright 2011 Jon Leighton
+
+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:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+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.
+*/
+
+export function base64ArrayBuffer(arrayBuffer) {
+ var base64 = '';
+ var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+
+ var bytes = new Uint8Array(arrayBuffer);
+ var byteLength = bytes.byteLength;
+ var byteRemainder = byteLength % 3;
+ var mainLength = byteLength - byteRemainder;
+
+ var a, b, c, d;
+ var chunk;
+
+ // Main loop deals with bytes in chunks of 3
+ for (var i = 0; i < mainLength; i = i + 3) {
+ // Combine the three bytes into a single integer
+ chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
+
+ // Use bitmasks to extract 6-bit segments from the triplet
+ a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
+ b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
+ c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
+ d = chunk & 63; // 63 = 2^6 - 1
+
+ // Convert the raw binary segments to the appropriate ASCII encoding
+ base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
+ }
+
+ // Deal with the remaining bytes and padding
+ if (byteRemainder == 1) {
+ chunk = bytes[mainLength];
+
+ a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
+
+ // Set the 4 least significant bits to zero
+ b = (chunk & 3) << 4; // 3 = 2^2 - 1
+
+ base64 += encodings[a] + encodings[b] + '==';
+ } else if (byteRemainder == 2) {
+ chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
+
+ a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
+ b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
+
+ // Set the 2 least significant bits to zero
+ c = (chunk & 15) << 2; // 15 = 2^4 - 1
+
+ base64 += encodings[a] + encodings[b] + encodings[c] + '=';
+ }
+
+ return base64;
+}
diff --git a/src/postal-mime.js b/src/postal-mime.js
index 37b293d..2c2d541 100644
--- a/src/postal-mime.js
+++ b/src/postal-mime.js
@@ -2,6 +2,7 @@ import MimeNode from './mime-node.js';
import { textToHtml, htmlToText, formatTextHeader, formatHtmlHeader } from './text-format.js';
import addressParser from './address-parser.js';
import { decodeWords, textEncoder, blobToArrayBuffer } from './decode-strings.js';
+import { base64ArrayBuffer } from './base64-encoder.js';
export { addressParser, decodeWords };
@@ -22,6 +23,13 @@ export default class PostalMime {
this.textContent = {};
this.attachments = [];
+ this.attachmentEncoding =
+ (this.options.attachmentEncoding || '')
+ .toString()
+ .replace(/[-_\s]/g, '')
+ .trim()
+ .toLowerCase() || 'arraybuffer';
+
this.started = false;
}
@@ -495,6 +503,33 @@ export default class PostalMime {
message.attachments = this.attachments;
+ switch (this.attachmentEncoding) {
+ case 'arraybuffer':
+ break;
+
+ case 'base64':
+ for (let attachment of message.attachments || []) {
+ if (attachment?.content) {
+ attachment.content = base64ArrayBuffer(attachment.content);
+ attachment.encoding = 'base64';
+ }
+ }
+ break;
+
+ case 'utf8':
+ let attachmentDecoder = new TextDecoder('utf8');
+ for (let attachment of message.attachments || []) {
+ if (attachment?.content) {
+ attachment.content = attachmentDecoder.decode(attachment.content);
+ attachment.encoding = 'utf8';
+ }
+ }
+ break;
+
+ default:
+ throw new Error('Unknwon attachment encoding');
+ }
+
return message;
}
}