Specs stay at repo root (cross-VM). Move deploy and code into logical projects with README per domain, updated manifest.yaml, and symlinks at legacy paths for VM122 backward compatibility.
239 lines
7 KiB
Python
239 lines
7 KiB
Python
"""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()
|