#!/usr/bin/env python3 """Optional lab bridge: Crkl emulator → host `gog` CLI (Gmail send). make gog-bridge # Integrations → Prefer gog + URL http://10.0.2.2:8765 Endpoints: GET /health POST /v1/gmail/send JSON: {to, subject, body} """ from __future__ import annotations import json import os import shutil import subprocess import sys from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer HOST = os.environ.get("CRKL_GOG_BRIDGE_HOST", "0.0.0.0") PORT = int(os.environ.get("CRKL_GOG_BRIDGE_PORT", "8765")) GOG = os.environ.get("GOG_BIN") or shutil.which("gog") or "gog" ACCOUNT = os.environ.get("GOG_ACCOUNT", "") def run_gog(args: list[str]) -> tuple[int, str, str]: cmd = [GOG, *args] if ACCOUNT: cmd[1:1] = ["-a", ACCOUNT] proc = subprocess.run(cmd, capture_output=True, text=True, timeout=90) return proc.returncode, proc.stdout.strip(), proc.stderr.strip() class Handler(BaseHTTPRequestHandler): def log_message(self, fmt: str, *args) -> None: sys.stderr.write("gog-bridge: " + (fmt % args) + "\n") def _json(self, code: int, payload: dict) -> None: raw = json.dumps(payload).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(raw))) self.end_headers() self.wfile.write(raw) def do_GET(self) -> None: if self.path.startswith("/health"): code, out, err = run_gog(["auth", "list", "-j"]) accounts: list = [] if code == 0 and out: try: parsed = json.loads(out) raw = parsed.get("accounts") if isinstance(parsed, dict) else parsed if isinstance(raw, list): accounts = raw except json.JSONDecodeError: accounts = [] ok = code == 0 and len(accounts) > 0 labels = [] for a in accounts[:5]: if isinstance(a, dict): labels.append(str(a.get("email") or a.get("account") or a)) else: labels.append(str(a)) msg = ( "gog ok: " + ", ".join(labels) if ok else "No gog tokens. Run: gog auth add (or tunnel Hermes:8765)" ) self._json(200 if ok else 503, {"ok": ok, "message": msg, "accounts": len(accounts)}) return self._json(404, {"ok": False, "message": "not found"}) def do_POST(self) -> None: length = int(self.headers.get("Content-Length", "0")) raw = self.rfile.read(length) if length else b"{}" try: body = json.loads(raw.decode() or "{}") except json.JSONDecodeError: self._json(400, {"ok": False, "message": "invalid json"}) return if self.path.startswith("/v1/gmail/send"): to = (body.get("to") or "").strip() subject = (body.get("subject") or "").strip() text = body.get("body") or "" if not to or not subject: self._json(400, {"ok": False, "message": "to and subject required"}) return args = ["gmail", "send", "--to", to, "--subject", subject, "--body", text] if body.get("account"): args = ["-a", body["account"], *args] code, out, err = run_gog(args) if code != 0: self._json(502, {"ok": False, "message": err or out or "send failed"}) return self._json(200, {"ok": True, "message": f"Sent via gog → {to}\n{out}".strip()}) return self._json(404, {"ok": False, "message": "not found"}) def main() -> None: print(f"Crkl gog bridge on http://{HOST}:{PORT} (gog={GOG})") print("Emulator URL: http://10.0.2.2:8765") ThreadingHTTPServer((HOST, PORT), Handler).serve_forever() if __name__ == "__main__": main()