From f04e033dbe780835b5724575e33df9eb3f9d1261 Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Thu, 7 May 2026 17:17:18 +0000 Subject: [PATCH] =?UTF-8?q?fix(email):=20digest=20nu=20mai=20creeaz=C4=83?= =?UTF-8?q?=20noti=C8=9Be=20KB=20=E2=80=94=20fetch=20direct=20IMAP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit email_digest.py folosea save_unread_emails() care salva în memory/kb/emails/. Notițele KB trebuie create DOAR de heartbeat. Acum digest-ul face fetch direct din IMAP (ca email_forward.py), fără side effects pe KB. Co-Authored-By: Claude Sonnet 4.6 --- tools/email_digest.py | 150 ++++++++++++++++++++++++++++-------------- 1 file changed, 102 insertions(+), 48 deletions(-) diff --git a/tools/email_digest.py b/tools/email_digest.py index 6e11719..720f2d3 100644 --- a/tools/email_digest.py +++ b/tools/email_digest.py @@ -7,16 +7,24 @@ Usage: python3 tools/email_digest.py --dry-run # Afișează rezumatele fără a trimite """ +import re import sys import base64 +import mimetypes import subprocess import requests +import imaplib +import email as email_lib from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(PROJECT_ROOT)) -from tools.email_process import save_unread_emails, extract_original_sender +from tools.email_process import ( + IMAP_SERVER, IMAP_PORT, IMAP_USER, IMAP_PASS, + WHITELIST, decode_mime_header, extract_sender_email, + get_email_body, extract_original_sender, +) from src.config import Config BRIDGE_URL = "http://127.0.0.1:8098" @@ -29,23 +37,77 @@ def get_owner_jid() -> str: return f"{owner}@s.whatsapp.net" -def generate_summary(filepath: str, subject: str, from_full: str, date: str) -> str: - """Citește conținutul emailului și generează rezumat via Claude CLI.""" - try: - email_content = Path(filepath).read_text(encoding="utf-8") - except Exception as e: - return f"[Eroare la citirea fișierului: {e}]" +def fetch_unread_emails() -> list[dict]: + """Preia emailurile necitite din inbox fără a salva KB notes.""" + mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT) + mail.login(IMAP_USER, IMAP_PASS) + mail.select('INBOX') - display_from = extract_original_sender(subject, email_content, from_full) + status, messages = mail.search(None, 'UNSEEN') + email_ids = messages[0].split() if messages[0] else [] + results = [] + + for eid in email_ids: + status, data = mail.fetch(eid, "(BODY.PEEK[])") + if status != "OK": + continue + msg = email_lib.message_from_bytes(data[0][1]) + + from_addr = decode_mime_header(msg['From']) + sender_email = extract_sender_email(from_addr) + + if sender_email not in WHITELIST: + continue + + attachment_data = {} + if msg.is_multipart(): + for part in msg.walk(): + fname = part.get_filename() + if fname: + fname = decode_mime_header(fname) + payload = part.get_payload(decode=True) + if payload: + attachment_data[fname] = payload + + results.append({ + 'id': eid.decode(), + 'subject': decode_mime_header(msg['Subject']), + 'from_full': from_addr, + 'date': msg['Date'], + 'body': get_email_body(msg), + 'attachments': list(attachment_data.keys()), + 'attachment_data': attachment_data, + }) + + mail.logout() + return results + + +def mark_as_seen(email_ids: list[str]) -> None: + try: + mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT) + mail.login(IMAP_USER, IMAP_PASS) + mail.select('INBOX') + for eid in email_ids: + mail.store(eid.encode(), '+FLAGS', '\\Seen') + mail.logout() + except Exception as e: + print(f"[warn] Marcare Seen esuata: {e}", file=sys.stderr) + + +def generate_summary(subject: str, from_full: str, date: str, body: str) -> str: + """Generează rezumat email via Claude CLI.""" + display_from = extract_original_sender(subject, body, from_full) + display_subject = re.sub(r'^(Fwd?|Fw)\s*[:\s]\s*', '', subject, flags=re.IGNORECASE).strip() or subject prompt = f"""Mai jos este conținutul unui email. Scrie un rezumat factual pentru WhatsApp. EMAIL: -{email_content} +{body} Instrucțiuni: - Începe cu header-ul fix (fără modificări): - SUBIECT: {subject} + SUBIECT: {display_subject} De la: {display_from} Primit: {date} --- @@ -76,22 +138,19 @@ Instrucțiuni: def send_whatsapp(to: str, text: str) -> bool: - """Trimite mesaj pe WhatsApp prin bridge.""" try: resp = requests.post( f"{BRIDGE_URL}/send", json={"to": to, "text": text}, timeout=15, ) - data = resp.json() - return data.get("ok", False) + return resp.json().get("ok", False) except Exception as e: print(f"[eroare send] {e}", file=sys.stderr) return False def send_discord_webhook(text: str) -> bool: - """Trimite mesaj pe Discord via webhook (max 2000 chars per mesaj).""" config = Config(PROJECT_ROOT / "config.json") url = config.get("discord.email_webhook_url", "") if not url: @@ -109,16 +168,13 @@ def send_discord_webhook(text: str) -> bool: return False -def send_whatsapp_document(to: str, filepath: str) -> bool: - """Trimite un fișier ca document WhatsApp prin bridge.""" +def send_whatsapp_document(to: str, filename: str, data: bytes) -> bool: try: - path = Path(filepath) - data_b64 = base64.b64encode(path.read_bytes()).decode() - import mimetypes - mimetype = mimetypes.guess_type(path.name)[0] or "application/octet-stream" + mimetype = mimetypes.guess_type(filename)[0] or "application/octet-stream" resp = requests.post( f"{BRIDGE_URL}/send-document", - json={"to": to, "filename": path.name, "mimetype": mimetype, "data_base64": data_b64}, + json={"to": to, "filename": filename, "mimetype": mimetype, + "data_base64": base64.b64encode(data).decode()}, timeout=30, ) return resp.json().get("ok", False) @@ -128,57 +184,55 @@ def send_whatsapp_document(to: str, filepath: str) -> bool: def run_digest(): - print("📬 Verific emailuri necitite...") - saved = save_unread_emails() + print("Verific emailuri necitite...") + emails = fetch_unread_emails() owner_jid = get_owner_jid() - if not saved: + if not emails: print("Niciun email nou de procesat.") if not DRY_RUN: - send_whatsapp(owner_jid, "📭 Nu sunt emailuri noi.") + send_whatsapp(owner_jid, "Nu sunt emailuri noi.") return - for result in saved: - if not result.get("ok"): - print(f"⚠️ Sărit: {result.get('error')}") - continue + sent_ids = [] + for em in emails: + subject = em['subject'] + print(f"Procesez: {subject}") - filepath = result["file"] - subject = result["subject"] - from_full = result.get("from_full", result.get("from", "")) - date = result.get("date", "") - attachment_paths = result.get("attachment_paths", []) - print(f"📧 Procesez: {subject}") - - summary = generate_summary(filepath, subject, from_full, date) + summary = generate_summary(subject, em['from_full'], em['date'], em['body']) if DRY_RUN: print("\n--- REZUMAT (dry-run) ---") print(summary) - if attachment_paths: - print(f"Atașamente: {attachment_paths}") + if em.get('attachments'): + print(f"Atașamente: {em['attachments']}") print("------------------------\n") else: ok = send_whatsapp(owner_jid, summary) if ok: - print(f"✅ Trimis pe WhatsApp: {subject}") + print(f"Trimis pe WhatsApp: {subject}") else: - print(f"❌ Trimitere eșuată: {subject}") + print(f"Trimitere esuata: {subject}") ok_dc = send_discord_webhook(summary) if ok_dc: - print(f"✅ Trimis pe Discord: {subject}") + print(f"Trimis pe Discord: {subject}") else: - print(f"❌ Discord eșuat: {subject}") + print(f"Discord eșuat: {subject}") - for att_path in attachment_paths: - ok_att = send_whatsapp_document(owner_jid, att_path) - name = Path(att_path).name + for fname, fdata in em.get('attachment_data', {}).items(): + ok_att = send_whatsapp_document(owner_jid, fname, fdata) if ok_att: - print(f"✅ Atașament trimis: {name}") + print(f"Atașament trimis: {fname}") else: - print(f"❌ Atașament eșuat: {name}") + print(f"Atașament eșuat: {fname}") + + if ok: + sent_ids.append(em['id']) + + if sent_ids and not DRY_RUN: + mark_as_seen(sent_ids) if __name__ == "__main__":