Do attachments come back?

Yes. The text inside each file comes back. File names and email headers often do not. Here is what each input returns.

The short answer

The text of a file inside another file comes back. This holds for a ZIP, an email attachment and a file attached to a PDF.

A ZIP also returns the path of each file, as a line before its text. An Outlook .msg returns the attachment name too.

An .eml does not return its headers. From, To, Cc, Subject and Date are all missing. An .mbox does not return them either.

Email and PDF attachments come back without a file name.

metadata.ocr stays false for a picture inside a ZIP, although the picture is read.

The engine is tika-server-standard 3.3.1.

Source: terraform/lambda-layers/build/versions.env (versions.env: TIKA_VERSION), accessed 2026-10-19.

What comes back, case by case

We sent each file to the production API. Every inner file holds one marker sentence, so you can see which text came from which file. Every row shows its source. The inputs and the raw responses are in the repo.

What nine attachment inputs return today
InputWhat comes backSource
A ZIP with a folder, a picture, Word, PDF and a nested ZIPEvery file comes back, in archive order. Each path is its own line before its text. The picture is read by OCR, yet metadata.ocr says false. The ZIP inside the ZIP is walked, and its files come back in the same flat list.Production API probe: POST /v1/extract
accessed 2026-10-19
The same ZIP as MarkdownEach path becomes a level-1 heading. The nested ZIP gives a heading for inner.zip, then one for deep.txt. Nothing marks deep.txt as a child of inner.zip.Production API probe: POST /v1/extract
accessed 2026-10-19
The same ZIP as element JSONEach path is an element of type heading with level 1. The text of each file follows as paragraph elements. Split on the headings to get one text per file.Production API probe: POST /v1/extract
accessed 2026-10-19
An .eml with a body and four attachmentsThe body comes back, then the text of the PDF, the Word file, the picture and the file inside the attached ZIP. From, To, Cc, Subject and Date do not come back. No attachment file name comes back, except the name of the file inside the ZIP.Production API probe: POST /v1/extract
accessed 2026-10-19
The same .eml as MarkdownThe same text. The ZIP entry name and its text fold into one paragraph: mail-inside.txt Text from mail-inside.txt. The json output folds them the same way.Production API probe: POST /v1/extract
accessed 2026-10-19
An .mbox with two messagesThe two bodies come back. There are no headers and no line between the messages.Production API probe: POST /v1/extract
accessed 2026-10-19
An Outlook .msg with one attachmentThe subject, the sender name, To, Cc and the body come back. The attachment file name comes back as its own line, then its text. The sender address does not come back.Production API probe: POST /v1/extract
accessed 2026-10-19
An Outlook .msg inside an Outlook .msgThe outer message comes back, then the inner message with its own subject and sender name. The inner message is named after its subject.Production API probe: POST /v1/extract
accessed 2026-10-19
A PDF with an attached fileThe page text comes back. The text of the attached file follows. The attached file name does not come back.Production API probe: POST /v1/extract
accessed 2026-10-19

Real output

Each block is the full response body, as the API sent it. In the JSON, \n is a line break inside the text.

A ZIP with a folder, a picture, Word, PDF and a nested ZIP

In

archive.zip

Out

{"status":"success","extracted_text":"\nnotes.txt\nText from notes.txt.\n\n\n\n\npages/page.html\nText from page.html.\n\n\n\nscan.png\nPicture text in scan.png\n\n\n\n\n\nmemo.docx\nText from memo.docx.\n\n\n\n\n\n\n\n\n\nreport.pdf\n\n\nText from report.pdf.\n\n\n\n\n\n\n\ninner.zip\n\n\ndeep.txt\n\nText in the inner zip.\n\n\n\n\n\n\n","metadata":{"content_type":"application/zip","bytes":40837,"chars":248,"ocr":false}}

The same ZIP as Markdown

In

archive.zip (format=markdown)

Out

{"status":"success","markdown":"# notes.txt\n\nText from notes.txt.\n\n# pages/page.html\n\nText from page.html.\n\n# scan.png\n\nPicture text in scan.png\n\n# memo.docx\n\nText from memo.docx.\n\n# report.pdf\n\nText from report.pdf.\n\n# inner.zip\n\n# deep.txt\n\nText in the inner zip.\n","metadata":{"content_type":"application/zip","bytes":40837,"ocr":false,"chars":234,"format":"markdown","tier":"standard","pages":1,"vlm":false}}

