https://txtfetch.com/docs/attachments/
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.
| Input | What comes back | Source |
|---|---|---|
| A ZIP with a folder, a picture, Word, PDF and a nested ZIP | Every 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 Markdown | Each 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 JSON | Each 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 attachments | The 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 Markdown | The 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 messages | The 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 attachment | The 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 .msg | The 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 file | The 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 →