Email to JSON: MailParse vs Amazon SES

Compare MailParse and Amazon SES for Email to JSON. Features, performance, and developer experience.

Why Email to JSON Matters When Choosing an Email Parsing Service

Developers rely on email-to-json to turn raw messages into structured, predictable data that applications can consume. When a user replies to a notification, when a customer sends a support email, or when a third-party system emits automated reports, your app needs clean JSON with addresses, subject, body text, HTML, attachments, and metadata. Done correctly, email-to-json simplifies workflows like ticket creation, comment threading, lead capture, and document ingestion. Done poorly, it leads to brittle parsing, missing attachments, and hours of maintenance.

This guide compares MailParse and Amazon SES for converting email to JSON, focusing on developer experience, feature depth, and operational reliability. If your goal is fast, robust email-to-json, the differences are significant.

How MailParse Handles Email to JSON

MailParse turns inbound MIME into clean JSON and delivers it to your application by webhook or via a simple REST polling API. You provision an address instantly, wire a destination, and start receiving structured payloads within minutes. No additional infrastructure is required.

Webhook delivery

When a message arrives, the service issues an HTTP POST to your endpoint with a JSON body that includes normalized headers, envelope details, bodies, and attachments. A typical payload looks like this:

{
  "eventId": "evt_01HV2E9X8H1YQ2Z",
  "timestamp": "2026-04-29T16:01:23.412Z",
  "envelope": {
    "from": "alerts@example.com",
    "to": ["inbox@your-app.io"],
    "helo": "mail-out.provider.net",
    "remoteIp": "203.0.113.24"
  },
  "message": {
    "messageId": "<CAD1t1=abc123@example.com>",
    "subject": "Build complete",
    "date": "2026-04-29T16:01:10.000Z",
    "headers": {
      "from": "Alerts <alerts@example.com>",
      "to": "Inbox <inbox@your-app.io>",
      "content-type": "multipart/alternative; boundary=abc",
      "dkim-signature": "...",
      "received-spf": "pass",
      "x-priority": "3"
    },
    "text": "Your build finished successfully.\nLog: https://ci.example.com/build/42",
    "html": "<p>Your build finished successfully.</p><p><a href=\"https://ci.example.com/build/42\">View log</a></p>",
    "attachments": [
      {
        "filename": "build-log.txt",
        "contentType": "text/plain",
        "size": 18421,
        "contentId": null,
        "inline": false,
        "sha256": "d7b10c6d...",
        "downloadUrl": "https://files.your-endpoint/download/evt_01HV2E9X8H1YQ2Z/1"
      }
    ],
    "inline": [
      {
        "filename": "logo.png",
        "contentType": "image/png",
        "size": 4212,
        "contentId": "logo@ci",
        "inline": true,
        "downloadUrl": "https://files.your-endpoint/download/evt_01HV2E9X8H1YQ2Z/2"
      }
    ],
    "spamVerdict": "pass",
    "dkimVerdict": "pass",
    "spfVerdict": "pass",
    "dmarcVerdict": "pass",
    "charset": "utf-8"
  }
}

Key details for developers:

  • Normalized addresses - arrays for to, cc, bcc, and a cleaned from.
  • Both text and html bodies, with CID references preserved so you can render inline images.
  • Attachments and inline assets listed with content metadata and stable download URLs, so your app can fetch or defer storage.
  • Verdicts for SPF, DKIM, and spam surfaced alongside the message for easy filtering.
  • Explicit charset normalization to UTF-8 with fallback handling for edge encodings.

Delivery is retried with exponential backoff on non-2xx responses. Each POST includes an idempotency key like eventId so you can deduplicate. Signatures can be verified with an HMAC header such as X-Signature to confirm authenticity. You control per-endpoint timeouts and can respond with 202 to decouple processing from acknowledgment.

REST polling API

If webhooks are difficult in your environment, a cursor-based API exposes the same JSON records:

# Fetch events after a cursor
GET /v1/inbound/events?after=evt_01HV2E9X8H1YQ2Z&limit=100

# Example response snippet
{
  "data": [ /* same structure as webhook */ ],
  "next": "evt_01HV2EAPQ5B3C6",
  "hasMore": true
}

