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 cleanedfrom. - Both
textandhtmlbodies, 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
- 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. - Create a receipt rule set that matches your domain or specific recipients.
- Add an S3 action to store the full MIME content in an S3 bucket. Optionally include a prefix and KMS encryption.
- 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 Pythonemailpackage. - 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,
mailparseris common. On Python,emailwithpolicy=defaultand possiblyflankerormail-parserfor 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