Files
echo-core/tools/email_forward.py
Marius Mutu 30678e6abf fix(email): send WhatsApp notification when no new emails found
Previously digest and forward commands silently exited when inbox
was empty, leaving the user with no feedback after the initial
"processing..." confirmation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:29:58 +00:00

181 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""
Email forward: forwardează emailuri necitite direct pe WhatsApp fără rezumat.
Usage:
python3 tools/email_forward.py # Run forward
python3 tools/email_forward.py --dry-run # Afișează fără a trimite
"""
import sys
import re
import requests
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
)
from src.config import Config
import imaplib
import email as email_lib
BRIDGE_URL = "http://127.0.0.1:8098"
DRY_RUN = "--dry-run" in sys.argv
MAX_WA_LENGTH = 4096
def get_owner_jid() -> str:
config = Config(PROJECT_ROOT / "config.json")
owner = config.get("whatsapp.owner", "")
return f"{owner}@s.whatsapp.net"
def extract_original_sender(body: str, from_full: str) -> tuple[str, str]:
"""Dacă e email forwarded, extrage expeditorul și data originale."""
# Caută header-ul tipic de forward: "De la: X\nDate: Y" sau "From: X\nDate: Y"
match = re.search(
r'(?:De la|From):\s*(.+?)\n(?:Date|Data):\s*(.+?)(?:\n|$)',
body, re.IGNORECASE
)
if match:
return match.group(1).strip(), match.group(2).strip()
return from_full, ""
def format_for_whatsapp(subject: str, from_full: str, date: str, body: str) -> str:
"""Curăță corpul emailului și îl formatează pentru WhatsApp."""
# Extrage expeditorul original dacă e forward
original_from, original_date = extract_original_sender(body, from_full)
display_from = original_from
display_date = original_date or date
# Elimină header-ul markdown din fișierul KB (# Titlu, **De la:**, **Data:**, **Salvat:**)
body = re.sub(r'^#.+\n?', '', body, flags=re.MULTILINE)
body = re.sub(r'^\*\*(?:De la|Data|Salvat):\*\*.+\n?', '', body, flags=re.MULTILINE)
# Elimină header-ul forwarded (---------- Forwarded message ---------)
body = re.sub(r'-{5,}\s*Forwarded message\s*-{5,}.*?\n', '', body, flags=re.IGNORECASE)
# Elimină liniile De la/From/Date/Subject/To din header-ul forwarded
body = re.sub(r'^(?:De la|From|Date|Data|Subject|To):.+\n?', '', body, flags=re.IGNORECASE | re.MULTILINE)
# Elimină comentariile KB (<!-- Echo: ... -->)
body = re.sub(r'<!--.*?-->', '', body, flags=re.DOTALL)
# Elimină separatoarele --- rămase
body = re.sub(r'^\s*---\s*$', '', body, flags=re.MULTILINE)
# Elimină linii goale consecutive (mai mult de 2)
body = re.sub(r'\n{3,}', '\n\n', body)
# Elimină spații trailing
body = '\n'.join(line.rstrip() for line in body.splitlines())
body = body.strip()
header = f"*{subject}*\nDe la: {display_from}\nPrimit: {display_date}\n---\n"
full = header + body
if len(full) <= MAX_WA_LENGTH:
return [full]
# Împarte în bucăți la limita WhatsApp, tăind pe linie întreagă
parts = []
remaining = full
while remaining:
if len(remaining) <= MAX_WA_LENGTH:
parts.append(remaining)
break
cut = remaining[:MAX_WA_LENGTH].rfind('\n')
# Dacă nu găsește newline sau e prea aproape de început, taie la limită
if cut < MAX_WA_LENGTH // 2:
cut = MAX_WA_LENGTH
parts.append(remaining[:cut])
remaining = remaining[cut:].lstrip('\n')
total = len(parts)
if total > 1:
parts = [f"{p}\n\n[{i+1}/{total}]" for i, p in enumerate(parts)]
return parts
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 fetch_unread_emails():
"""Preia emailurile necitite din inbox fără a le salva sau marca ca citite."""
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:
# BODY.PEEK nu marchează ca citit
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
results.append({
'subject': decode_mime_header(msg['Subject']),
'from_full': from_addr,
'date': msg['Date'],
'body': get_email_body(msg),
})
mail.logout()
return results
def run_forward():
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
for em in emails:
subject = em['subject']
print(f"Trimit: {subject}")
parts = format_for_whatsapp(subject, em['from_full'], em['date'], em['body'])
if DRY_RUN:
for i, part in enumerate(parts):
print(f"\n--- FORWARD {i+1}/{len(parts)} (dry-run) ---")
print(part)
print("------------------------\n")
else:
for part in parts:
ok = send_whatsapp(owner_jid, part)
if not ok:
print(f"Trimitere esuata: {subject}")
break
else:
print(f"Trimis pe WhatsApp ({len(parts)} mesaje): {subject}")
if __name__ == "__main__":
run_forward()