> Source: https://txtfetch.com/extract/msg > Plain-text twin — every page on txtfetch.com has one. https://txtfetch.com/text --- # Outlook .msg isn't email, structurally. MAPI property streams inside an OLE2 container. Nested .msg-in-.msg attachments. Whole mailbox databases. None of it looks like an .eml file. the-problem It's easy to assume .msg is just email in a different wrapper. Structurally, though, it has nothing to do with .eml. A .msg file is an OLE2 compound-file container, the same binary-filesystem-in-a-file format as legacy .doc/.xls. It holds MAPI property streams named things like \_\_substg1.0\_37010102 for the body, or \_\_substg1.0\_1000001E for plain text. There's no MIME and no RFC-822 headers to parse. Every .eml/RFC-822 parser fails on a .msg outright, because it isn't looking at the right binary shape at all. Attachments are embedded OLE storages inside the same container. A forwarded or embedded email shows up as a .msg nested inside a .msg. Zoom out further, and .pst/.ost files aren't single messages at all. They're whole mailbox databases, potentially thousands of messages in one binary file. one-request-solution txtfetch detects the OLE2 signature and routes .msg through Tika's MAPI-aware parser. That parser reads the property streams directly, body, headers-as-properties, and attachments, rather than expecting MIME. Nested .msg-in-.msg attachments are walked recursively. And .pst/.ost mailbox files are unpacked message by message. All of it goes through the same /v1/extract endpoint and response shape as a plain .eml. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@forwarded-thread.msg ``` Python ```python import os import requests with open("forwarded-thread.msg", "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"]) ``` JavaScript ```javascript import { readFile } from "node:fs/promises"; const file = new Blob([await readFile("forwarded-thread.msg")]); const form = new FormData(); form.append("file", file, "forwarded-thread.msg"); 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); ``` Go ```go 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("forwarded-thread.msg") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "forwarded-thread.msg") 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 ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/exports/mailbox-2024.pst" \ -H "Authorization: Bearer $TXTFETCH_KEY" ``` Python ```python 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/exports/mailbox-2024.pst"}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript const endpoint = new URL("https://api.txtfetch.com/v1/extract"); endpoint.searchParams.set("url", "https://example.com/exports/mailbox-2024.pst"); const res = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` Go ```go 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/exports/mailbox-2024.pst") 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 .msg 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. Sprint Status Update (Outlook MSG) `sprint-status-update.msg` · application/vnd.ms-outlook · left pane read via Apache Tika, **right pane: the human-checked expected.json** raw parser output ``` From: Dana Lee To: Engineering Subject: Sprint Status Update Hi all, Here is where things stand heading into the demo on Friday. Benchmark harness: on track Corpus curation: on track CI wiring: at risk, needs a second reviewer Ping me if you want to pair on the CI job. Thanks, Dana ``` the human-checked ideal ``` From: Dana Lee To: Engineering Subject: Sprint Status Update Hi all, Here is where things stand heading into the demo on Friday. Benchmark harness: on track Corpus curation: on track CI wiring: at risk, needs a second reviewer Ping me if you want to pair on the CI job. Thanks, Dana ``` [See all ten documents, with the full explanation →](https://txtfetch.com/diff) formats-covered - `.msg` - `.eml` - `.mbox` - `.pst` - `.ost` faq **Why does my .eml parser fail on a .msg file?**: Because .msg isn't RFC-822/MIME at all. It's an OLE2 compound-file container with MAPI property streams. Any parser built around parsing email headers and MIME parts is looking for a structure that simply isn't there in a .msg file. **How is .msg different from the .eml page on this site?**: /extract/email covers RFC-822 .eml and .mbox: text-based, MIME-encoded formats. This page covers .msg, .pst, and .ost instead: OLE2-based Outlook formats with a completely different binary structure. The end result, message text, looks the same in the API response either way. **Can a .msg file contain another .msg file?**: Yes. A forwarded or embedded message shows up as a nested OLE2 storage inside the parent .msg. txtfetch walks these recursively rather than stopping at the first level. **Can I extract a whole .pst or .ost mailbox in one request?**: Yes. .pst and .ost are whole mailbox databases rather than single messages. txtfetch unpacks and extracts the contained messages' text through the same endpoint. go-further - [RFC-822 .eml and .mbox (a different format, despite the overlap) →](https://txtfetch.com/extract/email) - [Drop a real .msg and see the extracted text, free →](https://txtfetch.com/tools/email-to-text) - [API quickstart →](https://txtfetch.com/docs) - [How the pipeline works →](https://txtfetch.com/how-it-works) - [Extract it from your language →](https://txtfetch.com/for) - [Get an API key →](https://app.txtfetch.com/signup) Email - [`.eml`](https://txtfetch.com/extract/email) - [All formats →](https://txtfetch.com/extract) ## Send a real .msg through it. One HTTP call returns the text. Read one in your browser first, for free. [Get an API key →](https://app.txtfetch.com/signup) [Open the free .msg reader →](https://txtfetch.com/tools/email-to-text)