Files
claudetools/.claude/skills/mailprotector/SKILL.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

6.7 KiB

name, description
name description
mailprotector 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.

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.

# 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.

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

# 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.

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.