Email to JSON: MailParse vs Mailgun Inbound Routing

Compare MailParse and Mailgun Inbound Routing for Email to JSON. Features, performance, and developer experience.

Introduction

Email is still the most universal interface for users to send data into applications, but raw messages arrive as MIME with complex multipart structures, character sets, and edge cases. Developers do not want to wrangle MIME in application code. They want a reliable email-to-json pipeline that converts arbitrary email messages into clean, structured JSON with predictable fields and consistent encodings. That conversion is what powers ticketing systems, SaaS automations, and workflow triggers.

This article compares two approaches to email to JSON: a dedicated parsing workflow in MailParse, and Mailgun Inbound Routing. We focus on how each platform transforms inbound MIME into developer-friendly structures, what the webhook payloads look like, how attachments and inline images are handled, and how performance and reliability hold up at scale. If you are deciding between a specialized email parsing API and a routing-centric product, the differences below will help you choose the right fit.

Related reading for planning your inbound pipeline:

How MailParse Handles Email to JSON

MailParse is built to receive raw MIME, normalize it, and deliver a single, structured JSON document to your backend. You can provision instant addresses programmatically, receive inbound messages immediately, and process them via JSON webhooks or REST polling.

Webhook delivery format

Inbound delivery uses a JSON POST with UTF-8 normalization and a stable schema. The metadata includes message identity, the full addressing envelope, normalized bodies, headers, attachment descriptors, and links for raw MIME and attachment streaming. A typical payload looks like this:

{
  "event_id": "evt_01J2Y8JQ9Y0B9A6Q3X3F3WZ0G4",
  "received_at": "2026-04-24T12:45:33.512Z",
  "message_id": "<CA+abc123@example.com>",
  "subject": "Re: April invoice",
  "from": { "name": "Alice", "address": "alice@example.com" },
  "to": [
    { "name": "Billing", "address": "ap@yourapp.dev" }
  ],
  "cc": [],
  "bcc": [],
  "reply_to": null,
  "headers": {
    "In-Reply-To": "<CA+xyz@example.com>",
    "References": "<CA+xyz@example.com>"
  },
  "text": "Hi team,\nPlease find the invoice attached.\n",
  "html": "<p>Hi team,</p><p>Please find the invoice attached.</p>",
  "attachments": [
    {
      "id": "att_01J2Y8KPZJ2XK6S3M3E5",
      "filename": "invoice.pdf",
      "content_type": "application/pdf",
      "size": 183422,
      "sha256": "3b7f...e1c9",
      "is_inline": false,
      "content_id": null,
      "download_url": "https://api.example.com/attachments/att_01J2Y8KP.../stream"
    }
  ],
  "inline_attachments": [
    {
      "id": "att_01J2Y8KQ8W2C7R2Q9P4G",
      "filename": "logo.png",
      "content_type": "image/png",
      "size": 10243,
      "sha256": "9a23...a01e",
      "is_inline": true,
      "content_id": "image001.png@01D9",
      "download_url": "https://api.example.com/attachments/att_01J2Y8KQ.../stream"
    }
  ],
  "cid_map": {
    "image001.png@01D9": "att_01J2Y8KQ8W2C7R2Q9P4G"
  },
  "verdicts": { "spam": false, "virus": false },
  "authentication": {
    "dkim": { "verified": true },
    "spf": { "result": "pass" }
  },
  "raw_mime_url": "https://api.example.com/messages/evt_01J2Y8JQ.../mime"
}

Headers are preserved, bodies are normalized to UTF-8, and attachments are represented as metadata with streaming URLs. Inline images include their Content-ID, which you can map to <img src="cid:..."> references inside HTML.

Security and idempotency

  • Each webhook includes an HMAC signature header that covers the request body, which your handler can verify before processing.
  • event_id is unique and stable. Use it as a natural idempotency key so duplicate deliveries are safe.
  • If your endpoint returns a non-2xx code, automatic retries occur with backoff. You can also replay events via the REST API.

Developer ergonomics

  • JSON out of the box, no multipart parsing needed.
  • Access raw MIME and attachments via REST if you need to re-run parsing or feed a downstream tool.
  • Optional base64 attachment embedding for small files if your workflow prefers single-payload delivery.

How Mailgun Inbound Routing Handles Email to JSON

Mailgun Inbound Routing focuses on message routing with flexible pattern matching and actions. A common configuration forwards inbound messages to your webhook. By default, the delivery format is multipart/form-data, not a JSON envelope. Mailgun posts fields like recipient, sender, from, subject, body-plain, body-html, and signature fields (timestamp, token, signature) for verification. Attachments arrive as uploaded files in the multipart request. To obtain the exact raw MIME, you typically add the store() action in your route and then fetch the stored message via the provided URL.