This approach is popular for batch processors, air-gapped environments, and workflows where your app controls the ingestion pace. Attachments can be streamed via signed URLs or pulled directly with a download API that supports range requests.

For product ideas built on email-to-json, see Top Inbound Email Processing Ideas for SaaS Platforms and Top Email Parsing API Ideas for SaaS Platforms.

How Amazon SES Handles Email to JSON

Amazon SES Receiving is part of AWS Simple Email Service. It accepts inbound mail for your domain, then executes a receipt rule set. A rule can store the raw MIME in S3, invoke Lambda, publish to SNS, or stop processing. By default, SES does not convert email to JSON - you build that with your own code and AWS resources.

Typical SES inbound architecture for email-to-json

  1. Verify your domain in SES and configure an MX record that points to the region's inbound endpoint, for example inbound-smtp.us-east-1.amazonaws.com.
  2. Create a receipt rule set that matches your domain or specific recipients.
  3. Add an S3 action to store the full MIME content in an S3 bucket. Optionally include a prefix and KMS encryption.
  4. Add a Lambda action in the same rule or a separate S3 trigger to process the object. The Lambda function reads the S3 object and parses the MIME using a library such as mailparser (Node.js) or the Python email package.
  5. Have the function output JSON to your system via HTTPS, SQS, a database, or another service bus.

SES provides helpful metadata in the Lambda invocation event - message ID, recipients, and verdicts such as spamVerdict and dkimVerdict. However, the raw MIME body is not included inline with the event. You typically retrieve it from S3 using the object key provided by SES. If you need webhook-style delivery, you set up API Gateway or call your API directly from Lambda.

Considerations when building your JSON layer

  • Parsing - choose a MIME parser that handles nested multiparts, attachments, and character sets. For Node.js, mailparser is common. On Python, email with policy=default and possibly flanker or mail-parser for improvements.
  • Attachments - decide whether to re-upload to S3 with a normalized folder structure, compute checksums, and generate signed URLs.
  • Inline images - maintain CID mappings so your app can render HTML with embedded assets.
  • Verdicts - merge SES verdicts from the event with your parsed message JSON.
  • Security - tighten S3 bucket policies, grant minimal IAM permissions to Lambda, and rotate secrets used for outbound webhooks.
  • Reliability - set Lambda timeouts and memory high enough to parse large messages, add a dead-letter queue for failures, and consider SQS between steps for backpressure.

Amazon SES is powerful inside AWS, but for email-to-json it requires assembling multiple services. That yields flexibility, at the cost of time and complexity.

Side-by-Side Comparison: Email to JSON Features

Feature MailParse Amazon SES
Native email-to-json conversion Built in Custom code required
Webhook delivery Yes No native webhook - use Lambda or API Gateway
REST polling API Yes No - build your own service layer
Normalized from, to, cc, bcc Yes Via parser you implement
Text and HTML bodies with consistent UTF-8 Yes Via parser and charset handling code
Inline image CID mapping Yes Via custom logic
Attachment extraction and metadata Yes, with content type, size, checksum, and URLs Requires parsing MIME and managing S3 objects
TNEF winmail.dat decoding Built in Requires extra library and logic
SPF, DKIM, DMARC, spam verdicts in JSON Included Available in SES event, merge into your JSON
Idempotent event IDs for deduplication Yes Implement with S3 keys or SES message IDs
Automatic retries with exponential backoff Yes for webhooks Build with Lambda retries and DLQs
Time to first JSON Minutes Hours to days depending on infrastructure
Message size handling Streams large attachments SES accepts up to 40 MB - ensure Lambda memory and streams

Code Examples: Developer Experience Side by Side

Webhook handler that receives JSON

Example Express server to receive a JSON payload, verify a signature header, and persist the message and attachments:

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

const app = express();
app.use(express.json({ limit: "50mb" }));

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

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

  const { eventId, message } = req.body;

  // idempotency check
  // if (await seen(eventId)) return res.sendStatus(200);

  // Save metadata
  await saveMessage({
    id: eventId,
    from: message.headers.from,
    to: message.headers.to,
    subject: message.subject,
    text: message.text,
    html: message.html,
    verdicts: {
      spf: message.spfVerdict,
      dkim: message.dkimVerdict,
      dmarc: message.dmarcVerdict
    }
  });

  // Stream attachments to object storage
  for (const att of [...(message.attachments || []), ...(message.inline || [])]) {
    if (!att.downloadUrl) continue;
    const r = await fetch(att.downloadUrl);
    await uploadToStorage(`messages/${eventId}/${att.filename}`, r.body, att.contentType);
  }

  // Acknowledge quickly
  res.sendStatus(202);
});

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

