#!/usr/bin/env python3 """ Email digest: procesează emailuri necitite și trimite rezumate pe WhatsApp. Usage: python3 tools/email_digest.py # Run digest 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 ( 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" def clean_urls(text: str) -> str: """Remove %0A and wrapped newlines from URLs in plain text emails.""" def _clean_url(m): url = m.group(0) url = url.replace('%0A', '').replace('%0a', '') url = re.sub(r'\s+', '', url) return url.rstrip('.') return re.sub(r'https?://\S+', _clean_url, text) DRY_RUN = "--dry-run" in sys.argv def get_owner_jid() -> str: config = Config(PROJECT_ROOT / "config.json") owner = config.get("whatsapp.owner", "") return f"{owner}@s.whatsapp.net" 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') 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 body = clean_urls(body) prompt = f"""Mai jos este conținutul unui email. Scrie un rezumat factual pentru WhatsApp. EMAIL: {body} Instrucțiuni: - Începe cu header-ul fix (fără modificări): SUBIECT: {display_subject} De la: {display_from} Primit: {date} --- - Ignoră complet orice persoană care a forwardat emailul. Nu o menționă în rezumat. - Scrie rezumatul în stil briefing: factual, clar, persoana a 3-a. * Prima propoziție: cine a trimis mesajul original, ce, cui. * Ce conține mesajul — concret și direct. Omite politețuri și amabilități; include doar faptele. * Dacă există termene, date, locuri sau acțiuni cerute — menționează-le explicit. * Dacă un item menționează un formular, document sau resursă cu link, include URL-ul direct după item, pe același rând sau pe rândul imediat următor — inline, nu secțiune separată la final. Copiază URL-urile COMPLET, fără trunchieri sau '...'. - Nu adăuga secțiuni goale sau care nu se aplică emailului. - Plain text, fără markdown. Fără emoji. - Răspunde DOAR cu rezumatul, nimic altceva.""" result = subprocess.run( ["claude", "-p", prompt, "--model", "sonnet", "--dangerously-skip-permissions"], capture_output=True, text=True, timeout=60, cwd=str(PROJECT_ROOT), ) if result.returncode != 0: return f"[Eroare la generarea rezumatului pentru: {subject}]\n{result.stderr.strip()[:200]}" return result.stdout.strip() def send_whatsapp(to: str, text: str) -> bool: try: resp = requests.post( f"{BRIDGE_URL}/send", json={"to": to, "text": text}, timeout=15, ) 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: config = Config(PROJECT_ROOT / "config.json") url = config.get("discord.email_webhook_url", "") if not url: return False try: chunks = [text[i:i+2000] for i in range(0, len(text), 2000)] for chunk in chunks: resp = requests.post(url, json={"content": chunk}, timeout=15) if resp.status_code not in (200, 204): print(f"[discord webhook] status {resp.status_code}", file=sys.stderr) return False return True except Exception as e: print(f"[discord webhook eroare] {e}", file=sys.stderr) return False def send_whatsapp_document(to: str, filename: str, data: bytes) -> bool: try: mimetype = mimetypes.guess_type(filename)[0] or "application/octet-stream" resp = requests.post( f"{BRIDGE_URL}/send-document", json={"to": to, "filename": filename, "mimetype": mimetype, "data_base64": base64.b64encode(data).decode()}, timeout=30, ) return resp.json().get("ok", False) except Exception as e: print(f"[eroare send-document] {e}", file=sys.stderr) return False def run_digest(): print("Verific emailuri necitite...") emails = fetch_unread_emails() owner_jid = get_owner_jid() if not emails: print("Niciun email nou de procesat.") if not DRY_RUN: send_whatsapp(owner_jid, "Nu sunt emailuri noi.") return sent_ids = [] for em in emails: subject = em['subject'] print(f"Procesez: {subject}") summary = generate_summary(subject, em['from_full'], em['date'], em['body']) if DRY_RUN: print("\n--- REZUMAT (dry-run) ---") print(summary) 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}") else: print(f"Trimitere esuata: {subject}") ok_dc = send_discord_webhook(summary) if ok_dc: print(f"Trimis pe Discord: {subject}") else: print(f"Discord eșuat: {subject}") 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: {fname}") else: 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__": run_digest()