# postal-mime turns a raw RFC822 message into a structured object in one call

> postal-mime is a zero dependency TypeScript library that parses raw email messages into headers, recipients and attachments, built to run in Node.js, browsers, Web Workers and Cloudflare Email Workers. It suits anyone who has to handle inbound mail in a runtime without a mail server.

**postalsys/postal-mime** — Email parser for browser and serverless environments

- Repository: https://github.com/postalsys/postal-mime
- Website: https://postal-mime.postalsys.com
- Stars: 558 · Forks: 45
- Language: TypeScript
- License: NOASSERTION
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/postalsys-postal-mime

## The problem of holding a raw message in a JavaScript runtime

postal-mime takes a raw email message in RFC822 format and outputs a structured object containing headers, recipients, attachments and more. The library is aimed at Node.js, browsers including Web Workers, and serverless environments such as Cloudflare Email Workers. The problem it solves sounds trivial until you look at what is actually in a raw message. An email body is a MIME tree. A message can be multipart, the parts can nest, an attachment can be base64 encoded, a subject line can be encoded as an RFC2047 word with a charset, and a recipient list can contain display names, groups and comments. Any application that receives mail and wants the subject, the from address and the attachments has to implement that parsing, and most implementations get it wrong in the same handful of ways.

The key design constraint is that it has to work in environments with no filesystem, no child processes and no native modules. A parser that shells out to a mail tool, or one that depends on a Node-only library, is useless in a Cloudflare Worker or in a browser. postal-mime is written in TypeScript with no external dependencies at all, and it uses the web platform primitives the runtime already provides.

The stated runtime requirement is precise about that. The package needs TextDecoder, Blob and ReadableStream globals, which means Node.js 20 or newer, any modern browser, Deno, Bun or Cloudflare Workers. The package metadata agrees, with engines node >=20. It is also worth knowing where the project comes from, because it explains the design. The documentation says postal-mime is developed by the makers of EmailEngine, a self-hosted email gateway that provides a REST API for IMAP and SMTP servers and sends webhooks when something changes in registered accounts. A library born from an email gateway has been shaped by real, messy mail in production.

## One API, three import shapes

The public surface is a class with a static parse method, plus two utility functions. What varies is how you import it. In Node.js and in serverless functions you import the package by name and the runtime resolves the module build:

```js
import PostalMime from 'postal-mime';
```

In a browser with a bundler such as Vite, webpack or esbuild, the same import works and the bundler picks up the ES module build. Without a bundler, including inside a Web Worker, you import the ES module build directly by path:

```js
import PostalMime from './node_modules/postal-mime/dist/esm/postal-mime.js';
```

In a CommonJS project the package resolves to its CommonJS build, and the utility functions are destructured from the same module:

```js
const PostalMime = require('postal-mime');
const { addressParser, decodeWords } = require('postal-mime');
```

The package manifest is what makes those three cases work. It declares type module, points main at the CommonJS entry, module at the ES module entry and types at the CommonJS declaration file, and the exports map has separate import and require conditions under the package root plus a package.json subpath:

```json
    "main": "./dist/cjs/postal-mime.js",
    "module": "./dist/esm/postal-mime.js",
    "types": "./dist/cjs/postal-mime.d.ts",
    "exports": {
        ".": {
            "import": "./dist/esm/postal-mime.js",
            "require": "./dist/cjs/postal-mime.js"
        },
```

The documentation states that the ES module build in dist/esm/ and the CommonJS build in dist/cjs/ are compiled from the same TypeScript source, and each ships its own type declarations. One source, two builds, no drift between them.

## Installing it and parsing your first message

Installation is a single npm command:

```bash
npm install postal-mime
```

Then you call parse with the raw message. The documentation example uses a short message with a subject line and an HTML body, and you read a field off the returned object, so the shape of the call is parse with the raw text and then read a property such as subject off the result.

```js
import PostalMime from 'postal-mime';
import util from 'node:util';
```

In Node.js the documentation shows using util.inspect to pretty print the whole parsed object, with the depth set to 22 and colours enabled, which is worth copying because the output is a nested structure and a single console.log of the object is hard to read.

There is an options object, typed as PostalMimeOptions, and the documented option is attachmentEncoding, with the value base64 shown in the TypeScript example. That is the setting to look at first, because attachment bytes have to end up somewhere and the representation is a choice rather than a detail. There is also a live demo page at postal-mime.postalsys.com/demo, which is the fastest way to see what the parsed structure looks like for a message you paste in, without writing any code.

## The security limits, which are the interesting engineering

The feature list includes a line that most email libraries omit: security limits built as protection against deeply nested or very wide messages, oversized headers and runaway nested message parsing. That deserves more than a sentence, because it points at a real class of denial of service against mail processors. A message can declare itself as multipart, contain a part that is itself a multipart, and contain a message attached as an attachment, which is a message, which can contain a multipart. Left unbounded, parsing that structure is a stack overflow or a very long loop, and an attacker who can send you mail controls the shape of what you parse.

The library is built to run where a request has a strict time budget, in a serverless function that is billed by invocation or in an edge worker with a CPU limit. A parser without these limits is not usable there, because a single hostile message can occupy a worker long enough to run up the bill or trip the limit. So the limits are not defensive decoration, they are what makes the library fit the environment it targets.

