All solutions

Solutions

Email Parsing API: Inbound Mail as Structured JSON, Zero MIME Handling

An email parsing API takes a raw inbound message — MIME multiparts, HTML, encoded headers and all — and delivers a clean, structured JSON event to your endpoint instead: sender, subject, plain-text body, thread identifier, attachment metadata, and a prompt-injection score, ready for your code or your LLM to consume directly. Mails.ai does this as a managed service: you register a webhook or open the SSE stream, and parsed events arrive the moment mail lands.

From raw MIME to typed JSON in one hop

The typical path to parsed inbound email: configure MX records, set up an IMAP client, handle OAuth token refresh, fetch raw messages, load a MIME parser, strip HTML, decode quoted-printable, reconstruct threads from In-Reply-To headers. Mails.ai collapses that into a single webhook call:

// What your endpoint receives — no parsing needed on your side
{
  "event": "message.received",
  "message_id": "msg_01jx8kpq2mfr4v",
  "thread_id": "thr_01jx4nbq8cde7w",
  "from": { "address": "alice@example.com", "name": "Alice" },
  "to": [{ "address": "support@yourworkspace.mails.ai" }],
  "subject": "Re: Order #2241",
  "body_text": "Hi — still waiting on the tracking number for my order.",
  "body_html": "<p>Hi — still waiting on the tracking number for my order.</p>",
  "injection_score": 0.02,
  "attachments": [],
  "received_at": "2026-09-06T08:14:02Z"
}

Thread reconstruction is automatic: every reply carries the same thread_id as the original message, so your agent or handler can retrieve the full conversation context in one query without parsing In-Reply-To headers yourself.

Register a webhook — parsing starts immediately

Give each inbox its own address (or bring a custom domain), register your endpoint, and every inbound message is parsed and POSTed to you:

import Mails from "@mailsai/sdk";
const mails = new Mails({ apiKey: process.env.MAILS_API_KEY });

// One-time: register a webhook for parsed inbound events
await mails.webhooks.create({
  url: "https://your-app.com/api/email",
  events: ["message.received"],
});

// Or skip the webhook entirely — use the SSE stream or long-poll
for await (const event of mails.events.stream({ type: "message.received" })) {
  const { body_text, thread_id, injection_score, from } = event;

  // body_text is already plain-text — no HTML stripping, no MIME decoding
  if ((injection_score ?? 0) > 0.5) {
    // Treat as suspicious; skip or quarantine
    continue;
  }

  // Reply in the same thread — no Message-ID wrangling
  await mails.messages.reply(event.message_id, {
    body_text: `Hi ${from.name ?? from.address} — on it, will follow up shortly.`,
  });
}

Python integration: consuming parsed events

The email parsing API is plain HTTPS + JSON — no SDK required for Python. Register your webhook once and handle parsed events in Flask or FastAPI:

import hmac, hashlib, os, requests
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["MAILS_WEBHOOK_SECRET"]
API = "https://api.mails.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MAILS_API_KEY']}"}

@app.post("/api/email")
def handle_parsed_email():
    # Verify HMAC signature before trusting the payload
    body = request.get_data()
    sig  = request.headers.get("X-Mails-Signature", "")
    expected = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):
        return jsonify({"error": "invalid signature"}), 401

    event = request.json
    # All fields already parsed — no MIME wrangling in your handler
    body_text = event["body_text"]      # clean plain text
    thread_id = event["thread_id"]      # stable conversation ID
    inj_score = event.get("injection_score", 0)

    if inj_score > 0.5:
        return jsonify({"status": "quarantined"}), 200

    reply = your_agent.respond(body_text, thread_id=thread_id)

    requests.post(f"{API}/messages", headers=HEADERS, json={
        "agent":       "support",
        "to":          event["from"]["address"],
        "in_reply_to": event["message_id"],
        "body_text":   reply,
    })
    return jsonify({"status": "ok"}), 200

For the async FastAPI variant with asyncio.create_task and HMAC verification, see the dedicated inbound email API webhooks for Python agents page.

Attachment and inline content parsing

The email parsing API surfaces attachments as typed metadata — you never touch a raw MIME boundary. Each attachment object in the parsed event includes:

  • filename — decoded from RFC 2047 encoding if necessary
  • content_type — e.g. application/pdf, image/png
  • size — bytes, before any base64 overhead
  • disposition — attachment or inline (inline images embedded in HTML are separated)

Attachment content is fetched separately via GET /v1/messages/:id/attachments/:index — the parsed event stays lean and your webhook handler is not forced to buffer large blobs.

Prompt-injection scanning built into every parsed event

