Skip to content
api mime developers email-to-json parsing

Preserve Raw Multipart MIME in POST /v1/parse

A practical guide to sending intact RFC 822 MIME, including multipart bodies, to MailFrame's synchronous parse API, with curl and code examples.

MailFrame Team

MailFrame’s shipped path is one synchronous endpoint. You POST /v1/parse with the raw email in raw_mime and a JSON Schema, and typed, validated JSON comes back in the same HTTP response.

The quality of that extraction depends heavily on what you put in raw_mime. A frequent source of poor results is not a schema problem at all. It is a transport problem: the caller strips the message down to a body fragment before sending it, and the parser never sees the headers, the multipart structure, or the plain-text alternative that would have made the extraction much easier.

This post explains why the full raw message matters, exactly what raw_mime expects, how to capture a multipart message intact from common sources, and the handful of mistakes that quietly degrade results.

Why the raw message matters

A real email is more than its visible body. An RFC 822 message carries several layers of signal that the extraction pipeline uses:

  • Headers. From, To, Date, Subject, and Message-ID identify the sender, anchor the message in time, and give you a stable key for deduplication. A schema field like order_date is easier to fill when the Date header is part of the input, because the extractor has a second place to find the date if the body is ambiguous.
  • Multipart structure. Most transactional email is multipart/alternative with a text/plain part and a text/html part. The plain-text part is usually the cleanest source for extraction. If you send only the HTML, the parser has to work through markup and tracking pixels to find the same facts.
  • Charsets and transfer encodings. Each part declares its own charset and Content-Transfer-Encoding (often quoted-printable or base64). MailFrame decodes these per part. If you decode them yourself and re-serialize incorrectly, currency symbols and accented names can arrive corrupted.
  • Attachments. Encoded attachment parts can only be told apart from the readable body when the multipart boundaries are intact. Break the boundaries and base64 attachment bytes can end up mixed into the text handed to extraction.

The pipeline design post covers how these stages fit together. The short version is that MIME extraction runs before the model ever sees the text, and it does its best work on the original message.

What raw_mime actually is

raw_mime is a JSON string containing the full RFC 822 message text. Headers, a blank line, then the body, including every multipart boundary. That is the whole contract.

Three things it is not:

  • It is not base64. You do not encode the message before putting it in the JSON string. The API hands the string directly to the MIME extractor.
  • It is not a pre-extracted body. You do not need to pick the text part, strip HTML, or decode quoted-printable. Send the message as the mail server delivered it.
  • It is not a file upload. There is no multipart form field for email. The message travels as a string inside a JSON request body.

Because it is a JSON string, the only transformation you apply is JSON escaping. Line breaks become \r\n or \n inside the string, and any double quotes inside header values or boundaries become \". Every JSON library does this for you when you serialize an object. The only time you would hand-write those escapes is in a curl example like the one below.

A complete multipart example

Here is a small but structurally complete multipart/alternative receipt. It has real headers, a declared boundary, a text/plain part, and a text/html part:

From: Acme Coffee <receipts@acmecoffee.example>
To: jordan@example.com
Date: Wed, 28 Oct 2026 09:14:02 +0000
Subject: Your receipt from Acme Coffee
Message-ID: <rcpt-7731@acmecoffee.example>
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="b1_7731"

--b1_7731
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: 7bit

Thanks for your order on October 28, 2026.
Order #7731
Total charged: $29.99

--b1_7731
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: 7bit

<html><body><p>Thanks for your order on October 28, 2026.</p>
<p>Order <b>#7731</b></p><p>Total charged: <b>$29.99</b></p></body></html>

--b1_7731--

To send it, place that exact text in raw_mime with JSON escaping applied. The schema travels inline in the same request. The newlines are written as \r\n, and the quotes around the boundary and charset become \":

