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
textandhtmlbodies, plus anattachmentsarray 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,${