The same ZIP as element JSON

In

archive.zip (format=json)

Out

{"status":"success","elements":[{"type":"heading","text":"notes.txt","level":1,"page":null,"offset":0,"bbox":null},{"type":"paragraph","text":"Text from notes.txt.","page":null,"offset":11,"bbox":null},{"type":"heading","text":"pages/page.html","level":1,"page":null,"offset":33,"bbox":null},{"type":"paragraph","text":"Text from page.html.","page":null,"offset":50,"bbox":null},{"type":"heading","text":"scan.png","level":1,"page":null,"offset":72,"bbox":null},{"type":"paragraph","text":"Picture text in scan.png","page":null,"offset":82,"bbox":null},{"type":"heading","text":"memo.docx","level":1,"page":null,"offset":108,"bbox":null},{"type":"paragraph","text":"Text from memo.docx.","page":null,"offset":119,"bbox":null},{"type":"heading","text":"report.pdf","level":1,"page":null,"offset":141,"bbox":null},{"type":"paragraph","text":"","page":1,"offset":153,"bbox":null},{"type":"paragraph","text":"Text from report.pdf.","page":1,"offset":155,"bbox":null},{"type":"paragraph","text":"","page":1,"offset":178,"bbox":null},{"type":"heading","text":"inner.zip","level":1,"page":null,"offset":180,"bbox":null},{"type":"heading","text":"deep.txt","level":1,"page":null,"offset":191,"bbox":null},{"type":"paragraph","text":"Text in the inner zip.","page":null,"offset":201,"bbox":null}],"metadata":{"content_type":"application/zip","bytes":40837,"ocr":false,"chars":223,"format":"json","tier":"standard","pages":1,"vlm":false}}

An .eml with a body and four attachments

In

mail.eml

Out

{"status":"success","extracted_text":"Text in the mail body.\n\n\n\n\n\n\nText from mail-report.pdf.\n\n\n\n\n\n\n\nText from mail-memo.docx.\n\n\n\n\n\n\n\n\n\nPicture text in mail-scan.png\n\n\n\n\n\n\n\nmail-inside.txt\n\nText from mail-inside.txt.\n\n\n\n\n\n\n\n","metadata":{"content_type":"message/rfc822","bytes":61368,"chars":186,"ocr":false}}

The same .eml as Markdown

In

mail.eml (format=markdown)

Out

{"status":"success","markdown":"Text in the mail body.\n\nText from mail-report.pdf.\n\nText from mail-memo.docx.\n\nPicture text in mail-scan.png\n\nmail-inside.txt Text from mail-inside.txt.\n","metadata":{"content_type":"message/rfc822","bytes":61368,"ocr":false,"chars":153,"format":"markdown","tier":"standard","pages":null,"vlm":false}}

An .mbox with two messages

In

two.mbox

Out

{"status":"success","extracted_text":"Text in the first message.\n\n\n\n\n\nText in the second message.\n\n\n\n\n\n","metadata":{"content_type":"application/mbox","bytes":586,"chars":65,"ocr":false}}

An Outlook .msg with one attachment

In

outlook.msg

Out

{"status":"success","extracted_text":"Outlook subject\n\tFrom\n\tAna Silva\n\tTo\n\tBen Okoye\n\tCc\n\tChi Wang\n\nText in the Outlook body.\nnote.txt\nText in the attached note.\n\n\n\n","metadata":{"content_type":"application/vnd.ms-outlook","bytes":4096,"chars":128,"ocr":false}}

An Outlook .msg inside an Outlook .msg

In

forwarded.msg (the generator builds it; we do not commit it)

Out

{"status":"success","extracted_text":"Outer subject\n\tFrom\n\tAna Silva\n\nText in the outer body.\nInner subject.msg\nInner subject\n\n\t\tFrom\n\n\t\tChi Wang\n\n\n\nText in the inner body.\n\n\n","metadata":{"content_type":"application/vnd.ms-outlook","bytes":4608,"chars":137,"ocr":false}}

A PDF with an attached file

In

attached.pdf

Out

{"status":"success","extracted_text":"\nText on the PDF page.\n\n\n\nText in the embedded file.\n\n\n","metadata":{"content_type":"application/pdf","bytes":1670,"chars":55,"ocr":false}}

