# Attachments and raw email

Choose how an inbound route sends attachments to your application. Lettermint can include the file in the webhook or provide a temporary signed URL.

| Mode | Choose it when |
| --- | --- |
| **Base64 encoded** | You want the file in `content`. This option is suitable for small attachments and makes the webhook larger. |
| **Signed URLs** | You process files in a worker. Download each file from `url` before `expires_at`. |

Each attachment uses one payload shape:

- A Base64 attachment contains `content`. It does not contain `url` or `expires_at`.
- A signed URL attachment contains `url` and `expires_at`. It does not contain `content`.

## Choose an attachment mode

1. Open the inbound route and select **Settings**.
2. Find **Attachments**.
3. Select **Base64 encoded** or **Signed URLs**.
4. Select **Save changes**.

This setting changes attachments only. Every `message.inbound` payload includes a signed URL for the original email.

## Decode an inline attachment

```json
{
  "filename": "invoice-4821.pdf",
  "content_type": "application/pdf",
  "size": 18342,
  "content_id": null,
  "content": "JVBERi0xLjcKJc..."
}
```

Decode `content` from Base64 before you scan or store it. Base64 is an encoding method. It is not encryption.

```typescript
type InlineAttachment = {
  filename: string
  content_type: string
  size: number
  content_id: string | null
  content: string
}

function decodeInlineAttachment(attachment: InlineAttachment): Buffer {
  const bytes = Buffer.from(attachment.content, 'base64')

  if (bytes.length !== attachment.size) {
    throw new Error('Attachment size does not match the webhook metadata')
  }

  return bytes
}
```

## Download an attachment from a signed URL

```json
{
  "filename": "invoice-4821.pdf",
  "content_type": "application/pdf",
  "size": 18342,
  "content_id": null,
  "url": "https://storage.lettermint.co/inbound/attachments/...?expires=...&signature=...",
  "expires_at": "2026-09-03T10:15:30+00:00"
}
```

Lettermint keeps the stored attachment for 28 days. The signed URL expires at the end of this period.

Download the file from a background worker. Store it in storage that you control if you need it for more than 28 days.

```typescript
type UrlAttachment = {
  filename: string
  content_type: string
  size: number
  content_id: string | null
  url: string
  expires_at: string
}

async function downloadAttachment(attachment: UrlAttachment): Promise<Buffer> {
  if (Date.parse(attachment.expires_at) <= Date.now()) {
    throw new Error('Attachment URL has expired')
  }

  const response = await fetch(attachment.url, { redirect: 'error' })
  if (!response.ok) {
    throw new Error(`Attachment download failed with ${response.status}`)
  }

  const bytes = Buffer.from(await response.arrayBuffer())
  if (bytes.length !== attachment.size) {
    throw new Error('Attachment size does not match the webhook metadata')
  }

  return bytes
}
```

Treat each signed URL as a temporary secret. Do not send it to browsers, analytics tools, error trackers, or application logs.

## Access the original email

The `raw` object is always present. It contains a URL for the original RFC 5322 message in `.eml` format:

```json
{
  "raw": {
    "url": "https://storage.lettermint.co/inbound/raw/...?expires=...&signature=...",
    "expires_at": "2026-09-03T10:15:30+00:00"
  }
}
```

Lettermint keeps the raw message for 28 days. The raw message URL expires at the end of this period.

Use the raw message for original headers, MIME boundaries, or content that is not in the parsed fields. Use the parsed webhook fields for normal application logic.

## Store files safely

Attachment metadata is untrusted. Apply these controls before you make a file available:

- Create your own storage key. Do not use `filename` as a file path.
- Remove path separators and control characters from the display filename.
- Detect the file type from the file bytes. Do not trust `content_type` or the filename extension.
- Scan the decoded or downloaded file for malware.
- Apply your size and file type rules before you store the file.
- If possible, serve stored files as downloads from a separate origin.
- Do not put signed URLs or attachment content in logs.

An inline image can have a `content_id` that matches a `cid:` reference in the HTML body. Sanitize the HTML first. Then map only known content IDs to scanned files.

:::warning
Lettermint accepts an inbound message with a maximum total size of 25 MB. This limit includes headers, bodies, MIME encoding, and attachments. Base64 content makes the webhook larger than the decoded files. Configure your web server and queue limits for this increase.
:::

See the [`message.inbound` reference](/platform/webhooks/events#messageinbound) for the complete attachment and raw-message fields.
