# Holds & injection scanning

Source: https://docs.simail.ai/holds/

> Why inbound mail is held, what agents see while it is held, how prompt-injection scanning works, and how humans release or reject mail.

A **hold** keeps a message away from your agent until a human looks at it. Holds are decided by
deterministic checks **before** the message is stored, so there is never a moment when a suspicious
message is readable by an agent.

## What a held message looks like to an agent

On every channel (REST, MCP, webhooks, `GET /v0/events`, WebSocket) a held message is
**metadata only**:

```json
{
  "message_id": "msg_…",
  "thread_id": "thr_…",
  "inbox_id": "agent-7k2m@simail.email",
  "from": "unknown@example.com",
  "timestamp": "2026-10-18T17:02:11Z",
  "size": 18422,
  "labels": ["received", "held"],
  "hold_reasons": ["injection_suspected"],
  "held": true,
  "trust_envelope": { "computed_by": "none", "untrusted": true }
}
```

`timestamp` is when simail received the message (the same field as on every message).

Subject, body, attachments and raw source are withheld. Listings include held messages only for keys
with `hold_metadata_read`. Held mail never wakes your agent, and search matches only its metadata.
Fetching the body of a held message does not work; neither does the attachment or raw endpoint.

Your agent cannot release mail: `POST /v0/inboxes/{inbox_id}/messages/{message_id}/agent-armor/release`
returns `403 human_required`, and the MCP server has no release tool.

## Why mail is held

| `hold_reasons` | Cause |
|---|---|
| `rate_limited` | more than 50 messages per hour from one sender to one inbox, or 2,000 per hour to the organization (far above that, extra copies are counted and dropped) |
| `unparseable` | the message could not be parsed safely |
| `injection_suspected` | the prompt-injection score reached the threshold (below) |
| `otp_limit` | external organizations only: too many verification-code messages for the organization today (more than 10, or from more than 3 sending domains). Verification-code mail to the internal tenant goes to your agent as usual |
| `sensitive_material` | the message looks like regulated or privileged material (for example legal case material) that simail does not accept |
| `enrichment` | a later, optional check (attachment text, model-based scoring) found a problem |

## Prompt-injection scanning

The scanner is a set of deterministic heuristics, run on the subject, body and HTML of every inbound
message:

- instruction phrasing aimed at an AI ("ignore previous instructions", role or system prompts), in
  English and Chinese;
- hidden text: zero-width characters, CSS-hidden content, text in the same colour as the background;
- fake system prompts and tool-call syntax;
- very long encoded blobs.

The result is `trust_envelope.injection_score` between 0 and 1. At or above
`injection.hold_threshold` (0.7 by default; your policy can lower it, never raise it above the
platform's value) the message is held.

Two optional checks are **off by default** and can be turned on by a human: text extraction from
attachments, and model-based injection scoring. Both run after the message is stored and can only
**add** a hold, never remove one. A message that is held later disappears from search results and
produces a `message.held` event.

:::caution
No filter catches every prompt injection. A message that is not held is still **untrusted input**.
Never follow instructions found in an email.
:::

## Releasing and rejecting (humans)

In the console, a human with access to the inbox opens **Held** and chooses:

- **Release**: the message becomes a normal message. Its body is indexed for search, and a
  `message.released` event is published (which can wake your agent).
- **Reject**: the message stays out of reach of agents and is deleted with the trash.

Both require re-verification within 5 minutes. A human can also add the sender to a block list.

## Events

| Event | When |
|---|---|
| `message.held` | a message was stored as held (metadata only) or held later by an optional check |
| `message.released` | a human released it |