curl -X POST https://api.mailframe.ai/v1/parse \
  -H "Authorization: Bearer $MAILFRAME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "schema": {
      "type": "object",
      "required": ["merchant", "order_number", "total_amount", "order_date"],
      "properties": {
        "merchant": { "type": "string" },
        "order_number": { "type": "string" },
        "total_amount": { "type": "number", "minimum": 0 },
        "order_date": { "type": "string", "format": "date" }
      }
    },
    "raw_mime": "From: Acme Coffee <receipts@acmecoffee.example>\r\nTo: jordan@example.com\r\nDate: Wed, 28 Oct 2026 09:14:02 +0000\r\nSubject: Your receipt from Acme Coffee\r\nMessage-ID: <rcpt-7731@acmecoffee.example>\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative; boundary=\"b1_7731\"\r\n\r\n--b1_7731\r\nContent-Type: text/plain; charset=\"UTF-8\"\r\nContent-Transfer-Encoding: 7bit\r\n\r\nThanks for your order on October 28, 2026.\r\nOrder #7731\r\nTotal charged: $29.99\r\n\r\n--b1_7731\r\nContent-Type: text/html; charset=\"UTF-8\"\r\nContent-Transfer-Encoding: 7bit\r\n\r\n<html><body><p>Thanks for your order on October 28, 2026.</p>\r\n<p>Order <b>#7731</b></p><p>Total charged: <b>$29.99</b></p></body></html>\r\n\r\n--b1_7731--\r\n"
  }'

A clean parse returns HTTP 200 with the parse id, a status of completed, and the extracted object under data. The values below are illustrative; the id is generated per request and the fields depend on the model’s run against your input:

{
  "id": "parse_9d41e2",
  "status": "completed",
  "data": {
    "merchant": "Acme Coffee",
    "order_number": "7731",
    "total_amount": 29.99,
    "order_date": "2026-10-28"
  }
}

Nothing about the request changes when the message grows. A ten-part newsletter with inline images and a PDF attachment goes in the same field, as the same kind of string. Extraction targets the readable text parts of the message; attachment contents are not parsed as input today. See the JSON Schema email parser guide for more on designing the schema side of the request.

How to capture the raw message from common sources

The right way to get a message into raw_mime is to grab the original bytes and never reshape them. The specific API differs by source, so check your provider’s documentation for the exact call. The pattern is the same everywhere:

  • IMAP. Fetch the whole message, not a body section. In most IMAP libraries this is the RFC822 or BODY[] item rather than BODY[TEXT] or a specific part number. The result is the complete message with headers and boundaries.
  • Inbound email webhooks. Many inbound-email services can hand you the full raw message alongside their pre-parsed fields, sometimes as a separate option or a separate field. Prefer the raw form. The pre-parsed text and html fields are convenient for display but drop the structure MailFrame uses.
  • Object storage. If your mail pipeline writes messages to S3-style storage, the object is typically the raw message already. Read it as bytes and convert to a string without loss. Messages with 7-bit, quoted-printable, or base64 parts are plain ASCII on the wire, so this is straightforward. Legacy messages with 8bit or binary parts in a non-UTF-8 charset need care: a blind UTF-8 decode of the whole message can replace bytes with U+FFFD and corrupt those parts before MailFrame sees them. Do not try to fix this by decoding the whole byte string with one charset, because different parts can declare different charsets. Instead, load the message with a MIME-aware library and re-serialize it with a 7-bit-safe transfer encoding such as quoted-printable or base64, which preserves each part’s bytes and charset declaration while producing an ASCII string you can place in JSON safely.
  • Mailbox exports. .eml files are raw RFC 822 messages and can be read directly. .mbox files are concatenated messages separated by From envelope lines. Do not split on those lines yourself: body lines that begin with From are escaped in some mbox variants and not in others, and a naive line split can cut messages apart or merge them. Use a proper mbox parser to iterate the messages, keep each message’s full headers and body intact, and send each one individually.

When in doubt, look at the first line of what you are about to send. If it is a header like Received:, From:, or Return-Path:, you have the raw message. If it is <html> or a sentence of body text, you have a fragment.

Common mistakes

