Email

Captain indexes an email as one text document for the message and one document per attachment, linked by relations and sharing the same metadata. This page covers what one email becomes, how the parts are linked, the metadata keys, identity and re-indexing, and examples.

This is useful for

  • Supplier and customer correspondence, where the answer is in the attachment rather than the message
  • Threads that need to be searched as a whole, from the first message to the last reply
  • Mailboxes exported from Outlook or an archive, where attachments must keep the sender and date they arrived with
  • Collections where deleting or re-sending an email should update its parts in place

What one email becomes

Send a .msg or .eml through any indexing route (upload, raw bytes, or a cloud storage connector). Captain produces:

  • One text document for the message: From, To, Cc, Date and Subject, then the body, with HTML rendered to text.
  • One document per attachment, parsed by its own lane and billed as its type.
  • A forwarded message attached to the email is a body of its own, linked to the outer message. Forwards nested deeper than one level are skipped.

Not indexed: inline signature images, empty attachments, a second copy of identical bytes within the same email, and attachment types Captain does not support.

How an email is chunked

1

Each part uses its own lane

A PDF attachment is chunked as a document, with page ranges. A workbook is chunked as a sheet, with row bands. An image is chunked as an image. The body is a short text document.

2

Relations

email_attachment links the body to each attachment. email_reply_to links a reply to the message it answers, taken from In-Reply-To and then the last References id. Both are chunk relations with one row each, readable from either end with relation_direction: "both". A reply whose parent arrives in a later job gets no relation but shares email_thread_id.

3

Metadata on every part

All parts carry email_role, email_message_id, email_thread_id, email_from, email_subject, email_date (ISO 8601 UTC) and email_source_uri. The body also carries email_in_reply_to and email_attachment_count. Attachments also carry email_parent_identity, email_attachment_name and email_content_sha256. A header the email did not have is stored as an empty string rather than omitted, so a filter on it does not silently exclude a part. The job’s custom_metadata is copied onto every part. Text values are cut at 1,024 characters.

4

Identity and re-indexing

The body’s identity is msg://<Message-ID>. An attachment’s is msg://<Message-ID>#att/<file name>, with ~2 for a second attachment of the same name. Neither depends on the file’s location or the job, so re-indexing an email replaces its parts in place. An attachment that is no longer in the email is removed, with its relation rows. Deleting the email from a synced bucket, or deleting the body by document id, removes every attachment. A message with no Message-ID uses its file location as identity.

Examples

Goal: return the attached quote together with the message that sent it, searching by the message text.

request
{
"query": "revised quote for the pumps",
"limit": 3,
"include": {
"metadata": true,
"related_chunks": true
},
"relation_types": [
"email_attachment"
],
"relation_direction": "both"
}
result (one item)
{
"text": "From: Anna Kowalska <anna@supplier.test>\nDate: 2026-09-01T09:14:00+00:00\nSubject: RE: Pump order, revised quote\n\nPlease find the revised quote attached, valid to 30 September...",
"custom_metadata": {
"email_role": "body",
"email_thread_id": "thread-root@example.com",
"email_attachment_count": 1
},
"related_chunks": [
{
"relation_type": "email_attachment",
"text": "Quotation Q-2026-0912 ... 3 x centrifugal pumps, 40 mm, 3,985.00 each, delivery 4 weeks ...",
"custom_metadata": {
"email_role": "attachment",
"email_attachment_name": "quote-rev2.pdf"
}
}
]
}

The body matched the query. The email_attachment relation adds the PDF’s passage to the result, and the attachment carries the sender and date of the email.

Goal: limit a search to one reply chain.

request
{
"query": "delivery date for the pumps",
"filter": {
"email_thread_id": {
"$eq": "thread-root@example.com"
}
},
"include": {
"metadata": true,
"relations": true
},
"relation_direction": "both"
}

Every message and attachment in the thread shares email_thread_id.

Goal: return only the attachments of one message.

request
{
"query": "unit price",
"filter": {
"email_parent_identity": {
"$eq": "msg://quote-2026-09-01@example.com"
},
"email_role": {
"$eq": "attachment"
}
}
}

Every attachment carries its body’s identity in email_parent_identity.

Goal: return messages and attachments from one sender within a date range.

request
{
"query": "pumps",
"filter": {
"email_from": {
"$eq": "Anna Kowalska <anna@supplier.test>"
},
"email_date": {
"$gte": "2026-09-01T00:00:00+00:00",
"$lt": "2026-10-01T00:00:00+00:00"
}
}
}

Attachments carry email_from and email_date as well as the body, so the filter returns both. email_date is an ISO 8601 string, so the range comparison is correct.

See Supported File Types for the extension table and Advanced Search & Relations for relation traversal.

© 2026 Captain