# Shield rules

> Allow, block, or reroute mail as it arrives — before it is stored or delivered.

A **shield rule** is judged against every inbound message the moment it arrives,
before anything else happens to it. Rules are how you stop paying attention to
mail you never wanted — and how you stop paying for it.

Each rule is a set of conditions and one action:

```json
{
  "name": "Drop flagged spam",
  "action": "block",
  "conditions": [
    { "field": "spam_verdict", "operator": "equals", "value": "FAIL" }
  ]
}
```

## The two rules about rules

<Steps>
  <Step title="All conditions must match">
    Conditions inside a rule are ANDed. There is no OR and no nesting — for
    "either/or", write two rules. This is a deliberate limit: a filter you can't
    predict is worse than no filter when the consequence is deleted mail.
  </Step>
  <Step title="The first match wins">
    Rules are evaluated in order, top to bottom. The first rule whose conditions
    all match decides the outcome, and no later rule runs.
  </Step>
</Steps>

Order is therefore the most consequential property a rule has. It is also the
least visible one, which is why the console leads each row with its position.

## Actions

<CardGroup cols={3}>
  <Card title="Allow" icon="circle-check">
    Deliver normally and stop. Its purpose is to sit **above** a broad block and
    carve an exception out of it.
  </Card>
  <Card title="Block" icon="circle-x">
    Drop the message permanently. Not stored, not delivered — but still counted.
  </Card>
  <Card title="Route" icon="arrow-right">
    Deliver to endpoints you name **instead of** the address's own.
  </Card>
</CardGroup>

### Block is a hard drop

<Warning>
  A blocked message is never written down. There is no quarantine folder, no
  body to inspect later, and nothing to restore — only a record of the sender,
  recipient, and subject.
</Warning>

That is the point rather than a limitation: mail you told us to throw away
shouldn't cost you storage, retention, or a delivery attempt. If you want the
message kept, use **route with no endpoints** instead — that quarantines it,
leaving it readable over the API and MCP while delivering it nowhere. It's also
the safe way to try out a new rule before letting it destroy anything.

### Blocked mail still counts against your allotment

A rule cannot give back the one cost that was already paid. By the time any
rule runs, the message has been accepted, parsed, and billed for as a received
email — so it is metered as one, exactly like a message you keep.

What blocking saves is everything *after* that: storage, the retention window,
delivery attempts, and the noise in your inbox. What it cannot save is the fact
that the message arrived.

<Warning>
  On the Free plan, where reaching the allotment stops mail being stored at all,
  a flood of junk will exhaust your allotment whether or not a rule blocks it.
  A block rule keeps the junk out of your inbox; it does not keep it out of your
  quota.
</Warning>

### Route replaces, it doesn't add

A `route` rule sends the message to its endpoints *instead of* wherever the
address would have sent it. "Send receipts to the billing webhook" means instead
of, not as well as. The same fan-out limit applies as everywhere else: at most
one webhook and one email destination per rule.

## Scope

A rule applies to the whole account by default. Set `domainId` to narrow it to
one domain, or `addressId` to narrow it to a single address route. You can't set
both.

Narrowing only limits which mail a rule *judges*. It never changes where mail
that isn't judged would otherwise go, and it does not make a narrower rule beat
a broader one — only position does that. An account-wide `allow` sitting above a
domain-wide `block` wins, which is exactly the arrangement you usually want.

## Fields and operators

| Field | What it looks at | Operators |
| --- | --- | --- |
| `from` | The From header *and* the bare address inside it | `contains`, `not_contains`, `equals`, `ends_with` |
| `from_domain` | Domain half of the sender | `equals`, `ends_with`, `contains` |
| `to` | The recipient being judged (see below) | `equals`, `contains`, `starts_with` |
| `subject` | Subject line | `contains`, `not_contains`, `equals`, `starts_with`, `ends_with` |
| `body` | Text and HTML bodies together, first 256 KB | `contains`, `not_contains` |
| `has_attachment` | Whether there are any attachments | `is_true`, `is_false` |
| `attachment_name` | Any attachment's filename | `contains`, `ends_with`, `equals` |
| `attachment_type` | Any attachment's content type | `equals`, `contains`, `starts_with` |
| `spam_verdict` | What SES thought (`PASS` / `FAIL` / `GRAY`) | `equals` |
| `spf_verdict` | SPF result | `equals` |
| `dkim_verdict` | DKIM result | `equals` |

All comparisons are case-insensitive. Attachment fields match if **any**
attachment matches — except `not_contains`, which must hold for every one, so
"no `.exe` attachment" isn't satisfied by an innocent second file.

<Note>
  There is deliberately no regular-expression operator. Patterns you write would
  run against text a stranger chose, on every message — that combination is a
  denial-of-service waiting to happen, and `contains` covers what filters
  actually need.
</Note>

## One message, several recipients

A single message can name several of your addresses. Rules are evaluated **per
recipient**, and the outcomes are reconciled:

- If a block covers **every** recipient, the message is dropped.
- If it covers only **some**, the message is still stored for the others. The
  blocked recipients simply lose their delivery.

One recipient's rule can never delete another recipient's mail.

## Reviewing what was blocked

Because a block leaves nothing behind, every one writes a record of the
envelope — sender, recipient, subject, which rule caught it, and when. Read it
in the console under **Shield → Recently blocked**, or over MCP with
`list_shield_blocks`.

These records expire on the same retention schedule as stored mail, and survive
the deletion of the rule that produced them, so the log of what a since-removed
rule was catching stays readable.

## Where shield sits in the pipeline

<Steps>
  <Step title="Virus check">
    Mail SES flags as carrying a virus is dropped before rules are consulted.
  </Step>
  <Step title="Shield rules">
    Evaluated here. A blocked message is dropped and metered — it never reaches
    storage or delivery, but it counts as received.
  </Step>
  <Step title="Quota">
    Every plan hard-blocks past its monthly allotment. Paid plans can turn on
    pay-as-you-go to keep receiving and be billed for the overage instead.
  </Step>
  <Step title="Store and deliver">
    The message is written down and fanned out to its endpoints — a shield
    `route` verdict overriding the address's own destinations.
  </Step>
</Steps>

## Limits

| Plan | Rules |
| --- | --- |
| Free | 3 |
| Basic | 10 |
| Pro | 50 |
| Scale | 200 |

A cap rather than a paid feature: filtering is how an inbox stays usable, and
putting it behind a paywall would price the fix out of reach of exactly the
accounts drowning in junk. The cap only bounds how much matching work each
message can trigger.
