---
name: mailprotector
description: "Manage the ACG Mailprotector CloudFilter email-security gateway: search/release held mail, mail-flow logs, allow/block rules; releases/rule-changes gated --confirm. Triggers: mailprotector, cloudfilter, held/quarantined mail, release email, allow/block rule, INKY."
---

# Mailprotector / CloudFilter Skill

Standalone CLI client for the **Mailprotector CloudFilter REST API**
(`emailservice.io`), the reseller email-security platform ACG layers on top of
client mail flow. Read-only by default; every write (release, rule add, config
change) is gated behind `--confirm`.

## The two-layer context (important)

ACG's email security sits in front of client mailboxes as two cooperating layers:

| Layer | What it does |
|---|---|
| **Mailprotector CloudFilter** | The delivery / filtering gateway. Inbound and outbound mail passes through it; spam, virus, and policy hits are **held / quarantined** here. Releasing a held message re-injects it for delivery. This is the API this skill drives. |
| **INKY** | Email annotation / phishing-banner layer. Adds the warning banners and protects against impersonation. Not part of this API surface. |

Both sit **layered on top of the client's own Exchange / M365 mail flow** — so a
"missing email" investigation usually means: was it held at CloudFilter (check
`messages` / `logs`), or did it pass CloudFilter and stall in Exchange?

## Connection

| Item | Value |
|---|---|
| Base URL | `https://emailservice.io/api/v1` (override `MAILPROTECTOR_API_BASE_URL`) |
| Auth | `Authorization: Bearer <api_key>` |
| Vault entry | `msp-tools/mailprotector.sops.yaml`, field `credentials.api_key` |
| Env override | `MAILPROTECTOR_API_KEY` |

Credential resolution order: `MAILPROTECTOR_API_KEY` env -> vault
`credentials.api_key`. The key is never hardcoded; a clear setup error is raised
if neither resolves.

### Scopes

Five entity types carry `logs` / `messages` / `configuration` /
`allow_block_rules` / `users` / `domains` sub-resources. Path form is
`/{scope}/{id}/...`:

```
resellers, customers, domains, user_groups, users
```

The CLI validates `scope` against this set.

## Running the CLI

This machine's Python launcher is `py` (per identity.json); `python` / `python3`
also work. Run from the scripts dir so the two modules resolve.

```bash
cd C:/claudetools/.claude/skills/mailprotector/scripts

bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py status                      # validate token (GET /domains, per_page=1)
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py domains                     # list domains (global)
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py domains --scope customers --id <id>
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py domain <domain_id>
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py customers <reseller_id>
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py customer <customer_id>
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py users <scope> <id>
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py user <user_id>
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py find-user user@client.com   # locate a user / alias by email (a READ)
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py config <scope> <id>         # shows permissions.messages.allow_spam_release
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py rules <scope> <id>
```

### Mail-flow logs and held mail (the common investigation)

Both accept the same filters: `--sender --recipient --subject --decision
--sort-field --sort-direction --page --page-size`.

```bash
# Why didn't this arrive? Look at the decision in the flow logs.
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py logs domains <domain_id> --recipient ceo@client.com --decision quarantine_spam

# Held / quarantined mail search.
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py messages domains <domain_id> --sender boss@vendor.com
```

`--decision` values: `default`, `deliver`, `quarantine_spam`,
`quarantine_virus`, `quarantine_policy`, `bounce`, `encrypt`, `delete`.
`--sort-field` values: `@timestamp` (default), `prime.direction`,
`prime.from_header_raw`, `prime.recipient`, `prime.subject`, `prime.decision`,
`prime.score`.

## Writes (gated)

Every mutating command prints a `[DRY RUN]` line and exits non-zero unless you
pass `--confirm`.

```bash
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release <message_id> --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release <message_id> --recipients alt@client.com --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release-many <scope> <id> --ids 111,222,333 --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release-many <scope> <id> --all --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py add-rule <scope> <id> --value vendor.com --type allow --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py enable-release <scope> <id> --confirm
```

## The `allow_spam_release` gotcha

Releasing a held **spam** message fails if the owning entity does not have
`permissions.messages.allow_spam_release = true`. Workflow:

1. `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py config <scope> <id>` — check `allow_spam_release`.
2. If `false`: `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py enable-release <scope> <id> --confirm`.
3. Re-run the `release` / `release-many`.

Virus and policy quarantines are governed separately — only spam release is
gated by this permission.

## Example workflow: find a client's held outbound mail from a sender and release it

```bash
# 1. Find the client's domain.
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py domains --scope customers --id <customer_id>

# 2. Search held messages from the sender (outbound = sender is the client user).
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py messages domains <domain_id> --sender user@client.com --decision quarantine_spam

# 3. If it's spam-held, make sure release is permitted on the domain.
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py config domains <domain_id>          # check allow_spam_release
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py enable-release domains <domain_id> --confirm   # only if needed

# 4. Release by message id (DRY RUN first — omit --confirm to preview).
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release <message_id>                # [DRY RUN]
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py release <message_id> --confirm      # actually release
```

## Raw escape hatch

The named commands cover the common surface; for anything else, hit the path
directly. Non-GET methods still require `--confirm`.

```bash
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py raw GET  domains/<id>/logs
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" mp.py raw POST messages/<id>/deliver --body '{"include_original_recipients":1}' --confirm
```

## Notes

- This is the **LIVE production reseller CloudFilter platform**. A release
  re-delivers real mail to real recipients, and an allow rule can let real spam
  or phishing through — confirm the target entity with a read command before any
  write, and prefer releasing specific message ids over `--all`.
- Pagination: `page` (default 1) and `per_page` (default 25); reseller
  `messages` caps `per_page` at 50. The `X-Pagination` response header carries
  the page/total metadata.
- Full endpoint catalog, filter tables, and the global `field[op]=value`
  operators live in `references/api.md`.
