mrkeyoor.com_
Sat 08 Aug 17:43 UTC
npmUtilsupdated 08 Aug 2026

postal-mime

postal-mime parses a raw RFC 822 email into a JavaScript object containing decoded headers, addresses, text and HTML bodies, threading identifiers, and attachment bytes. The same zero-dependency package runs in Node.js, browsers, Web Workers, serverless functions, and Cloudflare Email Workers, with ESM, CommonJS, and TypeScript declarations. It parses message structure only: it does not sanitize HTML, verify sender authenticity, scan malware, fetch mail, or send a reply.

Verdict

postal-mime is an excellent small parser when the same code must run across browser, Worker, and Node environments. Treat its output as untrusted structured input and add independent size limits, HTML sanitization, authentication checks, and attachment scanning.

API stability4/5The central `PostalMime.parse` contract, address parser, encoded-word decoder, dual module support, and typed result model are compact and easy to pin. Version 2.7.6 adds detailed, validated security-limit behavior that callers must understand, especially `rfc822DepthExceeded`. The 2.x API is coherent, but email edge-case fixes can legitimately alter parsed output, so snapshot tests are still warranted.
Docs5/5The README and linked site document every accepted input form, ESM and CommonJS imports, Cloudflare usage, exported TypeScript unions, complete result properties, utility functions, attachment encodings, exact default security limits, invalid-option behavior, breadth limits, and the nested-message scanner bypass. That last warning is unusually specific and gives corrective code rather than vague security language.
Maintenance5/5Version 2.7.6 was published on August 7, 2026, the repository was pushed the same day, and GitHub reports 1 open issue or pull request with 544 stars. The package is maintained by Postalsys, whose email products exercise the same MIME domain. Current code, release, documentation, and low open-work signals all point to active ownership despite the modest star count.
Ecosystem4/5postal-mime recorded 7,054,812 downloads in the latest measured week and deliberately supports browsers, Web Workers, Node.js, serverless runtimes, Cloudflare Email Workers, ESM, CommonJS, and TypeScript. Zero dependencies reduce integration conflicts. The surrounding feature ecosystem is narrower than Nodemailer's because authentication, transport, sanitization, and security scanning remain separate tools.

Use it if

  • You receive raw email in a browser, Worker, serverless function, or Node service and need one cross-runtime parser
  • You need typed access to nested MIME parts, address groups, inline images, and attachments
  • A zero-runtime-dependency parser is preferable to a Node-only mail stack
  • You need explicit nesting and header-size controls for untrusted messages
Skip it if

Setup reality

`npm install postal-mime` has no runtime dependencies. Node ESM imports the package root, CommonJS can `require('postal-mime')`, and browser examples import `src/postal-mime.js`; real browser projects should let a bundler resolve that source rather than expose `node_modules` publicly. `PostalMime.parse` is asynchronous and accepts a string, Buffer, Blob, ArrayBuffer, Uint8Array, or ReadableStream, but the returned object materializes bodies and attachment content, so cap the raw message size before parsing. Attachment content defaults to ArrayBuffer; choose `base64` or `utf8` only when the next boundary needs it because either can increase memory or corrupt arbitrary binary data. Security limits are present and validated as non-negative integers: MIME nesting defaults to 256, total header bytes per parser to 2,097,152, and inline message/rfc822 depth to 10. Those controls limit depth, not breadth, and nested RFC 822 messages each get a parser, so a wide multipart message can still be expensive. When `rfc822DepthExceeded` is true, deeper content was intentionally left opaque; a scanner must reject it or reparse under its own global work budget. Address fields use a union of mailbox and group shapes, requiring a type guard before reading `.address`. Finally, parsing does not make hostile HTML safe, authenticate the sender, or validate attachment content, so those production steps remain separate.

Patterns

Parse a raw email stringparse-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);

The parser returns untrusted content; do not render HTML or trust address strings without downstream policy checks.

Parse an email file in Node.jsparse-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);

Reading the whole file and parsing attachments both consume memory; reject oversized files before `readFile` in an upload path.

Return attachment content as base64choose-attachment-encoding

import PostalMime from 'postal-mime';

const email = await PostalMime.parse(rawMessage, {
  attachmentEncoding: 'base64',
});
for (const attachment of email.attachments) {
  console.log(attachment.filename, attachment.encoding, attachment.content);
}

Base64 increases the representation size; the default ArrayBuffer is better when you will write or inspect binary bytes directly.

Set parser depth and header limitslimit-untrusted-message

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

These values limit nesting and headers, not raw message bytes or multipart breadth; enforce a separate total input-size limit.

Handle messages hidden below the RFC 822 depth limitinspect-nested-attachments

for (const attachment of email.attachments) {
  if (attachment.rfc822DepthExceeded) {
    // Either reject it or reparse under a strict global work budget.
    const nested = await PostalMime.parse(attachment.content, {
      maxRfc822NestingDepth: 0,
    });
    await scan(nested);
  }
}

The README warns that deeper content is absent from the outer text, HTML, and attachment lists, so scanners must check this flag.

Distinguish a mailbox from an address groupnarrow-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);
}

Address fields are unions; reading `.address` without narrowing fails for RFC address groups.

Parse and flatten an address headerparse-address-list

import { addressParser } from 'postal-mime';

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

Flattening discards group structure, which is convenient for delivery lists but loses the original semantic grouping.

Decode MIME encoded wordsdecode-header-words

import { decodeWords } from 'postal-mime';

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

Use this utility for an isolated encoded header value; full message parsing already decodes standard structured fields.

Read a header not promoted to a propertyfind-raw-header

const listId = email.headers.find(({ key }) => key === 'list-id')?.value;

Header keys in the returned array are lowercase, and repeated headers remain separate entries.

Parse a Cloudflare Email Worker messageparse-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 }));
  },
};

Bound the accepted message size at the email-routing boundary and avoid logging full bodies or attachments.

Load the CommonJS builduse-commonjs

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

const email = await PostalMime.parse(rawMessage);

The package provides a generated CommonJS build as well as ESM, so no dynamic-import workaround is required.

Sanitize HTML before browser renderingsanitize-html-body

import DOMPurify from 'dompurify';

const safeHtml = DOMPurify.sanitize(email.html || '', {
  FORBID_TAGS: ['style'],
  FORBID_ATTR: ['srcset'],
});
container.innerHTML = safeHtml;

PostalMime parses HTML but does not sanitize it; also proxy or block remote image URLs if sender-controlled tracking is a concern.

Alternatives

PackageRegistryPick it when
mailparsernpmA Node-only service wants Nodemailer's established parser and Node stream integration
emailjs-mime-parsernpmYou want a lower-level MIME parser for browser-oriented email tooling
mailsplitnpmYou need streaming MIME node rewriting or inspection rather than one fully materialized result