> For a complete page index of the Captain API documentation, fetch https://docs.captain.dev/llms.txt?excludeSpec=true

# Email

> How Captain indexes .msg and .eml files. The message becomes a text document, each attachment becomes a document of its own, and relations and metadata keep the thread together.

## Agent Quick Reference - Email

* Extensions: `.msg` (Outlook), `.eml` (RFC 822). Sent through any indexing route or connector.
* One email becomes one text document for the message (`From`, `To`, `Cc`, `Date`, `Subject`, then the body) plus one document per attachment, each parsed as its own type and billed as its type.
* Relations: `email_attachment` (body to each attachment), `email_reply_to` (reply to parent, from `In-Reply-To` / `References`). One row each, readable from either end with `relation_direction: "both"`.
* Metadata on every part: `email_role`, `email_message_id`, `email_thread_id`, `email_from`, `email_subject`, `email_date` (ISO 8601 UTC), `email_source_uri`. Body only: `email_in_reply_to`, `email_attachment_count`. Attachments only: `email_parent_identity`, `email_attachment_name`, `email_content_sha256`. Absent header = empty string, never a missing key.
* Skipped: inline signature images, empty attachments, exact duplicate bytes within one email, unsupported types, a forward nested deeper than one level.
* Identity: body `msg://<Message-ID>`, attachment `msg://<Message-ID>#att/<file name>`. Re-indexing replaces parts in place and removes attachments no longer present. Deleting the email, or the body by document id, removes its attachments.

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

#### Each part uses its own lane

A PDF attachment is chunked as a [document](/guides/file-types/documents), with page
ranges. A workbook is chunked as a [sheet](/guides/file-types/sheets), with row bands. An
image is chunked as an [image](/guides/file-types/images). The body is a short text
document.

#### 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`.

#### 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.

#### 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

#### Find a quote by what the email said about it

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

**`request`**

```json title="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)`**

```json title="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.

#### Search one thread

Goal: limit a search to one reply chain.

**`request`**

```json title="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`.

#### List one email's attachments

Goal: return only the attachments of one message.

**`request`**

```json title="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`.

#### Everything from one sender in a month

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

**`request`**

```json title="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](/guides/supported-file-types#email) for the extension table and
[Advanced Search & Relations](/guides/advanced-querying) for relation traversal.