Each of these produces a request that succeeds but extracts worse than it should:

  1. Sending only the HTML part. The request may still return a result, but you have thrown away the plain-text alternative and every header. Dates, sender identity, and IDs that lived in headers are now missing, and the model is working through markup to find the rest.
  2. Base64-encoding the message. raw_mime is a plain string. If you base64-encode the whole message, the parser receives one long opaque token with no headers and no body to extract from.
  3. Dropping headers to “save space”. Headers are small and carry some of the most reliable fields in the message. Keep them.
  4. Mangling boundaries. If a templating step, a trim, or an overzealous whitespace normalizer alters the --boundary lines, the parser can no longer split the parts, and the HTML, plain text, and attachment bytes may merge into one blob. Do not touch the message content between fetching it and serializing it into the JSON string.
  5. Decoding parts yourself. Converting quoted-printable or per-part base64 before sending is unnecessary and error-prone. The extractor handles this in two distinct steps for each part: it first reverses the part’s Content-Transfer-Encoding to recover the original bytes, then decodes those bytes to text using the charset declared on that part.
  6. Reaching for file to send an email. The file field is not for email. See the next section.

Base64 is transport encoding, not the email format

The parse request has two input fields, and they have different encodings for a reason.

raw_mime is a plain JSON string. The server passes it straight into the MIME extractor, which understands the message’s own internal encodings. Base64 appears inside a MIME message only as a Content-Transfer-Encoding on individual parts, and the extractor decodes those parts itself. You never add an outer layer.

file is different. It is designed for binary documents such as PDFs and images, which cannot be placed in a JSON string as-is. For that field, base64 is the transport encoding: the server decodes the base64 string back to bytes before processing. PDF and image input through file is on the roadmap, not yet available, so today every request should use raw_mime.

The simple rule: if the input is an email, it is text, and it goes in raw_mime without any encoding. Base64 is only for binary input, and binary input is a planned feature.

Roadmap context

The only shipped capability described here is the direct, synchronous POST /v1/parse call with raw_mime plus a schema, returning typed JSON in the HTTP response. The sync request has a 30 second timeout.

Two related inputs are planned, not yet available:

  • Inbox forwarding to a provisioned address, so messages arrive at MailFrame without your code making the API call. When it ships, the message that reaches the parser will be the same raw MIME you would otherwise send yourself, so everything in this post still applies.
  • PDF and image input through the base64 file field, as described above.

If you are planning around delivery of results rather than ingestion, the webhook design post describes the planned approach. Webhook delivery is also on the roadmap and not yet available.

Production checklist

Before you point real traffic at the endpoint:

  • Preserve the original message. Fetch the full message from your source and keep its content unmodified until JSON serialization. Do not trim, re-wrap lines, or rewrite boundary lines. Convert bytes to a string losslessly. For legacy 8bit or binary parts, load the message with a MIME-aware library and re-serialize it with a 7-bit-safe transfer encoding; do not decode the entire message with one charset.
  • Let your JSON library escape it. Build an object with schema or schema_id and raw_mime, then serialize. Hand-escaping is only for curl demos.
  • Pick schema_id or an inline schema. A saved schema_id keeps requests small and lets you evolve the contract in one place. An inline schema is fastest for iteration. Either way the request must include exactly one of them. The schema-first rationale explains why the contract lives on the request at all.
  • Check three things before storing. Confirm the HTTP response is OK, that status is completed, and that validation_errors is absent or empty. Only then treat data as schema-conformant. A response that carries a non-empty validation_errors array means the extraction did not fully satisfy the schema and deserves a review path. Passing validation proves the shape is right, not that every value is semantically correct, so keep a spot-check or reconciliation step for high-stakes fields. A non-2xx response carries the standard error envelope with type, message, and request_id, and no parse data.
  • Keep Message-ID. Since you are sending the headers anyway, store the Message-ID next to the parse result so repeated deliveries of the same message do not create duplicate records.

Next steps

Turn your inbox into an API

MailFrame parses raw email into typed JSON against your own schema — PDF and image input are planned. Request early developer access.

Request early access