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, but structurally 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 — holding MAPI property streams named things like __substg1.0_37010102 for the body or __substg1.0_1000001E for plain text. There's no MIME, 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, and 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, which 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 through the same /v1/extract endpoint and response shape as a plain .eml.

curl
curl -X POST https://api.txtfetch.com/v1/extract \
  -H "Authorization: Bearer $TXTFETCH_KEY" \
  -F file=@forwarded-thread.msg
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
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
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 -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/exports/mailbox-2024.pst" \
  -H "Authorization: Bearer $TXTFETCH_KEY"
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
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
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": "..."
}

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 — OLE2-based Outlook formats with a completely different binary structure, even though the end result (message text) looks the same in the API response.
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

Stop parsing. Start shipping.

Create an account and get an API key in minutes — the free Hobby plan needs no card.

Get started →