Files
claudetools/.claude/skills/mailprotector/references/api.md
Mike Swanson ce9744832d 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>
2026-06-05 07:03:47 -07:00

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:

  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.