Email to JSON: MailParse vs Mandrill Inbound

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

Why Email to JSON Matters for Modern Applications

Email-to-JSON conversion sits at the boundary between the messy world of internet email and the predictable shape that application code expects. Raw email is MIME, full of multipart boundaries, encodings like quoted-printable and base64, embedded images with Content-ID references, and legacy charsets. Applications, by contrast, want a clean JSON document: who sent it, who received it, the subject, a reliable text body, an HTML body if present, attachments with metadata and content, plus envelope and authentication details. If your system ingests support tickets, order confirmations, vendor replies, or IoT status emails, this conversion determines how much code you write and how many edge cases you carry for years.

When choosing between providers for converting email to JSON, look closely at how they parse MIME, how they deliver structured JSON, and how predictable the schema is under load and under weird real-world email conditions. This article compares two approaches head to head: a developer-first parsing service and Mandrill Inbound from Mailchimp.

How MailParse Handles Email to JSON

This service focuses on MIME parsing first, delivery second. It gives you instant receiving addresses and routes inbound mail to either a webhook or a polling queue. The key idea is that parsing happens server-side, then your application receives a normalized JSON document that is stable across senders and clients. The payload emphasizes clarity and data types over vendor-specific event wrappers.

Parsing and normalization

  • MIME-aware extraction: multipart/alternative selection with both text and html preserved, content-transfer decoding for quoted-printable and base64, and safe fallback when one part is missing.
  • Header normalization: case-insensitive headers surfaced as canonical fields like message_id, in_reply_to, and references for threading.
  • Address normalization: parsed from, to, cc, and bcc arrays contain structured objects with name and address.
  • CID resolution: inline images are mapped so you can resolve cid: URLs to attachment objects including content_id and content_url.
  • Charset handling: content is transcoded to UTF-8 with a charset field for provenance when detection was required.

Delivery options are straightforward. Webhooks post a single message per request using application/json. Attachments can be delivered as presigned URLs to avoid large payloads, with the option to inline small files as base64 for batch jobs. A REST API is also available for polling if you cannot accept inbound webhooks, with explicit acknowledgement to mark messages processed.

For background on implementation patterns and standards, see Email Parsing API: A Complete Guide | MailParse and Webhook Integration: A Complete Guide | MailParse.

How Mandrill Inbound Handles Email to JSON

Mandrill Inbound is part of Mailchimp's transactional email offering. It accepts inbound mail at configured domains or routes and forwards JSON events to your webhook. Mandrill posts data using application/x-www-form-urlencoded with a mandrill_events field that contains a JSON array of events. Each inbound event includes a msg object with common fields and, optionally, raw_msg for the original RFC 5322 message. If you enable raw message forwarding, your code can re-parse MIME on your side when needed.

What the inbound event looks like

  • Event envelope: each POST can batch multiple events. You parse mandrill_events, then iterate events where event is "inbound".
  • Message fields: msg.text, msg.html, msg.subject, msg.from_email, msg.from_name, msg.to (array of address pairs), msg.headers, msg.attachments where each attachment is base64 with type and name.
  • Raw MIME: if enabled, msg.raw_msg includes the original email so you can run a custom parser, which is useful when you want complete control over MIME edge cases.
  • Security: requests include an X-Mandrill-Signature header that you verify using your webhook key and the concatenation of the request URL and form fields.

This design is flexible and mature. It does, however, couple inbound features to a Mailchimp account and the broader Mandrill configuration model. If your use case needs a narrow inbound-only setup or a single-purpose parser, that extra dependency might not be ideal.

Side-by-Side Comparison of Email-to-JSON Features

Capability Developer-first parser Mandrill Inbound
Webhook content type application/json, single message per POST application/x-www-form-urlencoded with mandrill_events array
JSON schema shape Message-centric, normalized fields for headers, bodies, and recipients Event-centric, msg object within each event
Attachments Presigned URLs by default, optional inline base64 for small files Inline base64 attachment objects under msg.attachments
Inline CID resolution Yes, provides mapping from cid: to attachment metadata Attachment data provided, CID mapping handled by application
Charset detection and UTF-8 normalization Built in, exposes original charset Common cases handled, raw MIME available for custom parsing
Raw MIME availability Downloadable source as needed Available as msg.raw_msg when enabled
Multipart logic Returns both text and html, with heuristics for malformed parts Returns msg.text and msg.html when provided by sender
Security and verification HMAC signed webhooks, verification helper snippets X-Mandrill-Signature HMAC verification against webhook key
Retries and idempotency Automatic retry with unique message IDs for deduplication Retry behavior managed by Mandrill, events include identifiers
Setup scope Inbound-only focused configuration Requires Mailchimp transactional setup and inbound routes

