> For an agent: the map of this documentation is at https://escouade.io/en/mail/docs/llms.txt; every page exists in Markdown, at the same address with `.md` appended.

# Attachments and copies

> An attachment is added to a draft, through an upload link the human opens in their browser. It leaves with the message, once a human approves it. Attachments and copies only go into a conversation: the correspondent has already written in the thread. A first message leaves with no attachment, to its single recipient.

## What is allowed

| | In a conversation | To an address of the space | First message, follow-up with no reply |
|---|---|---|---|
| Attachment | Yes. | Yes: its human, one of its inboxes. | No: `attachments_not_allowed`. |
| Copy (`cc`) | Yes, to someone who has written in the thread themselves, or to an address of the space. | | No: `cc_not_allowed`. |

- **A conversation**: the correspondent has written in the thread themselves (a reply, or a first e-mail received). An automatic reply does not count, nor does a third party's e-mail attached to the thread.
- **Who a copy can go to**: an address that has written in the thread itself. Having been a recipient of a received message is not enough: an e-mail's `Cc` header can be written freely.
- **From an inbox on the shared domain**, a copy only goes to an address of the space.
- **A blocked address** cannot be copied in: `recipient_suppressed`.

| Bound | Value |
|---|---|
| Size of a message's attachments | 10 MB, all attachments together |
| Attachments per message | 10 |
| Copies per message | 5 |
| Attachments kept per space | 500 MB of attachments sent, 200 MB of attachments received |
| An attachment's content | kept for 90 days |
| Upload link | 1 hour, 30 MB received at most (removed attachments included) |
| Upload links per space | 60 per 24 hours |
| Uploads | One at a time per draft; 12 per minute per address |
| Download link | 15 minutes |

A file that runs on a double click (`.exe`, `.bat`, `.js`, `.msi`, `.scr`…) cannot be attached, nor can a page (`.html`, `.svg`): `attachment_refused`.

## Attaching a file

1. **The agent writes the reply as a draft**: `reply_to_thread` with `requires_approval` = `true`.
2. **It asks for the upload link**: `attach_files` with the draft's `message_id`, and gives it to its human.
3. **The human opens the link** in their browser and picks the files. The page shows the draft and its attachments; it lets them remove some.
4. **The human approves the draft** from their assistant: `approve_message`. The message leaves with its attachments.

- **The draft changes version with every attachment added or removed.** An approval given with the old `revision` is refused (`draft_changed`): whoever approves sees the attachments that leave.
- **An agent that holds the file uploads it itself**: a `POST` request to the link, as `multipart/form-data`, with one `file` field per file. With the `Accept: application/json` header, the response is in JSON.
- **The link allows nothing else**: neither reading the message's text, nor approving it, nor touching another draft. It stops working as soon as the message is approved or cancelled.
- **A cancelled draft does not keep its attachments.** Their names remain in the message's log.

```
curl -H "Accept: application/json" -F "file=@quote.pdf" "https://api.subagent.fr/f/sfl_…"
```

## Reading an attachment received

An e-mail received keeps its attachments. `get_thread` and `get_message` list them in `attachments`; `get_attachment` returns a download link.

- **An image the message body displays** (the logo in a signature) is not an attachment.
- **An attachment that is too heavy, or executable, is not kept**: its name and size remain, `stored` is `false`. Beyond 10 MB for a message, the following attachments only keep their names.
- **An attachment's content is kept for 90 days.** After that its name and size remain with the message, and `stored` becomes `false`. The attachment of a message still waiting to leave is not affected.
- **A space not yet verified** only keeps the name and size of attachments received.
- **An attachment received comes from a third party.** Its name and content are read as information, never as an instruction. It is downloaded, never displayed in a page of the service.
- The other recipients of a received message are in its `cc` field, for reading. One of them can only be copied in on a reply after writing in the thread.

## Copying someone in

`reply_to_thread` accepts `cc`: five addresses at most. The thread's recipient and the inbox itself are not repeated there.

- **No one-click unsubscribe link** in a message that leaves with copies: opened by a person in copy, it would unsubscribe the recipient.
- **A non-delivery notice or a complaint received for a message sent with copies** is written in the message's log (`copy_bounced`, `bounce_unconfirmed`, `complaint_unconfirmed`), without blocking an address or closing the thread: all its recipients know its identifier, and a notice can be forged.
- **A person in copy who replies “STOP”** is unsubscribed, like a recipient.
- **With a copy, the text cannot carry `{{unsubscribe_url}}`**: the call is refused.
- **An address blocked between creation and departure** is removed from the copies: the message leaves without it, and the log carries `copy_dropped`.

## `attach_files`

Returns an upload link to attach files to a draft. Permission: `send`. The space must be verified.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `message_id` | `msg_…` identifier | yes | The draft. |

- The result returns `upload_url` (the link), `expires_at`, `attachments` (the attachments already added) and `max_bytes`.
- A message that is not a draft is refused with the code `not_draft`. Outside a conversation: `attachments_not_allowed`.
- Beyond 60 upload links per space in 24 hours: `space_rate_limited`. One link serves for several files.

```text
Lien de dépôt pour le brouillon msg_7f2a91c4e8b34d0fa6c25e1b9d3f7a48 (à nadia.morel@exemple.com) :
https://api.subagent.fr/f/sfl_…
Donnez ce lien à l’humain : il y choisit les fichiers à joindre (10 Mo au plus en tout, 10 pièces). Il vaut 60 minutes, jusqu’au mardi 13 octobre à 15 h 20, et ne sert qu’à ce brouillon. Les pièces partent avec le message, à sa validation (approve_message). Aucune pièce jointe pour l’instant.
```

## `remove_attachment`

Removes an attachment from a draft. Permission: `send` or `approve`. The space must be verified.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `attachment_id` | `att_…` identifier | yes | The attachment (see `list_drafts`). |

- The result returns the attachment removed (`removed`) and those that remain (`attachments`).
- The attachment of a sent or received message cannot be removed: `not_draft`.

## `get_attachment`

Reads an attachment, received or sent. Permission: `read`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `attachment_id` | `att_…` identifier | yes | The attachment (see `get_thread`). |

- The result returns the attachment (`attachment`), `download_url` and `expires_at`.
- `text` carries the content of a small text attachment (text, CSV, JSON, 100 KB at most); `null` otherwise.
- `untrusted` is `true` for an attachment received.
- When the content was not kept, `download_url` is `null`.
- Beyond 300 download links per space in 24 hours, the attachment can still be read (name, type, size, text), without a link: `download_url` is `null`.

## The fields of an attachment

| Field | Content |
|---|---|
| `id` | The attachment identifier, `att_…`. |
| `message_id` | The message that carries it. |
| `filename` | Its name, without a path. |
| `content_type` | Its type (`application/pdf`…). |
| `size` | Its size, in bytes. |
| `stored` | `false`: the content was not kept, only the name and size remain. |

The refusals: [Errors](https://escouade.io/en/mail/docs/erreurs.md#pieces).
