"""IMAP sync and CEO email approval helpers.""" from __future__ import annotations import email import imaplib import json from datetime import datetime, timezone from email.header import decode_header from typing import Any, Optional import httpx from app.config import settings from app.db import execute, execute_returning, fetch_all, fetch_one, json_param def _decode_header(value: Optional[str]) -> str: if not value: return "" parts = decode_header(value) out: list[str] = [] for chunk, enc in parts: if isinstance(chunk, bytes): out.append(chunk.decode(enc or "utf-8", errors="replace")) else: out.append(str(chunk)) return " ".join(out).strip() def _accounts_to_sync() -> list[dict[str, Any]]: return fetch_all( """ SELECT id, label, email_address, imap_host, imap_port, imap_user, imap_password, imap_use_ssl, last_sync_at FROM email_accounts WHERE is_active = TRUE AND sync_enabled = TRUE AND imap_host IS NOT NULL AND imap_host <> '' ORDER BY id """ ) def sync_all_accounts() -> dict[str, Any]: accounts = _accounts_to_sync() if not accounts: return {"accounts": 0, "imported": 0, "skipped": 0, "message": "No IMAP accounts configured"} imported = skipped = 0 details: list[dict[str, Any]] = [] for acct in accounts: try: result = _sync_account(acct) imported += result["imported"] skipped += result["skipped"] details.append({"account_id": acct["id"], **result}) except Exception as exc: # noqa: BLE001 details.append({"account_id": acct["id"], "error": str(exc)}) return {"accounts": len(accounts), "imported": imported, "skipped": skipped, "details": details} def _sync_account(acct: dict[str, Any]) -> dict[str, Any]: host = acct["imap_host"] port = int(acct.get("imap_port") or 993) user = acct.get("imap_user") or acct["email_address"] password = acct.get("imap_password") or "" use_ssl = acct.get("imap_use_ssl", True) if use_ssl: mail = imaplib.IMAP4_SSL(host, port) else: mail = imaplib.IMAP4(host, port) mail.login(user, password) mail.select("INBOX") status, data = mail.search(None, "UNSEEN") if status != "OK": mail.logout() return {"imported": 0, "skipped": 0} ids = data[0].split() if data[0] else [] imported = skipped = 0 for num in ids[-50:]: status, msg_data = mail.fetch(num, "(RFC822)") if status != "OK" or not msg_data or not msg_data[0]: continue raw = msg_data[0][1] msg = email.message_from_bytes(raw) message_id = (msg.get("Message-ID") or f"local-{acct['id']}-{num.decode()}").strip() existing = fetch_one("SELECT id FROM emails WHERE message_id = %s", (message_id,)) if existing: skipped += 1 continue subject = _decode_header(msg.get("Subject")) from_addr = _decode_header(msg.get("From")) to_addrs = [_decode_header(msg.get("To"))] if msg.get("To") else [] body_text = "" if msg.is_multipart(): for part in msg.walk(): if part.get_content_type() == "text/plain" and not part.get_filename(): payload = part.get_payload(decode=True) if payload: body_text = payload.decode(part.get_content_charset() or "utf-8", errors="replace") break else: payload = msg.get_payload(decode=True) if payload: body_text = payload.decode(msg.get_content_charset() or "utf-8", errors="replace") row = execute_returning( """ INSERT INTO emails ( message_id, direction, from_addr, to_addrs, subject, body_text, received_at, is_read, raw_headers ) VALUES (%s, 'in', %s, %s, %s, %s, %s, FALSE, %s) RETURNING id """, ( message_id, from_addr, to_addrs, subject, body_text[:50000] if body_text else None, datetime.now(timezone.utc), json_param({"account_id": acct["id"], "label": acct["label"]}), ), ) imported += 1 if row: _create_review_recommendation(int(row["id"]), subject, from_addr) execute( "UPDATE email_accounts SET last_sync_at = NOW() WHERE id = %s", (acct["id"],), ) mail.logout() return {"imported": imported, "skipped": skipped} def _create_review_recommendation(email_id: int, subject: str, from_addr: str) -> None: execute_returning( """ INSERT INTO ai_recommendations ( recommendation_type, title, description, priority, status, generated_by, related_entity_type, related_entity_id, data_sources ) VALUES ( 'email_review', %s, %s, 'medium', 'pending', 'email-agent', 'email', %s, %s ) RETURNING id """, ( f"Nieuwe e-mail: {subject[:200] or '(geen onderwerp)'}", f"Inkomend bericht van {from_addr}. Beoordelen en eventueel beantwoorden.", email_id, json_param({"email_id": email_id, "from": from_addr}), ), ) def list_pending_drafts() -> list[dict[str, Any]]: return fetch_all( """ SELECT id, title, description, status, data_sources, created_at FROM ai_recommendations WHERE recommendation_type IN ('email_draft', 'email_review') AND status = 'pending' ORDER BY created_at DESC LIMIT 50 """ ) def create_draft(title: str, body: str, to_addr: str, subject: str) -> dict[str, Any]: row = execute_returning( """ INSERT INTO ai_recommendations ( recommendation_type, title, description, priority, status, generated_by, data_sources ) VALUES ( 'email_draft', %s, %s, 'high', 'pending', 'email-agent', %s ) RETURNING id, title, status, created_at """, ( title, body, json_param({"to": to_addr, "subject": subject, "body": body}), ), ) return dict(row or {}) def approve_draft(rec_id: int) -> dict[str, Any]: rec = fetch_one( """ SELECT id, recommendation_type, data_sources, status FROM ai_recommendations WHERE id = %s """, (rec_id,), ) if not rec: raise ValueError("Recommendation not found") if rec["status"] != "pending": raise ValueError(f"Already {rec['status']}") ds = rec.get("data_sources") or {} if isinstance(ds, str): ds = json.loads(ds) if rec["recommendation_type"] == "email_draft": payload = { "to": ds.get("to", ""), "subject": ds.get("subject", rec.get("title", "")), "body": ds.get("body", ""), } with httpx.Client(timeout=30.0) as client: resp = client.post(f"{settings.TOOLS_API_URL}/emails/send", json=payload) resp.raise_for_status() send_result = resp.json() else: send_result = {"action": "marked_reviewed"} execute( "UPDATE ai_recommendations SET status = 'approved', updated_at = NOW() WHERE id = %s", (rec_id,), ) return {"recommendation_id": rec_id, "send_result": send_result, "status": "approved"}