This model is powerful and flexible, but email-to-json is an extra step you add in your code. You must parse multipart/form-data, lift fields into a schema you control, and decide how to represent attachments and inline images. Many teams also reconstruct a CID to attachment map by scanning the HTML for cid: references and matching them to uploaded files.

Security-wise, you verify the HMAC signature generated with your API key based on the provided timestamp and token. Mailgun retries failed webhooks and signs every request. Size constraints depend on your plan and configuration, and attachments are subject to typical inbound limits. For high-volume pipelines, some teams note variability in delivery timing and webhooks that require careful retry handling.

Side-by-Side Comparison

Capability MailParse Mailgun Inbound Routing
Default delivery format Single JSON webhook payload multipart/form-data with fields and file uploads
Raw MIME access Direct raw_mime_url per event Requires store() action, fetch via storage URL
Attachment representation Attachment metadata with streaming URLs, optional base64 embedding Multipart file uploads in the webhook request body
Inline image CID mapping Explicit cid_map plus is_inline flags Reconstruct by correlating HTML cid: references with files
Charset and encoding normalization Bodies normalized to UTF-8, decoded headers preserved Text fields provided as-is, normalization handled by your code
Nested multipart handling Flattens to coherent text, html, and attachment arrays Original parts available via raw MIME fetch when using store()
Webhook verification HMAC signature over JSON body HMAC signature using timestamp and token
Retries and idempotency Automatic retries with unique event_id for dedupe Automatic retries, build your own idempotency key
Developer effort for email-to-json Minimal - read JSON schema Moderate - parse multipart, map fields, optionally fetch MIME
Cost scaling Predictable per-message parsing Often higher at volume due to routing plus storage usage

Code Examples

JSON webhook handler example

Below is a minimal Node.js endpoint that accepts a JSON webhook, verifies the HMAC signature, and stores the normalized email in your database. The handler assumes an X-Signature header with HMAC SHA-256 of the raw request body using your shared secret.

const express = require('express');
const crypto = require('crypto');

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

function verifySignature(req, res, next) {
  const signature = req.get('X-Signature');
  if (!signature) return res.sendStatus(400);
  const secret = process.env.WEBHOOK_SECRET;
  const hmac = crypto.createHmac('sha256', secret).update(JSON.stringify(req.body)).digest('hex');
  if (crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(signature))) {
    return next();
  }
  return res.sendStatus(401);
}

app.post('/webhooks/inbound', verifySignature, async (req, res) => {
  const msg = req.body;

  // Use event_id as idempotency key
  await saveMessage({
    id: msg.event_id,
    subject: msg.subject || '',
    from: msg.from.address,
    to: msg.to.map(t => t.address),
    text: msg.text || '',
    html: msg.html || '',
    attachments: msg.attachments,
    rawMimeUrl: msg.raw_mime_url
  });

  // Example: resolve inline images in HTML for display
  // Replace cid: links with proxied download URLs using cid_map
  // left as an exercise for your rendering layer

  res.sendStatus(204);
});

app.listen(3000);

Mailgun Inbound Routing handler that builds JSON

When using mailgun-inbound-routing, your endpoint receives multipart/form-data. You parse fields and files, verify the signature, then assemble your email-to-json object. Here is an example in Node.js with multer and HMAC verification compatible with Mailgun's signature model:

const express = require('express');
const multer = require('multer');
const crypto = require('crypto');

const app = express();
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } });

function verifyMailgunSignature(req) {
  const { timestamp, token, signature } = req.body;
  const apiKey = process.env.MAILGUN_API_KEY;
  const digest = crypto.createHmac('sha256', apiKey).update(timestamp + token).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature));
}

app.post('/mailgun/inbound', upload.any(), async (req, res) => {
  if (!verifyMailgunSignature(req)) return res.sendStatus(401);

  // Extract common fields
  const data = req.body;
  const files = req.files || [];

  // Build a normalized JSON envelope
  const email = {
    message_id: data['Message-Id'] || null,
    subject: data.subject || '',
    from: { name: null, address: data.sender || data.from || '' },
    to: (data.recipient || '').split(',').map(addr => ({ name: null, address: addr.trim() })).filter(x => x.address),
    text: data['body-plain'] || '',
    html: data['body-html'] || '',
    headers: {}, // optional - reconstruct from data or fetch raw MIME
    attachments: files.filter(f => f.fieldname.startsWith('attachment')).map(f => ({
      filename: f.originalname,
      content_type: f.mimetype,
      size: f.size,
      buffer: f.buffer // stream or persist to storage - do not keep in memory for large files
    }))
  };

  // Optional: if your route uses store(), fetch raw MIME and fill headers or CID mapping
  // const mimeUrl = data['message-url']; // depends on your routing action
  // const rawMime = await fetch(mimeUrl, { headers: { Authorization: 'api:key-...' } }).then(r => r.text());

  await saveMessage(email);
  res.sendStatus(204);
});