Code Examples: Email-to-JSON on Both Platforms

Webhook handler for MailParse

This example uses Node.js and Express to handle a JSON webhook that delivers one message per request. The payload is already normalized, so most code is about verifying signatures, handling attachments, and writing to storage.

import express from 'express';
import crypto from 'crypto';
import fetch from 'node-fetch';

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

function verifySignature(req, secret) {
  const signature = req.header('X-Signature') || '';
  const body = JSON.stringify(req.body);
  const expected = crypto.createHmac('sha256', secret).update(body).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

app.post('/webhooks/inbound', async (req, res) => {
  if (!verifySignature(req, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('invalid signature');
  }

  const msg = req.body; // normalized email-to-JSON document
  const record = {
    id: msg.id,
    timestamp: msg.timestamp,
    subject: msg.subject || '',
    from: msg.from,           // [{ name, address }]
    to: msg.to,               // [{ name, address }]
    cc: msg.cc || [],
    bcc: msg.bcc || [],
    headers: msg.headers,     // { headerName: value }
    text: msg.text || '',
    html: msg.html || '',
    attachments: []
  };

  // Stream or fetch attachments using presigned URLs
  if (Array.isArray(msg.attachments)) {
    for (const a of msg.attachments) {
      if (a.content_url) {
        const r = await fetch(a.content_url);
        // Example: stream directly to object storage
        // await r.body.pipe(fs.createWriteStream(`/data/${msg.id}-${a.filename}`));
        record.attachments.push({
          filename: a.filename,
          contentType: a.content_type,
          size: Number(a.size),
          contentId: a.content_id || null,
          storage: 'external'
        });
      } else if (a.content_base64) {
        const buf = Buffer.from(a.content_base64, 'base64');
        // Save or process in-memory if small
        record.attachments.push({
          filename: a.filename,
          contentType: a.content_type,
          size: buf.length,
          storage: 'inline'
        });
      }
    }
  }

  // Persist your normalized record
  console.log('stored message', record.id);
  res.status(200).send('ok');
});

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

If you need to poll instead of receiving webhooks, you can call the REST endpoint on a schedule to fetch ready messages, process them, then acknowledge by ID. This pattern works well in locked-down networks or during migrations.

Webhook handler for Mandrill Inbound

Mandrill posts form-encoded data. You will parse mandrill_events as JSON, verify the signature, then transform the msg object into your internal shape. Attachments are included as base64 in the payload.

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

const app = express();
app.use(express.urlencoded({ extended: true, limit: '25mb' }));

function verifyMandrillSignature(req, webhookKey) {
  // Build the signed data: request URL + sorted form fields and their values
  const url = process.env.PUBLIC_WEBHOOK_URL; // must match Mandrill configuration
  const params = [];
  for (const [k, v] of Object.entries(req.body)) {
    params.push([k, v]);
  }
  params.sort((a, b) => a[0].localeCompare(b[0]));
  const signedData = [url, ...params.map(([k, v]) => `${k}${v}`)].join('');
  const expected = crypto.createHmac('sha1', webhookKey).update(signedData).digest('base64');
  const signature = req.header('X-Mandrill-Signature') || '';
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

app.post('/webhooks/mandrill', (req, res) => {
  if (!verifyMandrillSignature(req, process.env.MANDRILL_WEBHOOK_KEY)) {
    return res.status(401).send('invalid signature');
  }
  let events;
  try {
    events = JSON.parse(req.body.mandrill_events);
  } catch {
    return res.status(400).send('bad events');
  }

  for (const ev of events) {
    if (ev.event !== 'inbound' || !ev.msg) continue;
    const m = ev.msg;

    // Transform Mandrill's shape to your normalized shape
    const record = {
      id: m._id || m.ts || Date.now().toString(),
      subject: m.subject || '',
      from: [{ name: m.from_name || '', address: m.from_email }],
      to: (m.to || []).map(pair => ({ name: pair[1] || '', address: pair[0] })),
      cc: [],
      bcc: [],
      headers: m.headers || {},
      text: m.text || '',
      html: m.html || '',
      attachments: []
    };

    if (m.attachments) {
      for (const [filename, meta] of Object.entries(m.attachments)) {
        const buf = Buffer.from(meta.content || '', 'base64');
        record.attachments.push({
          filename: meta.name || filename,
          contentType: meta.type || 'application/octet-stream',
          size: buf.length,
          storage: 'inline'
        });
      }
    }

    // Optional: re-parse raw MIME when present for advanced needs
    if (m.raw_msg) {
      // pass to your MIME parser for CID mapping or special charsets
    }

    console.log('stored message', record.id);
  }

  res.status(200).send('ok');
});

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

Two practical notes for Mandrill webhooks: store the exact public webhook URL in configuration for signature verification, and apply streaming or chunked processing if you expect many attachments because payloads include base64 content.

Performance and Reliability in Email-to-JSON

Large messages and attachments

For inbound pipelines that process reports, images, or logs, attachments frequently push past 10 MB. Systems that post presigned URLs can keep webhook payloads small and make it easier to stream large files directly to object storage. Batching base64 attachments inside the webhook, as mandrill-inbound does, simplifies atomic processing but increases memory footprint for your handler. Plan for back-pressure by using a streaming parser when possible, or staging attachments to disk before transformation.

Edge cases in MIME

  • Malformed multipart boundaries: robust parsers fall back to the safest readable part and preserve raw MIME for forensic needs.
  • Legacy charsets like ISO-2022-JP or Windows-1252: make sure the provider transcodes to UTF-8 and exposes the detected charset, or be prepared to re-parse raw_msg yourself.
  • Inline images with Content-ID: your JSON should include a map from cid: to the corresponding attachment object so that your renderer can rewrite references deterministically.
  • Outlook TNEF (winmail.dat): look for automatic extraction of encapsulated attachments or a flag indicating TNEF so you can run a dedicated extractor.
  • Threading metadata: normalized access to Message-ID, In-Reply-To, and References enables reliable ticket or conversation threading across clients.

Retries, ordering, and idempotency

Your webhook will receive retries during transient failures. Prefer event bodies that include a stable message ID and delivery attempt number so you can deduplicate. Mandrill's event model includes identifiers inside each event. A message-centric webhook that posts a single message per request simplifies idempotency logic further. In either case, store a hash or unique key and return HTTP 200 only after durable persistence.

Security and authenticity

Both approaches support HMAC signatures. Always verify signatures before decoding large bodies. If SPF and DKIM verdicts are included in the JSON or available through headers, capture them in your record so downstream systems can score trust. When raw MIME is provided, you can run your own DKIM check or ARC analysis if you need stricter controls.

Operational visibility

Look for structured logs that include message IDs, retry counts, and webhook latency. Alert on parse failures, signature failures, and attachment retrieval errors separately, since the first indicates malformed mail, the second a security issue, and the third a storage or network problem. A test harness that replays archived raw messages is invaluable for regressions as you change parsing rules.

Verdict: Which Is Better for Email to JSON?

If your priority is a clean, predictable email-to-json payload that requires minimal transformation code, a parser that posts one message per JSON webhook with presigned attachment URLs often leads to simpler, more resilient handlers. You get normalized fields, consistent charset handling, and an attachment model designed for streaming at scale.

If you are already invested in Mailchimp and want to keep everything inside that ecosystem, mandrill inbound is reliable and gives you access to msg.raw_msg for custom parsing when needed. Expect to expend some effort normalizing event-oriented payloads into your application's shape and managing larger webhook bodies due to base64 attachments.

The right choice depends on your architecture and constraints. For inbound-only parsing, lean toward a focused parser that optimizes JSON shape and delivery. For teams already operating Mailchimp transactional services, Mandrill's inbound events may be the shortest path, provided you accept the additional dependency.

FAQ

Do both providers support raw MIME for advanced parsing?

Yes. Mandrill can include msg.raw_msg when you enable the option. A dedicated parser can expose a raw source download on demand. Raw MIME lets you re-run CID mapping, custom charset conversion, or specialty extractors like TNEF for winmail.dat.

How should I handle large attachments in email-to-json workflows?

Prefer streaming. If your webhook receives presigned URLs, stream attachments directly to object storage and process by reference. If your webhook receives base64 blobs, write them to temporary storage first to avoid unbounded memory, then process and delete. In both cases, enforce size limits and timeouts.

What is the best way to verify webhook authenticity?

Use the HMAC verification mechanism each platform provides, reject requests without a valid signature, and pin the exact public URL in your verification routine. Run signature checks before parsing JSON or base64 to reduce CPU exposure for invalid traffic.

Can I normalize threading headers for ticketing systems?

Yes. Extract Message-ID, In-Reply-To, and References into top-level JSON fields, then compute a deterministic conversation key. Preserve the raw values for debugging, and consider bucketing by References when In-Reply-To is missing.

Where can I learn more about MIME parsing details?

Deepen your understanding with MIME Parsing: A Complete Guide | MailParse. It covers charsets, multipart handling, and real-world edge cases you will encounter in production.

Ready to get started?

Start parsing inbound emails with MailParse today.

Get Started Free