---
name: mailprotector
description: >-
  Manage the Arizona Computer Guru (ACG) Mailprotector CloudFilter email-security
  gateway via the live CloudFilter REST API (emailservice.io). Search and release
  held / quarantined mail (inbound and outbound), pull mail-flow logs to explain
  why a message did or did not deliver, inspect entity configuration and
  allow/block rules, find a user or alias by email, and manage allow/block rules.
  Read-only by default; every release / rule-add / config-change is gated behind
  --confirm. Invoke for: "mailprotector", "cloudfilter", "emailservice.io", "held
  mail", "quarantined email", "release email", "outbound quarantine", "why didn't
  my email arrive", "email security gateway", "INKY", "mail flow logs", "allow
  block rule", "release spam". This skill talks to the LIVE production reseller
  CloudFilter platform — treat releases conservatively.
---

# 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

py mp.py status                      # validate token (GET /domains, per_page=1)
py mp.py domains                     # list domains (global)
py mp.py domains --scope customers --id <id>
py mp.py domain <domain_id>
py mp.py customers <reseller_id>
py mp.py customer <customer_id>
py mp.py users <scope> <id>
py mp.py user <user_id>
py mp.py find-user user@client.com   # locate a user / alias by email (a READ)
py mp.py config <scope> <id>         # shows permissions.messages.allow_spam_release
py 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.
py mp.py logs domains <domain_id> --recipient ceo@client.com --decision quarantine_spam

# Held / quarantined mail search.
py 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
py mp.py release <message_id> --confirm
py mp.py release <message_id> --recipients alt@client.com --confirm
py mp.py release-many <scope> <id> --ids 111,222,333 --confirm
py mp.py release-many <scope> <id> --all --confirm
py mp.py add-rule <scope> <id> --value vendor.com --type allow --confirm
py 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. `py mp.py config <scope> <id>` — check `allow_spam_release`.
2. If `false`: `py 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.
py mp.py domains --scope customers --id <customer_id>

# 2. Search held messages from the sender (outbound = sender is the client user).
py 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.
py mp.py config domains <domain_id>          # check allow_spam_release
py mp.py enable-release domains <domain_id> --confirm   # only if needed

# 4. Release by message id (DRY RUN first — omit --confirm to preview).
py mp.py release <message_id>                # [DRY RUN]
py 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
py mp.py raw GET  domains/<id>/logs
py 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`.