The related guarantee is that the parser follows the RFC 2822 and 5322 email standards rather than doing what a particular mail client happens to do. That choice cuts both ways and is worth being honest about. Standards compliance is what you want when the message came from a well behaved sender and what causes friction when it came from a broken one, since real mail in the wild carries vendor specific headers and malformed date formats. The parser also passes the Content-Disposition token through as it is, which the version 4 notes call out explicitly as a change in typing rather than in behaviour.

## What changed in version 4, and what it costs you

Version 4 is a TypeScript rewrite. The documentation is reassuring about the headline: code that imports postal-mime by name keeps working and the parsed output is unchanged. Then it lists five specific changes, and they are the ones that will surface in a pull request.

The browser deep import path moved from src/postal-mime.js to dist/esm/postal-mime.js, which affects only code that loaded the file from node_modules by its path, and the Node.js floor rose to 20. The Attachment.disposition field is now typed as AttachmentDisposition or null, because the parser always passed the Content-Disposition token through as it is and the type now says so instead of claiming only attachment and inline occur. Stream input is typed as ReadableStream of Uint8Array; a ReadableStream of ArrayBuffer or of string never parsed correctly and no longer type-checks, while any ArrayBufferView including a DataView is accepted. And addressParser no longer reads an undocumented _depth option.

The fifth is the one most likely to bite a TypeScript project. CommonJS TypeScript projects get a declaration that matches what require returns, using export =, so a project that compiles with both esModuleInterop and allowSyntheticDefaultImports switched off has to use the import equals require form rather than a default import. The documented forms are:

```typescript
import PostalMime = require('postal-mime');
// or, with esModuleInterop
// import PostalMime, { addressParser, decodeWords } from 'postal-mime';
```

This is the upgrade tax, and it is small but it is real. The library is MIT-0 licensed according to the package metadata, with the licence file shipped as LICENSE.txt and included in the published files list, which is close to public domain and carries almost no obligation for a commercial user embedding it.

## Where postal-mime is the wrong choice

The first boundary is scope. postal-mime only parses. If you need to send mail, compose a message, or handle delivery and bounce handling, you need a different library, and the most common pairing is with a sender such as nodemailer, which is present in this project's own dev dependencies for testing. The parsing side and the sending side are separate problems and separating them is the right design, but it means one package does not cover your mail pipeline.

The second boundary is the runtime floor and the environment assumption. Node.js 20 or newer is required, which rules out older server runtimes, and while the library runs in browsers and workers, that is only useful if your application is a browser or worker application in the first place. If your mail processing happens in a backend service with a full Node.js environment and access to a mail library you already deploy, the browser compatibility buys you nothing and the security limits are a duplicate of protections you may already have.

The third is the one that decides it for many teams: the parsed output is only as good as the parser's coverage of the mess in real mail, and the documentation does not enumerate the malformed inputs it tolerates. Standards compliance plus depth and width limits means the library is defensible, but a team processing customer mail with a long tail of broken senders should not assume the object shape matches what they expect without testing against their own corpus. The live demo and the example directory in the repository, with example.js, node-example.js and an index.html page, are there to let you check that before you commit to it.

## Conclusion

Adopt postal-mime if you receive raw mail in a JavaScript runtime, especially a serverless or edge one, and you want headers, recipients and attachments as typed objects without pulling in a mail server or a dependency tree. Do not adopt it if you need to send mail, since it only parses, and do not stay on version 3 without reading the upgrade notes, because version 4 is a TypeScript rewrite that raises the Node.js floor to 20 and changes the browser deep import path. Before you upgrade, read the Upgrading from 3.x section, check whether your project compiles with esModuleInterop and allowSyntheticDefaultImports switched off, and confirm the attachment encoding you need, since it is an option rather than a default.

## FAQ

### How do I install postal-mime and parse an email?

Run `npm install postal-mime`, then call the static parse method with the raw message: `const email = await PostalMime.parse(rawMessage)`. The returned object carries the headers, recipients and attachments, and the documentation shows using util.inspect in Node.js to print the nested structure readably.

### Which runtimes does postal-mime support?

It works in Node.js, browsers, Web Workers and serverless environments such as Cloudflare Email Workers. The package needs the TextDecoder, Blob and ReadableStream globals, so the practical floor is Node.js 20 or newer, any modern browser, Deno, Bun or Cloudflare Workers.

### What changed when I upgrade postal-mime from version 3 to version 4?

Version 4 is a TypeScript rewrite and the parsed output is unchanged, but the browser deep import moved to dist/esm/postal-mime.js, Node.js 20 is now required, Attachment.disposition is typed as nullable, stream input is typed as ReadableStream of Uint8Array, addressParser no longer reads an undocumented _depth option, and CommonJS TypeScript projects need import equals require when esModuleInterop and allowSyntheticDefaultImports are off.

### Does postal-mime have any dependencies?

No. The library is written in TypeScript with zero external dependencies, and it is published as a dual package with an ES module build and a CommonJS build compiled from the same source, each shipping its own type declarations.

### How does postal-mime protect against malicious messages?

The library has built-in security limits against deeply nested or very wide messages, oversized headers and runaway nested message parsing. It also follows the RFC 2822 and 5322 email standards rather than accommodating vendor specific variations.

## Sources

- [Issues](https://github.com/postalsys/postal-mime/issues)
- [postalsys/postal-mime on GitHub](https://github.com/postalsys/postal-mime)
- [Project website](https://postal-mime.postalsys.com)
- [README](https://github.com/postalsys/postal-mime/blob/master/README.md)
- [Releases](https://github.com/postalsys/postal-mime/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/postalsys-postal-mime
