"""Bloqueios ACCOUNT_EXISTS — libertar e-mail Carbonio (Spec 022).""" from __future__ import annotations import json import re from datetime import datetime, timezone from typing import Any EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") def _now() -> str: return datetime.now(timezone.utc).isoformat() def init_schema(conn) -> None: conn.execute( """ CREATE TABLE IF NOT EXISTS carbonio_account_blocks ( id INTEGER PRIMARY KEY, email TEXT NOT NULL, domain TEXT NOT NULL, session_id TEXT, ticket_id INTEGER, webhook_event_id INTEGER, error_message TEXT, status TEXT NOT NULL DEFAULT 'pending', resolved_by TEXT, resolved_at TEXT, resolution_note TEXT, created_at TEXT NOT NULL ) """ ) conn.execute( """ CREATE INDEX IF NOT EXISTS idx_carbonio_blocks_status ON carbonio_account_blocks(status, created_at DESC) """ ) conn.execute( """ CREATE INDEX IF NOT EXISTS idx_carbonio_blocks_session ON carbonio_account_blocks(session_id) """ ) def is_account_exists_failure(event: str, data: dict | None) -> bool: if event != "onboarding.failed": return False if not isinstance(data, dict): return False err = str(data.get("error") or data.get("message") or data.get("reason") or "") low = err.lower() return ( "account_exists" in low or "account.exists" in low or "já existe" in low or "already exists" in low ) def _extract_email(domain: str | None, data: dict | None) -> str | None: if isinstance(data, dict): for key in ("email", "account", "mailbox"): val = (data.get(key) or "").strip().lower() if EMAIL_RE.match(val): return val err = str(data.get("error") or "") m = re.search(r"[\w.+-]+@[\w.-]+\.\w+", err) if m: return m.group(0).lower() if domain and isinstance(data, dict): local = (data.get("local_part") or "").strip().lower() if local: return f"{local}@{domain.lower().strip()}" return None def upsert_from_webhook( conn, *, event: str, domain: str | None, session_id: str | None, data: dict | None, webhook_event_id: int | None, ticket_id: int | None, ) -> dict[str, Any] | None: if not is_account_exists_failure(event, data): return None email = _extract_email(domain, data) if not email: return None dom = (domain or email.split("@", 1)[-1]).lower().strip() sid = (session_id or "").strip() or None err_msg = str((data or {}).get("error") or "")[:2000] now = _now() if sid: row = conn.execute( """ SELECT id, status FROM carbonio_account_blocks WHERE email = ? AND session_id = ? AND status = 'pending' ORDER BY id DESC LIMIT 1 """, (email, sid), ).fetchone() if row: conn.execute( """ UPDATE carbonio_account_blocks SET error_message = ?, webhook_event_id = COALESCE(?, webhook_event_id), ticket_id = COALESCE(?, ticket_id) WHERE id = ? """, (err_msg, webhook_event_id, ticket_id, row["id"]), ) conn.commit() return get_block(conn, int(row["id"])) cur = conn.execute( """ INSERT INTO carbonio_account_blocks (email, domain, session_id, ticket_id, webhook_event_id, error_message, status, created_at) VALUES (?, ?, ?, ?, ?, ?, 'pending', ?) """, (email, dom, sid, ticket_id, webhook_event_id, err_msg, now), ) conn.commit() return get_block(conn, int(cur.lastrowid)) def get_block(conn, block_id: int) -> dict[str, Any] | None: row = conn.execute( "SELECT * FROM carbonio_account_blocks WHERE id = ?", (block_id,), ).fetchone() return _row_to_dict(row) if row else None def _row_to_dict(row) -> dict[str, Any]: return { "id": row["id"], "email": row["email"], "domain": row["domain"], "session_id": row["session_id"], "ticket_id": row["ticket_id"], "webhook_event_id": row["webhook_event_id"], "error_message": row["error_message"], "status": row["status"], "resolved_by": row["resolved_by"], "resolved_at": row["resolved_at"], "resolution_note": row["resolution_note"], "created_at": row["created_at"], } def list_blocks( conn, *, status: str | None = "pending", session_id: str | None = None, ticket_id: int | None = None, limit: int = 100, ) -> dict[str, Any]: limit = max(1, min(limit, 500)) clauses = [] params: list[Any] = [] if status and status != "all": clauses.append("status = ?") params.append(status) if session_id: clauses.append("session_id = ?") params.append(session_id.strip()) if ticket_id: clauses.append("ticket_id = ?") params.append(ticket_id) where = f"WHERE {' AND '.join(clauses)}" if clauses else "" rows = conn.execute( f""" SELECT * FROM carbonio_account_blocks {where} ORDER BY id DESC LIMIT ? """, (*params, limit), ).fetchall() total = conn.execute( f"SELECT COUNT(*) FROM carbonio_account_blocks {where}", tuple(params), ).fetchone()[0] return { "blocks": [_row_to_dict(r) for r in rows], "total": total, } def resolve_block( conn, block_id: int, *, resolved_by: str, note: str = "", ) -> dict[str, Any]: now = _now() cur = conn.execute( """ UPDATE carbonio_account_blocks SET status = 'resolved', resolved_by = ?, resolved_at = ?, resolution_note = ? WHERE id = ? AND status = 'pending' """, (resolved_by, now, note[:500], block_id), ) if cur.rowcount == 0: existing = get_block(conn, block_id) if existing and existing["status"] == "resolved": raise ValueError("already_resolved") raise ValueError("not_found_or_locked") conn.commit() return get_block(conn, block_id) or {} def append_ticket_resolution_note(conn, ticket_id: int, *, email: str, by: str) -> None: row = conn.execute("SELECT payload FROM tickets WHERE id = ?", (ticket_id,)).fetchone() if not row: return try: payload = json.loads(row["payload"] or "{}") except json.JSONDecodeError: payload = {} notes = payload.get("carbonio_release_notes") or [] notes.append({"at": _now(), "email": email, "by": by, "action": "account_deleted"}) payload["carbonio_release_notes"] = notes[-20:] conn.execute( "UPDATE tickets SET payload = ? WHERE id = ?", (json.dumps(payload, ensure_ascii=False), ticket_id), ) conn.commit()