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>
5.4 KiB
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:
py mp.py config <scope> <id>— confirmallow_spam_releaseisfalse.py mp.py enable-release <scope> <id> --confirm— flip it totrue.- Re-run the
release/release-many.
Virus and policy quarantines are governed separately — only spam release is gated by this permission.