app.listen(3000);

In this model, you own the JSON schema and explicitly control attachment storage and CID mapping. For large attachments, stream them to durable storage instead of buffering in memory.

Performance and Reliability

Parsing and normalization speed

Converting MIME to structured JSON involves decoding charsets, collapsing multipart alternatives, and extracting attachments. A direct JSON webhook avoids extra multipart parsing in your application and typically reduces CPU usage. This is noticeable under load when processing thousands of inbound messages per minute. With a multipart form, you pay an additional parse step in your backend and must handle memory limits for large files.

Edge cases handled by the parser

  • Nested multiparts - for example multipart/mixed with an inner multipart/related that contains HTML and inline images. A native parser should collapse bodies sensibly and capture a reliable cid_map.
  • Outlook TNEF (winmail.dat) - a robust pipeline detects TNEF, surfaces extracted attachments, and exposes the original blob if needed.
  • Odd encodings - encoded words in headers, unusual charsets like windows-1252, and malformed MIME boundary behavior. Normalizing everything to UTF-8 avoids downstream surprises.
  • Calendar invites and DSNs - iCalendar parts and bounce notifications should be preserved with content types and accessible as attachments or dedicated nodes.
  • S/MIME or PGP - surface application/pkcs7-mime or application/pgp-encrypted parts as attachments and set clear flags so downstream handlers can decide whether to decrypt.

Webhook delivery characteristics

Both platforms implement signed webhooks and automatic retries. A JSON-first delivery reduces the chance of handler failures caused by multipart parsing or memory spikes, so end-to-end success rates often improve with simpler handlers. Some teams have reported inconsistent webhook delivery timings with high-volume Mailgun's routes, especially when combining multiple actions like store() and forward. In any case, a resilient design includes:

  • Idempotent processing keyed by a unique event or content hash.
  • Fast acknowledgement with 2xx responses, then asynchronous downstream processing.
  • Replay tooling to re-deliver events after outages.
  • Persistent logging of signatures and request metadata for audit.

For operational checklists that help keep inbound pipelines reliable, see the Email Deliverability Checklist for SaaS Platforms.

Verdict: Which Is Better for Email to JSON?

If your goal is to convert arbitrary inbound messages into a consistent email-to-json structure with minimal code, MailParse provides the fastest path. You get ready-to-use JSON, explicit CID mappings, normalized encodings, and streaming URLs for raw MIME and attachments. Your handler becomes a small, secure JSON consumer rather than a multipart and MIME parser.

If you already rely on mailgun inbound routing and are comfortable writing your own adaptation layer, it remains a capable choice. You will parse multipart requests, verify Mailgun's signature, possibly fetch stored MIME for fidelity, and construct your own schema. This approach offers flexibility and tight control but adds development and maintenance overhead. At very high volumes, the JSON-first approach usually reduces complexity and cost-to-serve.

FAQ

What is email-to-json and why is MIME parsing tricky?

Email-to-json is the process of converting raw MIME messages into structured JSON fields that application code can consume directly. MIME can nest parts, embed inline images via Content-ID, include non-UTF-8 charsets, and attach proprietary blobs like TNEF. A good parser normalizes all of this and provides predictable keys for bodies, headers, and attachments.

Can I access the original raw MIME as well as JSON?

Yes. A robust pipeline exposes both. JSON is perfect for application logic, while raw MIME is essential for audits, training spam filters, or feeding downstream processors that require the original wire format. With mailgun-inbound-routing, use the store() action to get a URL for the stored message.

How should I secure inbound webhooks?

Always verify HMAC signatures, use HTTPS, and restrict by IP or mTLS if possible. A common pattern is to compute a SHA-256 HMAC of the payload or token data and compare with a request header using constant-time equality. Also implement per-event idempotency to handle automatic retries safely.

What is the best way to handle attachments at scale?

Stream attachments directly to object storage and keep only metadata in your primary database. Avoid buffering large files in memory in your webhook handler. Store the SHA-256 digest for deduplication and auditing. For inline images, maintain a CID map so your renderer can rewrite cid: URLs to signed download links.

How do I render HTML with inline images safely?

Sanitize HTML, then replace cid: references with secure, time-limited URLs for the corresponding attachments. Never serve attachments directly from raw storage without authorization checks. For clients that only accept plain text, fall back to the normalized text body provided by the parser.

Ready to get started?

Start parsing inbound emails with MailParse today.

Get Started Free