Introduction
MIME parsing sits at the heart of inbound email processing. If your application relies on replies, attachments, or machine-readable signals coming in via email, then accurate decoding of MIME-encoded messages determines whether features work reliably or fail under edge cases. Modern email is rarely a single plain text body. Real-world messages are multipart constructs that can include HTML and plain alternatives, inline images referenced by cid:, nested multipart/related for rich content, calendar invites, signed content, and attachments using different Content-Transfer-Encoding values and charsets. A MIME parser must normalize all of that into a format your code can trust.
This article compares MIME parsing capabilities between a dedicated inbound email parsing service and Twilio SendGrid's inbound email webhook - often referred to as sendgrid inbound parse or sendgrid-inbound-parse. The goal is to help developers pick the right approach for decoding MIME content into structured JSON and delivering it to production systems with minimal friction.
For product teams planning inbound email features, it also helps to think beyond the parser itself. Once messages are parsed, you still need reliable delivery to your app, resilient processing, and observability. For broader system planning, see these resources:
- Top Inbound Email Processing Ideas for SaaS Platforms
- Email Infrastructure Checklist for SaaS Platforms
- Email Deliverability Checklist for SaaS Platforms
How MailParse Handles MIME Parsing
MailParse normalizes raw RFC 5322 email into a fully decoded, structured JSON document that preserves hierarchy and metadata while removing MIME-specific complexities. The service receives email at instant addresses, parses the MIME tree, decodes bodies and filenames, maps inline images to cid: references, and delivers a compact JSON payload via webhook or makes it available via a REST polling API.
Core decoding and normalization
- Headers normalization - decodes RFC 2047 encoded words in
Subject,From,To, and custom headers to UTF-8, parses address fields into name and address components, preserves raw header lines for audit when needed. - Multipart support - fully traverses nested
multipart/mixed,multipart/alternative,multipart/related, andmultipart/signed, returning a list of parts with parent-child relationships. - Transfer encodings - handles base64, quoted-printable, 7bit, 8bit, and binary bodies with streaming decode to avoid large in-memory buffers.
- Charsets - converts common and obscure charsets to UTF-8, including ISO-2022-JP, Shift_JIS, GB18030, KOI8-R, and Windows-1252.
- Filename parameters - decodes RFC 2231 encoded parameters such as
filename*and folded continuations across headers. - Inline content - maps
Content-IDvalues to stable attachment IDs and provides an inline mapping so you can rewritecid:references to signed download URLs if desired. - Special cases - passes through S/MIME and PGP parts without modification for application-level handling, extracts TNEF
winmail.datwhere present, and exposes calendar invites as separate parts.
Webhook payload structure
The webhook is a single JSON document that includes high-level message metadata, a normalized parts tree, and a flat attachments index for convenience:
{
"id": "msg_01HX9Y8Q3R8C7D9R9E6F4T1ZKJ",
"received_at": "2026-04-24T14:03:02.481Z",
"envelope": {
"mail_from": "bounce@example.com",
"rcpt_to": ["support@yourapp.example"],
"remote_ip": "203.0.113.24"
},
"headers": {
"subject": "Client report – Q1",
"from": [{"name": "Ada Lovelace", "address": "ada@example.org"}],
"to": [{"name": "Support", "address": "support@yourapp.example"}],
"message_id": "<abcd1234@example.org>",
"date": "Fri, 24 Apr 2026 16:02:57 +0000",
"raw": "Subject: =?utf-8?q?Client_report_=E2=80=93_Q1?=\r\n..."
},
"parts": [
{
"id": "part_1",
"mime_type": "multipart/alternative",
"children": ["part_1_1", "part_1_2"]
},
{
"id": "part_1_1",
"parent": "part_1",
"mime_type": "text/plain",
"charset": "utf-8",
"disposition": "inline",
"content": "Hello team,\nHere is the plain text.\n",
"size": 52
},
{
"id": "part_1_2",
"parent": "part_1",
"mime_type": "text/html",
"charset": "utf-8",
"disposition": "inline",
"content": "<p>Hello team,</p><p>See the <strong>report</strong>.<img src=\"cid:img1\" />",
"size": 123
},
{
"id": "part_2",
"mime_type": "image/png",
"disposition": "inline",
"content_id": "img1",
"filename": "chart.png",
"attachment_id": "att_9vczp8",
"size": 48213
},
{
"id": "part_3",
"mime_type": "application/pdf",
"disposition": "attachment",
"filename": "Q1-report.pdf",
"attachment_id": "att_k2f3ms",
"size": 910024
}
],
"attachments": [
{
"attachment_id": "att_9vczp8",
"filename": "chart.png",
"mime_type": "image/png",
"content_id": "img1",
"download_url": "https://files.example.com/att_9vczp8?sig=...",
"sha256": "2d711642b726b04401627ca9fbac32f5"
},
{
"attachment_id": "att_k2f3ms",
"filename": "Q1-report.pdf",
"mime_type": "application/pdf",
"download_url": "https://files.example.com/att_k2f3ms?sig=...",
"sha256": "e3b0c44298fc1c149afbf4c8996fb924"
}
],
"security": {
"dkim": [{"domain": "example.org", "result": "pass"}],
"spf": {"result": "pass"},
"dmarc": {"result": "pass"}
}
}
API delivery and security
- Webhook delivery - HMAC signature header and timestamp to protect against tampering and replay. Respond with 2xx quickly and process asynchronously.
- REST polling - idempotent retrieval by message ID, useful for staging or offline processing.
- Attachment storage - deduplicated by hash with short-lived signed URLs or direct bytes retrieval via API.
How SendGrid Inbound Parse Handles MIME Parsing
Twilio SendGrid's inbound parse is a webhook that posts incoming messages to your HTTP endpoint. Configuration requires a domain or subdomain where MX records point to SendGrid, then a route that forwards inbound mail for that domain to your parse URL. Once configured, the service sends a multipart/form-data POST containing convenience fields and attachments.
Payload content
- Fields include
to,from,subject,text,html,headers,cc, and spam indicators. Attachments arrive as file parts namedattachment1,attachment2, and so on. - By default, you get the text and HTML bodies decoded into strings. Nested multipart structures are not preserved as a tree. Only the top-level plain and HTML views are materialized.
- There is an option called "Post the raw, full MIME message". When enabled, a field named
emailcontains the raw RFC 5322 message so that your application can handle deep mime-parsing on its own.
Setup and constraints
- DNS - requires MX records for your chosen subdomain to point to
mx.sendgrid.net. You then configure a parse setting that maps that recipient domain to your endpoint URL. - Security - the inbound parse does not include a message signature. Twilio's recommendation is to validate source IPs, use HTTPS, and add your own authentication on the endpoint.
- Size and rate - messages larger than typical limits are rejected or truncated. As of most recent guidance, the POST payload is limited around tens of megabytes. Many teams set their own cap, for example 10 to 30 MB, to keep endpoints responsive.
For straightforward use cases where your app only needs the body text, an HTML version, and attachment bytes, the builtin fields work well. If your use case requires precise MIME tree traversal, inline CID resolution, non-UTF-8 charset handling, or filename parameter decoding, you will likely need to enable the raw field and integrate your own parser or an additional mime-parsing layer.
Side-by-Side Comparison
| Feature | MailParse | SendGrid Inbound Parse |
|---|---|---|
| Delivery format | JSON with full MIME tree and decoded parts | multipart/form-data with text, html, attachments, optional raw |
| Raw RFC 5322 access | Available via API or included on request | Available if "Post raw" is enabled as email field |
| Nested multipart preservation | Yes, parent-child IDs retained | No, only text and HTML materialized |
| Inline CID mapping | Provided with contentId-to-attachment mapping | Not provided by default, developer must correlate |
| RFC 2047 header decoding | Decoded to UTF-8 in JSON and raw available | Headers provided as a raw string |
| RFC 2231 filename decoding | Yes | Not decoded by default |
| Character set conversion | Converts to UTF-8 with charset metadata | Text fields are decoded, fidelity varies for complex charsets |
| Transfer-encoding handling | Base64, quoted-printable, 7bit, 8bit, binary | Text fields decoded, attachments are raw file parts |
TNEF (winmail.dat) extraction |
Supported | Not extracted automatically |
| S/MIME and PGP | Pass-through with parts exposed | Pass-through via raw field if enabled |
| Webhook security | HMAC signature and timestamp | No signature, rely on IP filtering and HTTPS |
| Attachments indexing | Stable attachment IDs, hashes, signed URLs | Files posted in request, naming by index only |
| Delivery options | Webhook and REST polling | Webhook only |
Code Examples
Consuming a JSON webhook with a full MIME tree
The snippet below demonstrates how to accept a JSON webhook that includes decoded parts, verify an HMAC signature, and persist attachments by ID. It avoids blocking the HTTP response by queueing work.
// Node.js - Express
import crypto from "node:crypto";
import express from "express";
const app = express();
app.use(express.json({ limit: "50mb" }));
function verifySignature(req, body) {
const signature = req.get("X-Signature");
const timestamp = req.get("X-Timestamp");
const secret = process.env.WEBHOOK_SECRET;
const payload = `${timestamp}.${body}`;
const expected = crypto.createHmac("sha256", secret).update(payload).digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"));
}
app.post("/inbound", (req, res) => {
const raw = JSON.stringify(req.body);
if (!verifySignature(req, raw)) {
return res.status(401).send("invalid signature");
}
// Acknowledge quickly
res.status(202).send("accepted");
const msg = req.body;
// Choose the best body, prefer HTML, fall back to text
const htmlPart = msg.parts.find(p => p.mime_type === "text/html");
const textPart = msg.parts.find(p => p.mime_type === "text/plain");
const body = htmlPart?.content ?? textPart?.content ?? "";
// Rewrite cid: links to signed download URLs
const inlineByCid = new Map(
msg.attachments
.filter(a => a.content_id)
.map(a => [a.content_id, a.download_url])
);
const rewritten = body.replace(/cid:([^">]+)/g, (_, cid) => inlineByCid.get(cid) ?? `cid:${cid}`);
// Persist message metadata and rendered body
queueSave({
messageId: msg.headers.message_id,
from: msg.headers.from,
subject: msg.headers.subject,
body: rewritten,
attachments: msg.attachments
});
});
app.listen(3000);
Handling SendGrid Inbound Parse
SendGrid posts multipart/form-data. The example below uses Express with multer to parse the payload. It reads text, html, and attachments. If the "Post raw" option is enabled, the email field contains the raw message that you can feed into a MIME library of your choice.
// Node.js - Express + multer
import express from "express";
import multer from "multer";
const app = express();
const upload = multer({ limits: { fileSize: 30 * 1024 * 1024 } });
app.post("/sendgrid-inbound", upload.any(), async (req, res) => {
// Acknowledge quickly
res.status(200).send("OK");
const fields = req.body;
const files = req.files; // attachments e.g. attachment1, attachment2
const subject = fields.subject || "";
const from = fields.from || "";
const html = fields.html || null;
const text = fields.text || null;
// Choose one body to render
const body = html ?? text ?? "";
// Optional raw MIME for advanced parsing
if (fields.email) {
// Use your MIME library here to traverse nested parts
// const parsed = parseMime(fields.email);
// ...
}
// Save attachments
for (const f of files) {
// f.originalname, f.mimetype, f.buffer
await saveAttachment(from, subject, f.originalname, f.mimetype, f.buffer);
}
await saveMessage({ from, subject, body });
});
app.listen(3000);
If you need nested parts or inline CID mapping with SendGrid's webhook, enable the raw message field and run a mime-parsing step inside your handler. Without that step, the webhook provides only the flattened views.
Performance and Reliability
Throughput and streaming
For high volume systems, the biggest performance win is to stream parse and avoid loading entire messages into memory. A decoder that processes headers and bodies as byte streams can handle large attachments and quoted-printable sections without spikes. It also helps to persist attachments out of band with content hashes to de-duplicate common files like logos and signatures.
With SendGrid inbound parse, your endpoint receives multipart data for each incoming message. Plan for memory limits in your framework, and set file size limits in multipart middleware. If you enable the raw MIME field, consider streaming to disk or directly into your parser.
Edge cases to test
- Incorrect or missing
Content-Typeboundaries that require tolerant scanning. - Double-encoded filenames and parameters, for example RFC 2047 encoded words inside RFC 2231 containers.
- Messages with mixed encodings across parts, for example ISO-2022-JP text and base64 HTML with emoji.
- TNEF attachments that hide the real files inside
winmail.dat. - Inline images referenced by
cid:that must be mapped back to stored attachments. - Calendar invites and
multipart/signedwhere the signature must remain intact even as bodies are decoded for display.
Operational guidance
- Return a 2xx quickly from your webhook, then queue background work. Slow responses can trigger retries and duplicates.
- Use idempotency keys like Message-ID plus a source hash to avoid duplicate processing during retries.
- Verify webhook authenticity. If your provider signs requests, validate HMAC and timestamps. For SendGrid inbound parse, consider IP allowlists and endpoint auth since request signing is not included.
- Keep attachment processing off the critical request path. Store bytes, compute hashes, and virus-scan asynchronously.
If your team is building a customer support or post-sales workflow around inbound email, ensure the rest of the stack is production-ready. The