Inbound email from arbitrary senders is untrusted text. Every message processed by the email parsing API is scanned across six injection categories before the event fires — boundary manipulation, system-prompt override, data exfiltration, role hijacking, tool invocation, and encoding tricks. The result is a numeric injection_score (0.0 = clean, 1.0 = high-confidence attack) on every parsed event:

// Recommended thresholds for AI agent handlers
const { injection_score, body_text, thread_id } = event;

if (injection_score > 0.95) {
  await quarantine(event);          // auto-reject — very high confidence
} else if (injection_score > 0.5) {
  await flagForReview(event);       // human review queue
} else if (injection_score > 0.3) {
  await processWithCaution(event);  // log but still process
} else {
  await agent.handle(body_text, thread_id);  // normal path
}

Parsing is the product, not a feature. Unlike add-on inbound parsing from a transactional provider, the parsing layer here is first-class: multipart MIME is unwrapped, HTML is stripped to clean plain text, and Base64-encoded bodies are decoded before delivery. When you need the raw source for auditing, it is available separately — but your handler receives the clean form by default.

Delivery modes: webhook, SSE, and long-poll

The same parsed event is available in three delivery modes — choose what fits your deployment:

  • Webhook — HTTPS POST to your endpoint, HMAC-signed, retried with exponential backoff (5 attempts over ~10 minutes). Best for serverless and stateless handlers.
  • SSE stream — GET /v1/events/stream holds the connection open and pushes events as they arrive. Best for long-running processes that want to avoid polling.
  • Long-poll — GET /v1/events with optional filters (type, agent, since). Best for environments where persistent connections are inconvenient (behind certain proxies or load balancers).

Optional intent and entity classification — enabled per address or globally — adds intent, entities, and urgency fields to each parsed event at $0.003 per message. Useful when you want routing logic (high-urgency → escalate, refund-intent → billing agent) decided before your LLM runs rather than inside it.

For a deeper look at the inbound architecture — SES inbound → Lambda → structured event — see the inbound email parsing feature page.

Pricing

Parsed inbound events cost $0.002 each, including MIME handling, HTML stripping, thread reconstruction, and the injection-score scan. Intent classification is $0.003 more when enabled. The free tier covers 3,000 inbound messages per month — no card required to start. For workloads that need a dedicated address with its own MX routing, see the inbound email API for AI agents page. For outbound from the same parsed inbox, AI email agent covers the full bidirectional flow. Building the webhook handler in Python? See Email API with Webhooks for Inbound Messages — Python AI Agents for Flask and FastAPI patterns with HMAC signature verification. For the general bidirectional email pattern for agents, see email for AI agents.

Frequently asked questions

Why use an email parsing API instead of a library like mailparser or postal-mime?

A library parses a message you already fetched — which means you still need IMAP polling, OAuth refresh, and MIME retrieval to get the raw bytes in the first place. A managed email parsing API receives the message for you (via MX record), parses it server-side, and pushes a structured event to your webhook. You write a POST handler, not an email stack.

What fields does the parsed event include?

Every event includes: from (address + display name), to, subject, body_text (clean plain text, HTML stripped), body_html (when present), thread_id (stable across a full conversation), message_id, in_reply_to, attachment metadata (filename, content-type, size, inline vs. attached), and injection_score (0.0–1.0). Optional intent classification adds intent, entities, and urgency when you enable it.

Is webhook delivery the only option, or can I poll?

Three delivery modes: webhook (HTTPS POST, HMAC-signed, retried with exponential backoff), long-poll (GET /v1/events with filters), and SSE streaming (GET /v1/events/stream — holds the connection open and pushes events as they arrive). All three produce the same parsed JSON payload.

What does the injection-score field protect against?

Every inbound message is scanned for prompt-injection patterns before the parsed event is delivered. The score (0.0 = clean, 1.0 = high-confidence injection) lets your code branch on a number rather than relying on the LLM to notice it is being attacked inside the same prompt as the attack. Quarantine logic: if injection_score > 0.5, handle with elevated scrutiny.

Does the email parsing API work with Python as well as TypeScript?

Yes — the API is plain HTTPS + JSON, so any language that can POST to an endpoint consumes the parsed events. Python examples using the requests library (or httpx for async) work identically to the TypeScript SDK. For a complete Flask and FastAPI walkthrough with HMAC verification, see the inbound Python agent page.

How does thread reconstruction work across a multi-message conversation?

The email parsing API reads the Message-ID, In-Reply-To, and References headers of every message and assigns a stable thread_id to all messages that belong to the same conversation. You never parse these headers yourself — query your data store by thread_id to retrieve full context before handing a message to your LLM.

Live now

Built for agents.
Self-serve in minutes.

The API is live and self-serve. Drop ~6 lines into your agent and ship.

npmpnpmbunnpx
$ npm install @mailsai/sdk
Live on npm today · @mailsai/sdk + @mailsai/mcp-server