mrkeyoor.com_
Tue 22 Sept 18:51 UTC
npmUtilsupdated 22 Sept 2026

postal-mime review

postal-mime 3.0.0 reads a complete RFC 822 message and returns decoded headers, addresses, bodies, threading fields, and attachment bytes. The same parser runs in Node, browsers, Workers, and serverless runtimes. Version 3 changes header semantics: folded headers retain folding whitespace, the first duplicate wins for single-value fields, and recipient arrays follow document order. It also maps non-WHATWG charset labels instead of treating all of them as Windows-1252. Parsing supplies structure only; it does not authenticate senders, sanitize HTML, or scan files.

Verdict

postal-mime is the practical choice when identical parsing code must run in Node and edge or browser runtimes. It stops at MIME decoding, so production ingestion still needs total-size limits, authentication, HTML cleaning, and file scanning.

We installed it

Lab card: what happened when we installed postal-mimeScreenshot of postal-mime documentation
Install✓ · 0.6s2 packages on disk · 1 MB
ImportESM import works · require() works · ESM package with exports map
Browser22.2 KBgzipped (71.9 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does postal-mime install cleanly?

Yes. In a fresh container with an empty cache, npm install postal-mime finished in 0.6s, leaving 2 packages and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does postal-mime add to a browser bundle?

22.2 KB gzipped (71.9 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does postal-mime work with both ESM and CommonJS?

Yes. Both import 'postal-mime' and require('postal-mime') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does postal-mime include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

postal-mime or mailparser: which should you use?

mailparser: Use Nodemailer's parser when a Node-only service wants its stream API and established mail tooling. postal-mime is the practical choice when identical parsing code must run in Node and edge or browser runtimes.

When should you not use postal-mime?

You need DKIM, SPF, DMARC, spam, or malware decisions. Parsed headers are data, not proof that a sender or attachment is safe.

API stability3/5The main `PostalMime.parse` call, utility exports, and typed result shape remain compact, and the package explicitly supports ESM and CommonJS. Version 3 intentionally changes observable output: folded header whitespace is preserved, the first duplicate populates single-value properties, and recipient fields stay in document order. Those are standards-oriented corrections, but they can break snapshots, deduplication logic, or code that depended on the final duplicate, so this major deserves fixture-based migration tests.
Docs5/5The README and documentation site list every accepted input type, import style, parsed property, attachment encoding, address union, and Cloudflare entry point. Security guidance gives exact option defaults, validation rules, aggregate-header behavior, breadth limits, and the scanner blind spot created by `rfc822DepthExceeded`. The changelog also states version 3's duplicate and ordering changes in direct terms. Few parser READMEs explain both the configured limits and what those limits fail to cover this clearly.
Maintenance5/5Version 3.0.0 was published on 2026-08-11, and GitHub records the repository push at the same time with 544 stars, zero open issues and pull requests, and no archive flag. The release fixed nonstandard charset labels and RFC 5322 header unfolding after a run of 2.x work on recursion bounds, MIME depth, header budgets, address parsing, type accuracy, and CommonJS interop. Recent fixes address hostile and malformed mail cases rather than cosmetic churn.
Ecosystem4/5npm counted 7,736,470 downloads in the latest completed week. One package supplies browser, Web Worker, Node, serverless, Cloudflare Email Worker, ESM, CommonJS, and TypeScript use without runtime dependencies. That portability is its main ecosystem advantage. Transport, IMAP access, message composition, DKIM verification, spam scoring, HTML cleaning, and file scanning live elsewhere, so adopting postal-mime does not provide an end-to-end mail pipeline.

Use it if

  • Raw email reaches Node, a browser, a Web Worker, or a Cloudflare Email Worker and one parser must serve every runtime.
  • You need decoded mailbox groups, nested MIME parts, inline content IDs, attachment bytes, and original header order.
  • Untrusted messages need explicit header, MIME depth, and embedded-message recursion limits before downstream inspection.
  • A dependency-free parser with bundled TypeScript definitions is preferable to a Node mail stack.
Skip it if

Setup reality

We installed postal-mime 3.0.0 in a fresh unprivileged Node 22 Bookworm container with no cache. npm finished in 0.6 seconds and left two packages using 1 MB. The package itself has no direct or peer dependencies and is 380 KB unpacked. npm audit found zero known vulnerabilities. The license is MIT-0, and TypeScript declarations are bundled.

The package declares ESM and uses an exports map, with a generated CommonJS target for require(). Both require and ESM import worked in our test. Importing the full namespace into esbuild produced 71.9 KB minified and 22.2 KB gzipped. The parser accepts strings, byte arrays, Blob, Buffer, ArrayBuffer, and ReadableStream, but its final Email object materializes attachment data. Reject oversized input before parsing even when the source arrives as a stream.

Three parser options cap MIME nesting, aggregate header bytes, and inline message/rfc822 depth. Defaults are 256 levels, 2097152 header bytes, and 10 embedded messages. These checks do not limit multipart breadth or total raw bytes. When the embedded-message limit is crossed, the parser leaves those bytes in an attachment and sets rfc822DepthExceeded. A scanner must reject that case or reparse it under one global work budget.

Attachment content defaults to ArrayBuffer; base64 grows the representation, and UTF-8 is wrong for arbitrary binary files. Address values may be mailboxes or groups, so TypeScript callers need a guard before reading .address. Version 3 also changes duplicate-header selection and recipient order. Snapshot your real mail fixtures before upgrading from v2, then keep HTML cleaning, remote-image policy, sender authentication, and attachment scanning as separate stages.

Patterns

Parse a small raw message parse-email-string

import PostalMime from 'postal-mime'

const email = await PostalMime.parse(`From: Ada <ada@example.com>
Subject: Hello
Content-Type: text/plain; charset=utf-8

Message body`)

console.log(email.subject, email.text)

Treat every returned field as sender-controlled, including display names and HTML.

Read an EML file in Node parse-node-buffer

import { readFile } from 'node:fs/promises'
import PostalMime from 'postal-mime'

const raw = await readFile('message.eml')
const email = await PostalMime.parse(raw)

Check file size before reading an upload into memory; parsed attachments also occupy memory.

Accept a web ReadableStream parse-readable-stream

import PostalMime from 'postal-mime'

const response = await fetch(messageUrl)
if (!response.body) throw new Error('missing body')
const email = await PostalMime.parse(response.body)

A streamed input does not make the returned attachments streaming. Enforce a response-size limit at the fetch boundary.

Return attachment bytes as base64 choose-attachment-encoding

const email = await PostalMime.parse(rawMessage, {
  attachmentEncoding: 'base64',
})

for (const file of email.attachments) {
  console.log(file.filename, file.encoding)
}

Base64 takes more memory than the default ArrayBuffer. Choose it only when the next API requires text.

Tighten parser limits limit-untrusted-message

const email = await PostalMime.parse(rawMessage, {
  maxNestingDepth: 64,
  maxHeadersSize: 512 * 1024,
  maxRfc822NestingDepth: 5,
})

These settings do not cap raw message bytes or the number of sibling MIME parts.

Reject content below the recursion limit handle-hidden-rfc822

for (const file of email.attachments) {
  if (file.rfc822DepthExceeded) {
    throw new Error('nested message exceeds inspection budget')
  }
}

The hidden nested content is absent from the outer text, HTML, and expanded attachment list.

Distinguish groups from mailboxes narrow-address-union

import type { Address, Mailbox } from 'postal-mime'

function isMailbox(value: Address): value is Mailbox {
  return !('group' in value) || value.group === undefined
}

if (email.from && isMailbox(email.from)) {
  console.log(email.from.address)
}

RFC address groups do not have the same shape as an individual mailbox.

Flatten an address header parse-address-list

import { addressParser } from 'postal-mime'

const recipients = addressParser(
  'Team: Ada <ada@example.com>, Lin <lin@example.com>;',
  { flatten: true },
)

Flattening is convenient for a recipient list but discards the original group boundary.

Decode one encoded header value decode-header-words

import { decodeWords } from 'postal-mime'

const value = decodeWords(
  'Hello, =?utf-8?B?44Ko44Od44K544Kr44O844OJ?=',
)

Full message parsing already decodes promoted fields such as subject and display names.

Read repeated raw headers in order inspect-duplicate-headers

const received = email.headers
  .filter(header => header.key === 'received')
  .map(header => header.value)

Version 3 preserves document order. For single-value properties such as subject, the first duplicate wins.

Parse a Cloudflare Email Worker message parse-cloudflare-email

import PostalMime from 'postal-mime'

export default {
  async email(message, env, ctx) {
    const email = await PostalMime.parse(message.raw)
    ctx.waitUntil(env.QUEUE.send({
      subject: email.subject,
      text: email.text,
    }))
  },
}

Avoid logging full bodies or attachment bytes, and enforce routing-layer size policy.

Load the CommonJS export use-commonjs

const PostalMime = require('postal-mime')
const { addressParser } = require('postal-mime')

const email = await PostalMime.parse(rawMessage)

Version 3's exports map selects the generated CommonJS file for require callers.

Alternatives

PackageRegistryPick it when
mailparsernpmUse Nodemailer's parser when a Node-only service wants its stream API and established mail tooling.
emailjs-mime-parsernpmUse it for a lower-level MIME tree in browser-oriented email applications.
mailsplitnpmUse it when MIME nodes must be inspected or rewritten as an object stream instead of collected into one result.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.