Files
Aissa 5d60d33db1 Platform bundle: marketing publish, IT ops, packaging, agents mesh.
Volledige Foodlinkk Command Center uitbreiding met social automatisering,
reclamefolder filters, Proxmox monitoring en documentatie.
2026-06-09 00:41:27 +00:00

230 lines
7.5 KiB
Python

"""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"}