> ## Documentation Index
> Fetch the complete documentation index at: https://ctrlrun.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# An outbound-email agent with external-recipient approval

> An agent sends email: messages inside the company go on their own, any message to an external domain waits for a human who sees the exact recipient.

An assistant drafts and sends email on a team's behalf. Internal mail is routine. Anything
leaving the company waits for a person, who sees the exact recipient, and the approval is
bound to that recipient: an agent that re-plans the address after the yes finds its approval
matches nothing.

## The policy

```yaml runnable theme={null}
schema: ctrlrun.policy/v2

actions:
  email.send:
    effect: "email:{message_id}"
    rules:
      - when: { to_domain_eq: "example.com" }
        decision: allow
      - decision: approve
```

## The code

```python runnable file=main.py theme={null}
from pathlib import Path

import ctrlrun
from ctrlrun import Control, Policy, SQLiteStateStore

HERE = Path(__file__).resolve().parent
STATE = HERE / ".ctrlrun"
STATE.mkdir(exist_ok=True)
for name in ("state.db", "state.db-wal", "state.db-shm"):
    (STATE / name).unlink(missing_ok=True)

sent: list[str] = []


def smtp_send(message_id: str, to: str) -> str:
    sent.append(to)
    return "sent"


store = SQLiteStateStore(STATE / "state.db")
control = Control(Policy.from_file(HERE / "ctrlrun.yaml"), store)


@ctrlrun.protect("email.send", effect="email:{message_id}", control=control)
def send(message_id: str, to: str, to_domain: str, subject: str) -> str:
    return smtp_send(message_id, to)


with ctrlrun.context(agent="assistant"):
    print(
        "to a colleague:",
        send(message_id="m_1", to="li@example.com", to_domain="example.com", subject="Q3 numbers"),
    )

    try:
        send(message_id="m_2", to="press@partner.co", to_domain="partner.co", subject="Q3 numbers")
    except ctrlrun.ApprovalRequired as pending:
        print("to partner.co: a human decides:", pending.request_id)
        request_id = pending.request_id
    else:
        raise SystemExit("external mail went out without a human")

    store.grant_approval(request_id, "human:li@example.com")
    with ctrlrun.with_approval(request_id):
        # The agent re-plans the recipient after the yes.
        try:
            send(
                message_id="m_2",
                to="tips@journalist.example",
                to_domain="journalist.example",
                subject="Q3 numbers",
            )
        except ctrlrun.ApprovalMismatch:
            print("to a different address with the same approval: refused")
        else:
            raise SystemExit("an approval for one recipient sent mail to another")
        print(
            "to partner.co, as approved:",
            send(
                message_id="m_2",
                to="press@partner.co",
                to_domain="partner.co",
                subject="Q3 numbers",
            ),
        )

print("messages sent:", sent)
```

## What the agent sees

```text theme={null}
to a colleague: sent
to partner.co: a human decides: apr_…
to a different address with the same approval: refused
to partner.co, as approved: sent
messages sent: ['li@example.com', 'press@partner.co']
```

The approval hashed `to`, `to_domain`, `subject` and `message_id` together. Any of them changing
after the yes is a different action.

## The receipt

```bash runnable theme={null}
ctrlrun receipts --last 4
```

## When an AMBIGUOUS appears

An SMTP submission that timed out may have been accepted. Check the provider's message log for
`m_N`, then `ctrlrun resolve email:m_N --committed` or `--failed`. The effect key is the message
id, so a resend of the same message is one effect and a new message is a new one.

## Next

* [A customer-notification agent](/cookbook/customer-notification-agent).
* [Approval binding](/concepts/approval-binding) · [Get started](/get-started/quickstart) · [Why](/why).


## Related topics

- [Cookbook](/cookbook/index.md)
- [A customer-notification agent](/cookbook/customer-notification-agent.md)
- [Approval binding](/concepts/approval-binding.md)
- [Approvals in Slack via webhook](/cookbook/slack-approvals.md)
- [Effect keys](/concepts/effect-keys.md)
