https://txtfetch.com/extract/email/
Email body and attachments, one call.
The message body and the text of each attached document come out together. An .eml returns no From, To or Subject lines. Read them yourself, as /docs/attachments shows.
the-problem
Email is a container format wearing a text format's clothes. It has headers, a MIME-encoded body (often both plain-text and HTML parts), and arbitrary attached documents that themselves need extracting. Most extraction tools have no email code path at all. That's especially true for VLM-based tools built around single images or pages. Teams end up hand-rolling MIME decoding just to get the text they actually want.
one-request-solution
txtfetch treats email as a first-class format. POST an .eml, Outlook .msg, or .mbox archive, and Apache Tika extracts the body and the attachment text into one plain-text response. For .eml and .mbox it drops the headers. An Outlook .msg keeps its subject, sender name, To and Cc. The request shape is the same as every other format.
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@support-thread.emlimport os
import requests
with open("support-thread.eml", "rb") as f:
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
files={"file": f},
)
print(r.json()["extracted_text"])import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("support-thread.eml")]);
const form = new FormData();
form.append("file", file, "support-thread.eml");
const res = await fetch("https://api.txtfetch.com/v1/extract", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
body: form,
});
const { extracted_text } = await res.json();
console.log(extracted_text);package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
type extractResponse struct {
Status string `json:"status"`
ExtractedText string `json:"extracted_text"`
}
func main() {
f, err := os.Open("support-thread.eml")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "support-thread.eml")
if err != nil {
panic(err)
}
if _, err := io.Copy(part, f); err != nil {
panic(err)
}
writer.Close()
req, err := http.NewRequest("POST", "https://api.txtfetch.com/v1/extract", &body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var result extractResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
panic(err)
}
fmt.Println(result.ExtractedText)
}Or skip the download. Pass a url parameter and txtfetch fetches the document server-side:
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/archive/2024-q1.mbox" \
-H "Authorization: Bearer $TXTFETCH_KEY"import os
import requests
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
params={"url": "https://example.com/archive/2024-q1.mbox"},
)
print(r.json()["extracted_text"])const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/archive/2024-q1.mbox");
const res = await fetch(endpoint, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
});
const { extracted_text } = await res.json();
console.log(extracted_text);package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
)
type extractResponse struct {
Status string `json:"status"`
ExtractedText string `json:"extracted_text"`
}
func main() {
endpoint, err := url.Parse("https://api.txtfetch.com/v1/extract")
if err != nil {
panic(err)
}
q := endpoint.Query()
q.Set("url", "https://example.com/archive/2024-q1.mbox")
endpoint.RawQuery = q.Encode()
req, err := http.NewRequest("POST", endpoint.String(), nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var result extractResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
panic(err)
}
fmt.Println(result.ExtractedText)
}{
"status": "success",
"extracted_text": "..."
}what-comes-back
That is the shape. This is the text. A real .eml from the benchmark corpus, word-diffed against the raw parser output. Nothing here was written for this page. Every character comes from a committed fixture.
Invoice Follow-up (EML)
raw parser output
From: Priya Shah <priya@example.com>
To: Billing <billing@example.com>
Subject: Re: Invoice #7734
Hi team,
Following up on the invoice below — could you confirm receipt?
Description Qty Amount
Consulting hours 12 $1,800.00
Travel 1 $340.00
Invoice total: $2,140.00
Due date: 2026-08-15
Thanks,
Priya
the human-checked ideal
From: Priya Shah <priya@example.com>
To: Billing <billing@example.com>
Subject: Re: Invoice #7734
Hi team,
Following up on the invoice below — could you confirm receipt?
Description Qty Amount
Consulting hours 12 $1,800.00
Travel 1 $340.00
Invoice total: $2,140.00
Due date: 2026-08-15
Thanks, PriyaSee all ten documents, with the full explanation →
try-your-own
Drop your own .eml file below. Your browser reads it. Nothing uploads, and txtfetch never sees it.
Stays in your browser. Up to 25 MB.
What's not in this text:
Your browser read this file. No request left your browser, so txtfetch never received it. The API returns this same text as { status, extracted_text }.
See the real API response in the playground. Need many files, a folder, or a .zip? Use file-to-text instead.
formats-covered
.eml.msg.mbox
faq
- Can I parse a .eml email for an LLM?
- Yes. POST the .eml file to https://api.txtfetch.com/v1/extract and get back the body and any attachment text as one plain-text response. The From, To, Cc, Subject and Date headers do not come back, so read them with your own code. The text is ready to feed into a prompt or embedding step.
- Does it support Outlook .msg files?
- Yes, through the same endpoint, with the same response shape. Under the hood, .msg is a completely different container: OLE2 with MAPI property streams, not RFC-822. Read the dedicated .msg page if you're debugging attachment or nesting behavior.
- Can it process a whole .mbox archive?
- Yes. Apache Tika reads .mbox archives and extracts the contained messages' text. Only the bodies come back. There are no headers and no separator line between messages.
- Are email attachments included in the extracted text?
- Yes. Attached documents are parsed, and their text comes back after the message body. The attachment file names do not come back for .eml. An Outlook .msg returns them.
go-further
Send a real .eml through it.
One HTTP call returns the text. Read one in your browser first, for free.
Get an API key →