#!/usr/bin/env python3 """CLI for the mailprotector skill — Mailprotector CloudFilter REST API. Read subcommands run freely. Write subcommands (release, release-many, add-rule, enable-release, raw with a non-GET method) refuse to run unless --confirm is passed; without it they print what they WOULD do and exit non-zero. NOTE: find-user is a READ even though it is a POST under the hood — it is NOT gated. Read examples: py mp.py status py mp.py domains py mp.py domain py mp.py customers py mp.py users py mp.py find-user user@client.com py mp.py logs --sender boss@vendor.com --decision quarantine_spam py mp.py messages --recipient ceo@client.com py mp.py config py mp.py rules Write examples (all require --confirm): py mp.py release --confirm py mp.py release-many --ids 111,222,333 --confirm py mp.py add-rule --value vendor.com --type allow --confirm py mp.py enable-release --confirm Escape hatch (raw request against any path; non-GET requires --confirm): py mp.py raw GET domains/123/logs py mp.py raw POST messages/999/deliver --body '{...}' --confirm `scope` values are validated against: resellers, customers, domains, user_groups, users """ from __future__ import annotations import argparse import json import sys from mp_client import MailprotectorClient, MailprotectorError, VALID_SCOPES def _emit(obj) -> None: print(json.dumps(obj, indent=2, ensure_ascii=False, default=str)) def _parse_body(raw: str | None) -> dict | None: if raw is None: return None try: parsed = json.loads(raw) except json.JSONDecodeError as exc: raise SystemExit(f"--body is not valid JSON: {exc}") if not isinstance(parsed, dict): raise SystemExit("--body must be a JSON object") return parsed def _require_confirm(args, action: str, detail: str) -> None: if not getattr(args, "confirm", False): print(f"[DRY RUN] Would {action}: {detail}") print("Refusing to perform a write without --confirm. Re-run with --confirm.") raise SystemExit(2) def _add_log_filters(sp) -> None: """Attach the shared logs/messages filter flags to a subparser.""" sp.add_argument("scope", choices=VALID_SCOPES) sp.add_argument("id") sp.add_argument("--sender") sp.add_argument("--recipient") sp.add_argument("--subject") sp.add_argument( "--decision", choices=[ "default", "deliver", "quarantine_spam", "quarantine_virus", "quarantine_policy", "bounce", "encrypt", "delete", ], ) sp.add_argument( "--sort-field", dest="sort_field", choices=[ "@timestamp", "prime.direction", "prime.from_header_raw", "prime.recipient", "prime.subject", "prime.decision", "prime.score", ], ) sp.add_argument( "--sort-direction", dest="sort_direction", choices=["desc", "asc"] ) sp.add_argument("--page", type=int) sp.add_argument("--page-size", dest="page_size", type=int) def main(argv=None) -> int: p = argparse.ArgumentParser( prog="mp.py", description="Mailprotector CloudFilter REST API CLI" ) p.add_argument("--json", action="store_true", help="emit raw JSON (default)") sub = p.add_subparsers(dest="cmd", required=True) # --- read --- sub.add_parser("status", help="validate token (GET /domains per_page=1)") sp = sub.add_parser("domains", help="list domains (global or scoped)") sp.add_argument("--scope", choices=VALID_SCOPES) sp.add_argument("--id", help="entity id (required if --scope given)") sp.add_argument("--page", type=int, default=1) sp.add_argument("--per-page", dest="per_page", type=int, default=25) sp = sub.add_parser("domain", help="one domain") sp.add_argument("domain_id") sp = sub.add_parser("customers", help="customers under a reseller") sp.add_argument("reseller_id") sp.add_argument("--page", type=int, default=1) sp.add_argument("--per-page", dest="per_page", type=int, default=25) sp = sub.add_parser("customer", help="one customer") sp.add_argument("customer_id") sp = sub.add_parser("users", help="users under an entity") sp.add_argument("scope", choices=VALID_SCOPES) sp.add_argument("id") sp.add_argument("--page", type=int, default=1) sp.add_argument("--per-page", dest="per_page", type=int, default=25) sp = sub.add_parser("user", help="one user") sp.add_argument("user_id") sp = sub.add_parser("find-user", help="find a user/alias by email address") sp.add_argument("address") sp = sub.add_parser("logs", help="mail-flow logs for an entity") _add_log_filters(sp) sp = sub.add_parser("messages", help="held/quarantined messages for an entity") _add_log_filters(sp) sp = sub.add_parser("config", help="entity configuration") sp.add_argument("scope", choices=VALID_SCOPES) sp.add_argument("id") sp = sub.add_parser("rules", help="allow/block rules for an entity") sp.add_argument("scope", choices=VALID_SCOPES) sp.add_argument("id") # --- write (gated) --- sp = sub.add_parser("release", help="release one held message") sp.add_argument("message_id") sp.add_argument("--recipients", help="optional csv of override recipients") sp.add_argument("--confirm", action="store_true") sp = sub.add_parser("release-many", help="bulk-release held messages") sp.add_argument("scope", choices=VALID_SCOPES) sp.add_argument("id") sp.add_argument("--ids", help="csv of message ids to release") sp.add_argument("--all", action="store_true", help="release all selected") sp.add_argument("--recipients", help="optional csv of override recipients") sp.add_argument("--confirm", action="store_true") sp = sub.add_parser("add-rule", help="add an allow/block rule") sp.add_argument("scope", choices=VALID_SCOPES) sp.add_argument("id") sp.add_argument("--value", required=True) sp.add_argument("--type", dest="rule_type", required=True, choices=["allow", "block"]) sp.add_argument("--confirm", action="store_true") sp = sub.add_parser( "enable-release", help="enable spam release on an entity (allow_spam_release)" ) sp.add_argument("scope", choices=VALID_SCOPES) sp.add_argument("id") sp.add_argument("--confirm", action="store_true") # --- raw escape hatch --- sp = sub.add_parser("raw", help="raw request against any path") sp.add_argument("method", choices=["GET", "POST", "PUT", "PATCH", "DELETE"]) sp.add_argument("path", help="relative path, e.g. domains/123/logs") sp.add_argument("--body") sp.add_argument("--confirm", action="store_true") args = p.parse_args(argv) client = MailprotectorClient() try: if args.cmd == "status": result = client.status() _emit({"status": "ok", "auth": "valid", "sample": result}) elif args.cmd == "domains": if args.scope and not args.id: raise SystemExit("--id is required when --scope is given") _emit( client.domains( scope=args.scope, entity_id=args.id, page=args.page, per_page=args.per_page, ) ) elif args.cmd == "domain": _emit(client.domain(args.domain_id)) elif args.cmd == "customers": _emit( client.customers( args.reseller_id, page=args.page, per_page=args.per_page ) ) elif args.cmd == "customer": _emit(client.customer(args.customer_id)) elif args.cmd == "users": _emit( client.users( args.scope, args.id, page=args.page, per_page=args.per_page ) ) elif args.cmd == "user": _emit(client.user(args.user_id)) elif args.cmd == "find-user": _emit(client.find_user(args.address)) elif args.cmd == "logs": _emit( client.logs( args.scope, args.id, sort_direction=args.sort_direction, sort_field=args.sort_field, page=args.page, page_size=args.page_size, sender=args.sender, recipient=args.recipient, subject=args.subject, decision=args.decision, ) ) elif args.cmd == "messages": _emit( client.messages( args.scope, args.id, sort_direction=args.sort_direction, sort_field=args.sort_field, page=args.page, page_size=args.page_size, sender=args.sender, recipient=args.recipient, subject=args.subject, decision=args.decision, ) ) elif args.cmd == "config": _emit(client.configuration(args.scope, args.id)) elif args.cmd == "rules": _emit(client.allow_block_rules(args.scope, args.id)) elif args.cmd == "release": detail = args.message_id if args.recipients: detail += f" -> {args.recipients}" _require_confirm(args, "RELEASE held message", detail) _emit(client.release_message(args.message_id, recipients=args.recipients)) elif args.cmd == "release-many": if not args.ids and not args.all: raise SystemExit("release-many requires --ids or --all") target = "ALL selected" if args.all else f"ids={args.ids}" _require_confirm( args, "BULK RELEASE held messages", f"{args.scope}/{args.id}: {target}" ) _emit( client.release_many( args.scope, args.id, ids=args.ids, all_selected=args.all, recipients=args.recipients, ) ) elif args.cmd == "add-rule": _require_confirm( args, f"add {args.rule_type} rule", f"{args.scope}/{args.id}: {args.value}", ) _emit( client.add_rule(args.scope, args.id, args.value, args.rule_type) ) elif args.cmd == "enable-release": _require_confirm( args, "enable spam release (allow_spam_release=true)", f"{args.scope}/{args.id}", ) _emit(client.enable_release(args.scope, args.id)) elif args.cmd == "raw": body = _parse_body(args.body) if args.method != "GET": _require_confirm(args, f"{args.method} {args.path}", json.dumps(body)) _emit(client.request(args.method, args.path, json_body=body)) else: p.error(f"unknown command {args.cmd}") except MailprotectorError as exc: print(f"[ERROR] {exc}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())