feat(skills): add /mailprotector — CloudFilter held-mail search + release
Live Mailprotector CloudFilter REST client (emailservice.io/api/v1,
Bearer auth via vault msp-tools/mailprotector.sops.yaml). Lists mail-flow
logs and held/quarantined messages across client domains and releases them
(POST messages/{id}/deliver, deliver_many). Read-only by default; every
release/rule-add/config-change gated behind --confirm. Mirrors the
packetdial skill pattern. Built after diagnosing a Dataforth held-outbound
message that never reached ACG.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
155
.claude/skills/mailprotector/references/api.md
Normal file
155
.claude/skills/mailprotector/references/api.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Mailprotector CloudFilter REST API — Reference
|
||||
|
||||
Full endpoint catalog and filter tables for the `mailprotector` skill. SKILL.md
|
||||
stays lean; the detail lives here.
|
||||
|
||||
## Connection
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Base URL | `https://emailservice.io/api/v1` |
|
||||
| Override env | `MAILPROTECTOR_API_BASE_URL` |
|
||||
| Auth | `Authorization: Bearer <api_key>` |
|
||||
| Key env override | `MAILPROTECTOR_API_KEY` |
|
||||
| Vault entry | `msp-tools/mailprotector.sops.yaml`, field `credentials.api_key` |
|
||||
|
||||
Credential resolution order: `MAILPROTECTOR_API_KEY` env -> vault
|
||||
`credentials.api_key` (read via `bash <root>/.claude/scripts/vault.sh get-field`).
|
||||
A clear setup error is raised if neither resolves.
|
||||
|
||||
## Scopes
|
||||
|
||||
The five entity types that carry `logs`, `messages`, `configuration`,
|
||||
`users`, `domains`, and `allow_block_rules` sub-resources. Path form is
|
||||
`/{scope}/{id}/...`:
|
||||
|
||||
```
|
||||
resellers, customers, domains, user_groups, users
|
||||
```
|
||||
|
||||
The CLI validates `scope` against this set.
|
||||
|
||||
## Pagination
|
||||
|
||||
| Param | Default | Notes |
|
||||
|---|---|---|
|
||||
| `page` | 1 | 1-indexed page number |
|
||||
| `per_page` | 25 | Max **50** on reseller `messages` |
|
||||
|
||||
The response includes an `X-Pagination` response header (a JSON document with
|
||||
the page/total metadata).
|
||||
|
||||
## Global list filtering
|
||||
|
||||
List endpoints accept `field[op]=value` filters. Operators:
|
||||
|
||||
| Op | Meaning |
|
||||
|---|---|
|
||||
| `Gt` | greater than |
|
||||
| `Geq` | greater than or equal |
|
||||
| `Lt` | less than |
|
||||
| `Leq` | less than or equal |
|
||||
| `Eq` | equal |
|
||||
|
||||
Example: `created_at[geq]=2026-06-01`.
|
||||
|
||||
## Logs / messages filtering
|
||||
|
||||
Every `.../logs` and `.../messages` endpoint accepts these params:
|
||||
|
||||
| Param | Default | Allowed values |
|
||||
|---|---|---|
|
||||
| `sort_direction` | `desc` | `desc`, `asc` |
|
||||
| `sort_field` | `@timestamp` | `@timestamp`, `prime.direction`, `prime.from_header_raw`, `prime.recipient`, `prime.subject`, `prime.decision`, `prime.score` |
|
||||
| `page` | 1 | integer |
|
||||
| `page_size` | (API default) | integer |
|
||||
| `sender` | (none) | sender filter |
|
||||
| `recipient` | (none) | recipient filter |
|
||||
| `subject` | (none) | subject filter |
|
||||
| `decision` | `all` | `default`, `deliver`, `quarantine_spam`, `quarantine_virus`, `quarantine_policy`, `bounce`, `encrypt`, `delete` |
|
||||
|
||||
## READ endpoints
|
||||
|
||||
| Method | Path | Client method | CLI |
|
||||
|---|---|---|---|
|
||||
| GET | `/domains` | `domains()` | `domains` |
|
||||
| GET | `/{scope}/{id}/domains` | `domains(scope,id)` | `domains --scope --id` |
|
||||
| GET | `/domains/{id}` | `domain(id)` | `domain <id>` |
|
||||
| GET | `/resellers/{id}/customers` | `customers(id)` | `customers <reseller_id>` |
|
||||
| GET | `/customers/{id}` | `customer(id)` | `customer <id>` |
|
||||
| GET | `/{scope}/{id}/users` | `users(scope,id)` | `users <scope> <id>` |
|
||||
| GET | `/users/{id}` | `user(id)` | `user <id>` |
|
||||
| POST | `/users/find_by_address` | `find_user(address)` | `find-user <address>` |
|
||||
| GET | `/{scope}/{id}/logs` | `logs(scope,id,...)` | `logs <scope> <id>` |
|
||||
| GET | `/{scope}/{id}/messages` | `messages(scope,id,...)` | `messages <scope> <id>` |
|
||||
| GET | `/{scope}/{id}/configuration` | `configuration(scope,id)` | `config <scope> <id>` |
|
||||
| GET | `/{scope}/{id}/allow_block_rules` | `allow_block_rules(scope,id)` | `rules <scope> <id>` |
|
||||
|
||||
**`find_by_address` is a READ** despite being a POST — it looks up a user / alias
|
||||
by email. It is NOT gated behind `--confirm`.
|
||||
|
||||
`status` is a synthetic read: `GET /domains?per_page=1` used purely to validate
|
||||
the bearer token (HTTP 200 = key good).
|
||||
|
||||
## WRITE endpoints (gated behind `--confirm`)
|
||||
|
||||
Without `--confirm` the CLI prints `[DRY RUN] Would <action>: <detail>` and exits
|
||||
with code 2. With `--confirm` it performs the call.
|
||||
|
||||
### Release one held message
|
||||
|
||||
```
|
||||
POST /messages/{message_id}/deliver
|
||||
body: {"include_original_recipients": 1, "recipients": "<optional csv>"}
|
||||
```
|
||||
Client: `release_message(message_id, recipients=None)` — CLI: `release <message_id> [--recipients csv] --confirm`
|
||||
|
||||
### Bulk release held messages
|
||||
|
||||
```
|
||||
POST /{scope}/{id}/messages/deliver_many
|
||||
body: {"include_original_recipients": 1, "recipients": "<optional>",
|
||||
"all_selected": false, "ids": "<csv ids>"}
|
||||
```
|
||||
Client: `release_many(scope, id, ids=None, all_selected=False, recipients=None)`
|
||||
CLI: `release-many <scope> <id> [--ids csv | --all] [--recipients csv] --confirm`
|
||||
|
||||
### Add allow / block rule
|
||||
|
||||
```
|
||||
POST /{scope}/{id}/allow_block_rules
|
||||
body: {"value": "...", "rule_type": "allow" | "block"}
|
||||
```
|
||||
Client: `add_rule(scope, id, value, rule_type)` — CLI: `add-rule <scope> <id> --value <v> --type allow|block --confirm`
|
||||
|
||||
### Enable spam release on an entity
|
||||
|
||||
```
|
||||
PUT /{scope}/{id}/configuration
|
||||
body: {"permissions": {"messages": {"allow_spam_release": true}}}
|
||||
```
|
||||
Client: `enable_release(scope, id)` — CLI: `enable-release <scope> <id> --confirm`
|
||||
|
||||
This is required before an entity's held **spam** can be released. Check the
|
||||
state first with `config <scope> <id>` and look at
|
||||
`permissions.messages.allow_spam_release`.
|
||||
|
||||
## Raw escape hatch
|
||||
|
||||
```
|
||||
py mp.py raw <METHOD> <path> [--body JSON] [--confirm]
|
||||
```
|
||||
Non-GET methods require `--confirm`. Use for any endpoint not wrapped by a named
|
||||
command.
|
||||
|
||||
## The `allow_spam_release` gotcha
|
||||
|
||||
Releasing a held **spam** message will fail (or silently no-op) if the owning
|
||||
entity does not have `permissions.messages.allow_spam_release = true`. The fix:
|
||||
|
||||
1. `py mp.py config <scope> <id>` — confirm `allow_spam_release` is `false`.
|
||||
2. `py mp.py enable-release <scope> <id> --confirm` — flip it to `true`.
|
||||
3. Re-run the `release` / `release-many`.
|
||||
|
||||
Virus and policy quarantines are governed separately — only spam release is
|
||||
gated by this permission.
|
||||
Reference in New Issue
Block a user