MIME Parsing: MailParse vs Postmark Inbound

Compare MailParse and Postmark Inbound for MIME Parsing. Features, performance, and developer experience.

Why MIME Parsing Capability Matters

MIME parsing turns raw, MIME-encoded email messages into structured data you can trust. When your application relies on inbound email to create tickets, accept file uploads, process replies, or power automations, accurate decoding is more than a convenience. It is the difference between a stable pipeline and a constant stream of edge case bugs.

A robust MIME-parsing engine must do more than split out a text body and a handful of attachments. It should decode base64 and quoted-printable parts, normalize character sets, preserve header semantics, and represent nested multipart structures without losing detail. That is how you prevent misclassified inline images, broken reply chains, truncated content, and garbled non-ASCII text.

  • Preserve structure - multipart/mixed, multipart/alternative, related, and nested trees
  • Decode reliably - base64 and quoted-printable bodies, filenames with RFC 2231 and RFC 2047 encoding
  • Normalize text - charset handling across UTF-8, ISO-2022-JP, GB18030, and more
  • Resolve inline content - map cid references in HTML to attachment metadata
  • Keep headers intact - duplicates, folded lines, and long parameters

If you are planning broader email workflows, consider these resources for a stronger foundation: Top Inbound Email Processing Ideas for SaaS Platforms and Email Infrastructure Checklist for SaaS Platforms.

How MailParse Handles MIME Parsing

MailParse focuses on MIME parsing as a first-class capability. It ingests inbound email, decodes every MIME-encoded part, and outputs a structured JSON document that preserves the full tree while also giving you convenient fields for common use cases. You can receive this JSON via webhook, or you can fetch it later via REST polling when your application prefers pull-based integration.

Key behaviors:

  • Full MIME tree representation - each part includes content type, transfer encoding, charset, disposition, filename, size, and children
  • Decoded convenience fields - normalized text and html bodies, plus an attachments array and a map of inline Content-ID references
  • Header fidelity - duplicates preserved, parameters decoded, folding handled, and original case stored alongside normalized names
  • Character set normalization - text parts transcoded to UTF-8 while preserving original charset metadata
  • Flexible delivery - webhook with signature verification, or REST polling for message retrieval and reprocessing

A typical webhook payload includes both a flattened view and a complete MIME tree:

{
  "id": "evt_8f7c",
  "received_at": "2026-04-25T12:30:45Z",
  "envelope": {
    "mail_from": "alice@example.com",
    "rcpt_to": ["support@yourapp.test"],
    "remote_ip": "203.0.113.10"
  },
  "headers": [
    {"name": "From", "value": "Alice <alice@example.com>"},
    {"name": "To", "value": "Support <support@yourapp.test>"},
    {"name": "Subject", "value": "Report - Q2 Numbers"},
    {"name": "Content-Type", "value": "multipart/mixed; boundary=b1"}
  ],
  "subject": "Report - Q2 Numbers",
  "message_id": "<msgid@example.com>",
  "from": {"name": "Alice", "email": "alice@example.com"},
  "to": [{"name": "Support", "email": "support@yourapp.test"}],
  "text": "Hi team,\nPlease see the attached report.\n",
  "html": "<p>Hi team,</p><p>Please see the attached report.</p>",
  "attachments": [
    {
      "filename": "q2-report.pdf",
      "content_type": "application/pdf",
      "size": 182334,
      "disposition": "attachment",
      "checksum_sha256": "7a9e...",
      "download_url": "https://files.example.dev/a/evt_8f7c/att_1?token=...",
      "content_id": null
    },
    {
      "filename": "logo.png",
      "content_type": "image/png",
      "size": 8421,
      "disposition": "inline",
      "checksum_sha256": "29ba...",
      "download_url": "https://files.example.dev/a/evt_8f7c/att_2?token=...",
      "content_id": "logo@cid"
    }
  ],
  "inline_cid_map": {
    "cid:logo@cid": "https://files.example.dev/a/evt_8f7c/att_2?token=..."
  },
  "mime": {
    "content_type": "multipart/mixed",
    "boundary": "b1",
    "children": [
      {
        "content_type": "multipart/alternative",
        "boundary": "b2",
        "children": [
          {"content_type": "text/plain", "charset": "utf-8", "decoded_bytes": 64},
          {"content_type": "text/html", "charset": "utf-8", "decoded_bytes": 104}
        ]
      },
      {"content_type": "application/pdf", "disposition": "attachment", "filename": "q2-report.pdf"},
      {"content_type": "image/png", "disposition": "inline", "filename": "logo.png", "content_id": "logo@cid"}
    ]
  }
}

REST polling example:

# List recent inbound messages since a timestamp
curl -s -H "Authorization: Bearer <token>" \
  "https://api.example.dev/v1/inbound?since=2026-04-25T12:00:00Z&limit=100"

# Retrieve a message and stream an attachment
curl -s -H "Authorization: Bearer <token>" \
  "https://api.example.dev/v1/inbound/evt_8f7c"

curl -L -H "Authorization: Bearer <token>" \
  "https://files.example.dev/a/evt_8f7c/att_1?token=..." -o q2-report.pdf

How Postmark Inbound Handles MIME Parsing

Postmark Inbound focuses on webhook delivery. It processes an inbound message, then posts a JSON payload with common fields like TextBody, HtmlBody, Headers, and Attachments. The payload is convenient for many applications that only need a single text body, an HTML body, and attachment data. A structured MIME tree is not provided, and webhook delivery is the only supported integration mode for inbound email.

Typical Postmark Inbound payload:

{
  "From": "Alice <alice@example.com>",
  "FromName": "Alice",
  "FromFull": {"Email":"alice@example.com","Name":"Alice","MailboxHash":""},
  "To": "Support <support@yourapp.test>",
  "ToFull": [{"Email":"support@yourapp.test","Name":"Support","MailboxHash":""}],
  "Cc": "",
  "CcFull": [],
  "Bcc": "",
  "BccFull": [],
  "OriginalRecipient": "support@yourapp.test",
  "Subject": "Report - Q2 Numbers",
  "MessageID": "f0aa021e-1027-4874-9e4f-9573f5c0c6f2",
  "ReplyTo": "",
  "Date": "Sat, 25 Apr 2026 12:30:45 +0000",
  "MailboxHash": "",
  "TextBody": "Hi team,\nPlease see the attached report.\n",
  "HtmlBody": "<p>Hi team,</p><p>Please see the attached report.</p>",
  "Tag": "",
  "Headers": [
    {"Name":"Content-Type","Value":"multipart/mixed; boundary=b1"},
    {"Name":"In-Reply-To","Value":"<prev@example.com>"}
  ],
  "Attachments": [
    {
      "Name":"q2-report.pdf",
      "Content":"JVBERi0xLjQKJc...",
      "ContentType":"application/pdf",
      "ContentLength":182334,
      "ContentID": null,
      "ContentDisposition":"attachment"
    },
    {
      "Name":"logo.png",
      "Content":"iVBORw0KGgoAAA...",
      "ContentType":"image/png",
      "ContentLength":8421,
      "ContentID":"<logo@cid>",
      "ContentDisposition":"inline"
    }
  ]
}

Many teams will find this sufficient, especially when they are already using postmark's transactional email. The tradeoff is less visibility into nested multipart structure and no REST polling option for inbound retrieval or reprocessing. When you need to rebuild the MIME tree or introspect deep nesting, you will need to derive it from the flattened fields and attachment metadata.

Side-by-Side MIME Parsing Feature Comparison

Feature MailParse Postmark Inbound
Inbound delivery modes Webhook and REST polling Webhook only
MIME tree representation Full nested tree preserved in JSON Flattened bodies and attachments, no tree
Decoded convenience fields Text, HTML, attachments, inline CID map TextBody, HtmlBody, Attachments array
Charset normalization Transcodes text to UTF-8, preserves original metadata Text and HTML provided, charset details not exposed
Attachment delivery Stream via secure URLs or fetch via REST Base64 content embedded in payload
Headers Duplicate and folded headers preserved and decoded Headers available as Name and Value pairs
Inline image CID handling Explicit map of cid: to downloadable URLs ContentID provided, mapping left to the application
Reprocessing Fetch by ID via REST for replay and debugging Replay not provided via REST

Code Examples: MIME-parsing on Both Platforms

MailParse webhook payload and REST polling

Webhook handler with signature verification and inline CID resolution in Node.js:

import crypto from 'crypto';
import express from 'express';

const app = express();
app.use(express.json({ limit: '25mb' }));

function verifySignature(req, res, next) {
  const sig = req.header('X-Signature') || '';
  const secret = process.env.WEBHOOK_SECRET;
  const body = JSON.stringify(req.body);
  const hmac = crypto.createHmac('sha256', secret).update(body).digest('hex');
  if (crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(sig))) return next();
  return res.status(401).send('invalid signature');
}

app.post('/inbound', verifySignature, async (req, res) => {
  const msg = req.body;
  // Normalize HTML by replacing cid: URLs with secure download URLs
  let html = msg.html || '';
  for (const [cidUrl, downloadUrl] of Object.entries(msg.inline_cid_map || {})) {
    const safeCid = cidUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    html = html.replace(new RegExp(safeCid, 'g'), downloadUrl);
  }

  // Persist essential fields
  const record = {
    id: msg.id,
    subject: msg.subject,
    from: msg.from.email,
    text: msg.text,
    html,
    attachments: (msg.attachments || []).map(a => ({
      filename: a.filename,
      url: a.download_url,
      type: a.content_type,
      size: a.size
    }))
  };

  console.log('stored', record.id);
  res.status(200).send('ok');
});

app.listen(3000, () => console.log('listening on :3000'));

REST polling when your app cannot accept webhooks in all environments:

#!/usr/bin/env bash
set -euo pipefail

API="https://api.example.dev/v1"
AUTH="Authorization: Bearer ${TOKEN}"

# Poll for new messages
curl -s -H "$AUTH" "$API/inbound?since=$(date -u +%FT%TZ --date='-5 minutes')" | jq -r '.items[].id' | while read id; do
  echo "Processing $id"
  msg=$(curl -s -H "$AUTH" "$API/inbound/$id")
  echo "$msg" | jq -r '.subject, .from.email'
done

Postmark Inbound webhook handler

Express handler that reads the flattened bodies and attachments:

import express from 'express';
const app = express();
app.use(express.json({ limit: '25mb' }));

app.post('/postmark-inbound', (req, res) => {
  const p = req.body;

  const subject = p.Subject;
  const text = p.TextBody || '';
  const html = p.HtmlBody || '';
  const attachments = (p.Attachments || []).map(a => ({
    name: a.Name,
    cid: a.ContentID,
    type: a.ContentType,
    bytes: a.ContentLength,
    base64: a.Content
  }));

  // Example: replace cid: references in HTML using attachment ContentID
  let normalized = html;
  for (const att of attachments) {
    if (att.cid) {
      // ContentID may be wrapped with < >
      const cid = String(att.cid).replace(/^<|>$/g, '');
      const dataUrl = `data:${att.type};base64,${

Ready to get started?

Start parsing inbound emails with MailParse today.

Get Started Free