# Allow and block lists

> Control who may send to a Pidgeon address and who it may write to: allow and block lists, reply-only sending, and why a send was refused.

Two lists, one per direction. **Who may write to an address**, and **who that
address may write to**. They are separate endpoints because they answer
different questions and have different consequences — one decides what you are
charged for storing, the other decides what leaves.

Both are scoped to a single address. There is no list that silences a sender
for a whole account over the API, deliberately: a key that administers one
address must not be able to change what fourteen other addresses receive.

## With no rules at all

**An address with no receive rules accepts mail from everybody**, and the spam
filter decides where each message lands. **An address with no send rules may
write to whoever its send policy allows**: anyone
under `open`, the people who wrote first under `reply_only`, and nobody under
`list`. An empty list never blocks mail and never lets anything through on its
own; it hands the decision to the default.

## The order they apply in

**Receiving**, for each message, stops at the first step that decides:

1. **The address must exist.** Mail to an address nobody owns, and no
   routing rule catches, is delivered nowhere; no rule is read.
2. **This address's own rule** about the sender, if one matches.
3. **Otherwise an account-wide rule** from the dashboard, if one matches.
4. **Otherwise the spam filter** files it, by its content and its SPF, DKIM
   and DMARC results.

Within one list, **a rule naming an address beats a rule naming its domain**.
A rule matches the `From:` header, whether or not the message passed
authentication: `inbox` on `acme.com` rescues a forged `acme.com` message
from spam as readily as a real one. Nothing is refused for failing
authentication unless you wrote a rule that refuses it. The
[Firewall](https://pidgeon.ai/docs/firewall.md) verdict is worked out separately and no rule changes
it, so read `firewall` and the authentication results rather than trusting the
folder.

**Sending**, for each recipient:

1. **A matching rule decides**: `block` refuses, `allow` permits. An address
   rule beats a domain rule, so `block acme.com` with `allow bob@acme.com`
   lets Bob through.
2. **Otherwise the send policy decides.**

## Who may write to you

```text
GET    /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/receive-rules
POST   /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/receive-rules   { "action": …, "value": … }
DELETE /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/receive-rules?rule_id=…
```

`value` is an address or a domain and **the string says which**:
`billing@acme.com` matches one person, `acme.com` matches everybody there. You
are not asked to classify it, because a caller asked to classify it will
eventually classify it wrong.

`action` is one of five:

| `action`  | What happens                                                                |
| --------- | --------------------------------------------------------------------------- |
| `reject`  | The message is refused **before it is stored**. No row, no thread, nothing. |
| `spam`    | Stored, and filed as spam.                                                  |
| `trash`   | Stored, and filed as deleted.                                               |
| `archive` | Stored, and skips the inbox.                                                |
| `inbox`   | **Never classify this sender as spam.** The rescue, not a block.            |

```bash
curl -X POST https://pidgeon.ai/api/v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/receive-rules \
-H "Authorization: Bearer $PIDGEON_API_KEY" \
-d '{ "action": "reject", "value": "noreply@spam.example" }'
```

```ts
import { client, identitiesReceiveRulesCreate } from '@pidgeon-ai/sdk';

client.setConfig({ auth: () => process.env.PIDGEON_API_KEY! });

await identitiesReceiveRulesCreate({
path: { id: 'dec7b8d7-e8f0-41d2-a675-3ab627fd5348' },
body: { action: 'reject', value: 'noreply@spam.example' },
});
```

```python
from pidgeon.api.identities import identities_receive_rules_create
from pidgeon.models import IdentitiesReceiveRulesCreateBody

identities_receive_rules_create.sync(
  client=client,
  id="dec7b8d7-e8f0-41d2-a675-3ab627fd5348",
  body=IdentitiesReceiveRulesCreateBody(action="reject", value="noreply@spam.example"),
)
```

### `reject` is the one worth understanding

The other four decide **where** a message goes. `reject` decides whether there
is one at all — it refuses before the insert, so nothing is stored, nothing is
threaded, no storage is charged and **no webhook fires**.

That last part is the reason it exists. `message.spam` is a deliverable event,
so before this an address under sustained abuse was paying storage *and*
webhook deliveries for every message its owner had already said they did not
want. For a mailbox a person reads, `spam` is right — you want to be able to
look. For an address no human will ever open, `reject` is.

**It is not a security control.** The `From:` header is written by the sender,
and anybody blocked can change it for the price of a new address. What `reject`
buys is not paying for mail from senders who have not bothered, which is most
of them. If you need to be certain who sent something, read the
authentication results on the message rather than its `From:`.

### The more specific rule wins

A rule naming an address beats a rule naming its domain, so blocking a whole
domain and letting one person through does what it looks like:

```text
reject  acme.com              — everybody at acme.com
inbox   billing@acme.com      — except billing@, which is never spam
```

**And an address's own rule beats an account-wide one.** Rules made in the
dashboard apply to every address; rules made through this endpoint apply to
one, and the one wins. That ordering is what makes *this address receives
verification codes from one service and nothing else* expressible without
touching anything else you own.

## Who you may write to

```text
GET    /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/send-rules
POST   /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/send-rules   { "effect": "allow" | "block", "value": … }
DELETE /v1/identities/dec7b8d7-e8f0-41d2-a675-3ab627fd5348/send-rules?rule_id=…
```

`effect` rather than `action`, because the outbound side has a verdict rather
than a destination. The same matching applies — address beats domain, adding
replaces.

These rules hang off the address's **send policy**, which `GET` returns beside
them and which is set with `PATCH /v1/identities/{id}`:

| `send_policy` | Who it may write to                                      |
| ------------- | -------------------------------------------------------- |
| `open`        | Anyone.                                                  |
| `reply_only`  | Anyone who has written to it first, plus its allow list. |
| `list`        | Only its allow list.                                     |

**A `block` rule refuses under every policy, including `open`.** A block list
that a setting could switch off is not a block list. An `allow` rule matters
only under `reply_only` and `list`; under `open` everybody is already allowed.

A refusal tells you which kind it was in `error.code`: `blocked` means a rule
said no, `not_permitted` means the policy did. Both are `403`, and the
difference is where you go to fix it — a rule somebody wrote, or the address's
`send_policy`. For a while both answered `forbidden` and the only way to tell
was to read the sentence, which is not a thing to ask of a program.

A refusal is whole. One disallowed recipient refuses the send and nothing
reaches anybody, including the recipients that were fine — see
[sending](https://pidgeon.ai/docs/sending.md) for why that is not `rejected`.

## Reply is not a third list

An address that may receive from somebody and must not answer them is **one
`block` rule** on the send side. There is no separate reply list, because it
would be a second way to express a rule that already exists.

`reply_only` is the opposite case — may write, but only to people who wrote
first — and it is a policy rather than a list.

## Reading the rules from an agent

The MCP server exposes both as read-only tools — `list_receive_rules` and
`list_send_rules` — so an agent asking *why did nothing arrive* or *why was
that refused* can answer it. Writing a rule is deliberately not a tool: an
agent that can edit its own lists can quietly stop its own mail.