Amazon SES Lambda that converts S3 MIME to JSON

Node.js Lambda that reads the S3 object written by SES and parses it to JSON using mailparser:

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { simpleParser } from "mailparser";
import { createHash } from "crypto";

const s3 = new S3Client({});

export const handler = async (event) => {
  // SES passes metadata in event.Receipt and event.Mail (via SNS or direct Lambda action)
  // If using the S3 action, your Lambda will need the bucket/key
  const record = event.Records?.[0];
  const bucket = record.s3.bucket.name;
  const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "));

  const { Body } = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
  const parsed = await simpleParser(Body);

  // Normalize fields
  const toArray = (addr) => (addr ? addr.value.map(a => a.address) : []);
  const attachments = (parsed.attachments || []).map((a) => ({
    filename: a.filename,
    contentType: a.contentType,
    size: a.size,
    contentId: a.cid || null,
    inline: !!a.cid,
    sha256: createHash("sha256").update(a.content).digest("hex")
  }));

  const json = {
    messageId: parsed.messageId,
    subject: parsed.subject || "",
    date: parsed.date ? parsed.date.toISOString() : null,
    from: parsed.from?.value?.[0]?.address || null,
    to: toArray(parsed.to),
    cc: toArray(parsed.cc),
    bcc: toArray(parsed.bcc),
    text: parsed.text || "",
    html: parsed.html || "",
    attachments
  };

  // Post to your API
  await fetch(process.env.INGEST_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json", "Authorization": `Bearer ${process.env.API_TOKEN}` },
    body: JSON.stringify(json)
  });

  return { ok: true };
};

In production, add:

  • Streaming attachment uploads to avoid buffering large files in memory.
  • Merging SES verdicts from the event object into your JSON.
  • Retries with exponential backoff and a dead-letter queue via SNS or SQS.
  • Unit tests with a corpus of tricky MIME samples, including TNEF and nested multiparts.

Performance and Reliability in Email-to-JSON Pipelines

Handling large messages and attachments

Large messages stress parsers and memory. The webhook model avoids you managing object storage, but you still must stream downloads and enforce size limits in your application. If you poll, prefer range requests and chunked uploads to your storage provider.

On Amazon SES, set Lambda memory high enough to cope with parsing overhead and use streaming where possible. The simpleParser convenience function is great for small messages, but for very large inputs, prefer the streaming API in mailparser to reduce memory pressure. If you choose Python, avoid reading the entire object into memory - use iter_lines or iter_chunks when pulling from S3.

Character sets and malformed MIME

Email bodies show up in ISO-8859-1, Shift JIS, windows-1251, and sometimes mixed within a multipart structure. Robust email-to-json normalizes to UTF-8 and preserves original headers for forensic use. Ensure your parser decodes quoted-printable and base64, collapses strange line endings, and surfaces both text and HTML reliably. For malformed inputs, be strict in parsing but generous in what you accept - log anomalies and keep the payload consumable.

Spam and authentication verdicts

Both approaches benefit from surfacing SPF, DKIM, DMARC, and spam verdicts alongside the message. Webhook payloads that include these fields make downstream rules easier. With Amazon SES, extract the receipt verdicts from the event and merge them into your JSON so your app can filter or flag suspicious messages.

Idempotency and retries

Expect duplicate deliveries. Use a primary key based on a stable event or message ID, combine it with a hash of the raw data for safety, and deduplicate. For webhook delivery, acknowledge quickly with 200 or 202 and process asynchronously. For SES and Lambda, configure a dead-letter queue, track execution attempts, and ensure your JSON delivery to downstream systems is also idempotent.

Operational visibility

Instrument parsing time, attachment sizes, verdict distributions, and failure causes. For SES pipelines, use CloudWatch metrics and logs, plus structured logging in your Lambda functions. For any webhook ingestion, capture response codes and latency by endpoint and enable alerting when retries spike. If

Ready to get started?

Start parsing inbound emails with MailParse today.

Get Started Free