What this means for your pipeline

To get one text per file from a ZIP, ask for format=json. Split the elements on the level-1 headings. Each heading is a file path. A nested ZIP gives a flat list, so a path does not tell you the parent.

If you need the file name of an email attachment, send the attachments one by one. You hold the name, so you can keep it beside the text.

If you need the From, To or Subject of an .eml, read them yourself. The next section shows how. An Outlook .msg already returns them.

Do not read metadata.ocr as proof that OCR did not run. Check the text instead. Text in pictures and scans says where OCR runs. Quality checks lists the checks to run on the output.

How to get the missing parts yourself

Run each snippet next to your txtfetch call. Each snippet ran on the sample file named under it.

Read the headers and the attachment names of an .eml

The API drops both. The Python standard library reads them from the file you hold.

Python, run on mail.eml

import email
from email import policy

with open("mail.eml", "rb") as f:
    msg = email.message_from_binary_file(f, policy=policy.default)

for name in ("From", "To", "Cc", "Subject", "Date"):
    print(name + ":", msg[name])

for part in msg.iter_attachments():
    print("attachment:", part.get_filename(), part.get_content_type())

It printed

From: Ana Silva <ana@example.com>
To: Ben Okoye <ben@example.com>
Cc: Chi Wang <chi@example.com>
Subject: Quarterly files
Date: Mon, 19 Oct 2026 09:00:00 +0000
attachment: mail-report.pdf application/pdf
attachment: mail-memo.docx application/vnd.openxmlformats-officedocument.wordprocessingml.document
attachment: mail-scan.png image/png
attachment: mail-bundle.zip application/zip

Split a ZIP response into one text per file

Ask for format=json. Each level-1 heading is a file path. Group the elements after it. This snippet reads the saved ZIP response.

Python, run on responses.json

import json

with open("responses.json", encoding="utf-8") as f:
    response = json.loads(json.load(f)["archive-zip"]["json"])

files = {}  # path -> text
current = None
for element in response["elements"]:
    if element["type"] == "heading" and element.get("level") == 1:
        current = element["text"]
        files[current] = []
    elif current is not None and element["text"]:
        files[current].append(element["text"])

for path, parts in files.items():
    print(path, "->", " ".join(parts) or "(no text)")

It printed

notes.txt -> Text from notes.txt.
pages/page.html -> Text from page.html.
scan.png -> Picture text in scan.png
memo.docx -> Text from memo.docx.
report.pdf -> Text from report.pdf.
inner.zip -> (no text)
deep.txt -> Text in the inner zip.

What we did not test

We tested uploaded files. We did not test a page that txtfetch fetches from a URL. We did not test .pst or .ost mailboxes, or formats such as 7z, tar and RAR. We make no claim about them. We tested a ZIP with an encrypted member. It returns success with the member name only. See Password-protected files.

Read the ZIP page, the email page and the Outlook .msg page for the rest of each format.

Frequently asked questions

Does txtfetch read the files inside a ZIP?
Yes. It returns every file, in archive order. A ZIP inside a ZIP is walked too. A picture inside a ZIP is read by OCR.
Can I tell which file a text came from?
For a ZIP, yes. The path of each file comes back as its own line before its text. With format=markdown or format=json it is a level-1 heading. For email and PDF attachments, no. The file name does not come back.
Do the From, To and Subject lines of an .eml come back?
No. In our probe of production only the body and the attachment text came back. Read the headers from the file with your own code. An Outlook .msg is different. It returns the subject, the sender name, To and Cc.
Does an .mbox come back with its headers?
No. Only the message bodies come back. There is no header and no line between two messages.
Does metadata.ocr tell me a picture inside a ZIP was read?
No. In our probe the picture inside a ZIP was read by OCR, but metadata.ocr said false. Do not use that flag to decide whether OCR ran on an archive.
Why does an .eml with a ZIP attached give me one paragraph?
In format=markdown and format=json the entry name and its text join in one paragraph. Use the plain text output, or send the ZIP on its own, if you need the file names apart.
Does a PDF with an attached file come back whole?
The text of the attached file comes back after the page text. Its file name does not come back.

Watch it run, then take a key.

The playground replays a real recorded response for every sample document.

Open the playground →

Get an API key →