> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ruber.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Attachments

> List what is attached to a message and download the bytes, with the content-type rules that keep a hostile file from running in your browser.

Two endpoints: what is attached, and the file itself. Both take the same
`mailbox` (required) and `folder` parameters as the
[message endpoints](/api/messages).

**Required permission for both:** Attachments → Read (`attachments:read`)

<Note>
  **Inline parts are not attachments.** A signature logo, a tracking pixel,
  anything the HTML references with `cid:` — those are part of the rendered body
  and are left out of these endpoints.

  Listing them would turn every marketing email into a pile of phantom downloads,
  and an integration saving "all attachments" would fill a disk with spacer GIFs.
</Note>

## List attachments

```
GET https://app.ruber.me/api/v1/messages/{uid}/attachments
```

```bash theme={null}
curl "https://app.ruber.me/api/v1/messages/55/attachments?mailbox=you@yourdomain.com&folder=inbox" \
  -H "Authorization: Bearer rk_your_key_here"
```

```json theme={null}
{
  "data": [
    {
      "index": 0,
      "filename": "invoice-august.pdf",
      "content_type": "application/pdf",
      "size": 48213
    }
  ]
}
```

Metadata only — deciding whether to fetch a 12 MB PDF should not require
fetching it. The same array appears on
[`GET /v1/messages/{uid}`](/api/messages#get-one-message), so if you are already
reading the message you do not need this call.

`index` is this list's own ordering, stable for as long as the message is: MIME
parts do not reorder themselves. It is what the download endpoint takes.

## Download an attachment

```
GET https://app.ruber.me/api/v1/messages/{uid}/attachments/{index}
```

Returns the **raw bytes**, not JSON.

```bash theme={null}
curl "https://app.ruber.me/api/v1/messages/55/attachments/0?mailbox=you@yourdomain.com&folder=inbox" \
  -H "Authorization: Bearer rk_your_key_here" \
  -o invoice-august.pdf
```

| Response header          |                                                                  |
| ------------------------ | ---------------------------------------------------------------- |
| `Content-Type`           | The file's type, **or `application/octet-stream`** — see below.  |
| `Content-Disposition`    | `attachment`, with the filename in both ASCII and RFC 5987 form. |
| `Content-Length`         | Size in bytes.                                                   |
| `X-Content-Type-Options` | `nosniff`.                                                       |
| `X-Ruber-File-Risk`      | `executable` or `macro`, when applicable. Absent otherwise.      |

### The content type is ours, not the sender's

Every byte here describes something a stranger emailed you. A part declaring
itself `text/html` or `image/svg+xml` and served back verbatim is a script that
runs wherever it is opened.

So the declared type is only honoured when it is plainly inert — images, audio,
video, `text/plain`, `application/pdf`. **Everything else is served as
`application/octet-stream`**, with `nosniff` so a browser cannot guess its way
around the decision. The file itself is unchanged; only the label is.

If you need the sender's declared type, read `content_type` from the listing
above — that one is reported as-is.

### `X-Ruber-File-Risk`

Present when the filename's extension is one that executes: `.exe`, `.js`,
`.lnk`, `.iso` and similar get `executable`; macro-bearing Office formats get
`macro`.

It is a header rather than a refusal. Ruber's mail plane already scans inbound
mail with ClamAV and rejects what it recognises, so this is a second layer for
what a scanner cannot catch. The web app uses the same judgement to warn a
person before they download; warning a program in prose achieves nothing, and
refusing the download would break the legitimate case of an integration
archiving everything that arrives. So the judgement travels as a header your
code can act on, and the file still comes.

**Do not execute what arrives here**, and be careful about writing it to a path
derived from `filename` — the name comes from the sender and may contain
separators or traversal sequences.

## Errors

| Code               | HTTP  | Cause                                                                                         |
| ------------------ | ----- | --------------------------------------------------------------------------------------------- |
| `invalid_request`  | `400` | Missing `mailbox`, an unknown `folder`, a non-numeric `uid` or `index`, or a Private mailbox. |
| `unauthorized`     | `401` | Missing, malformed, unknown or revoked key.                                                   |
| `forbidden`        | `403` | The key lacks `attachments:read`.                                                             |
| `not_found`        | `404` | No such mailbox, no message with that uid in that folder, or no attachment at that index.     |
| `rate_limited`     | `429` | Over 600 requests in a minute.                                                                |
| `mail_unavailable` | `502` | The mail host did not answer. Transient; retry with backoff.                                  |

Errors are JSON even though a successful response is not.

See [Errors](/api/introduction#errors) for the full response shape.
