""" OutlookFilter — Frank Outlook Module (Layer 2) Classificazione email interno/esterno basata sul dominio del mittente. Le email vengono spostate in [Frank_Interni] o [Frank_Esterni] e le email interne vengono salvate in data/outlook_feed.json per Albert. """ import json from datetime import datetime from pathlib import Path try: from loguru import logger except ImportError: import logging logger = logging.getLogger(__name__) class OutlookFilter: """Classificatore email interno/esterno basato su dominio mittente.""" def __init__(self, rules_path='config/outlook_rules.json'): with open(rules_path, 'r', encoding='utf-8') as f: rules = json.load(f) self.internal_domains = [d.lower() for d in rules.get('internal_domains', [])] self.internal_senders = [s.lower() for s in rules.get('internal_senders', [])] self.date_from = rules.get('date_from', '2026-01-01') logger.info(f"Regole caricate — domini interni: {self.internal_domains}, date_from: {self.date_from}") def is_internal(self, sender_email: str) -> bool: """ Restituisce True se il mittente appartiene a un dominio interno. Gestisce anche indirizzi Exchange/MAPI (es. /O=EXCHANGELABS/...) che non hanno un dominio standard — trattati come esterni. """ if not sender_email or '@' not in sender_email: return False domain = sender_email.split('@')[-1].lower() return domain in self.internal_domains def classify(self, emails: list[dict]) -> list[dict]: """ Aggiunge il campo 'category': 'internal' | 'external' a ogni email. Classifica come interno se: - il dominio del mittente è in internal_domains, OPPURE - il SenderName è in internal_senders (account di servizio Exchange) Restituisce la lista arricchita (non modifica gli originali). """ result = [] for mail in emails: enriched = dict(mail) by_domain = self.is_internal(mail.get('sender_email', '')) by_name = mail.get('sender', '').lower() in self.internal_senders enriched['category'] = 'internal' if (by_domain or by_name) else 'external' result.append(enriched) return result def execute(self, emails: list[dict], connector) -> dict: """ Sposta ogni email nella cartella appropriata: - internal → [Frank_Interni] - external → [Frank_Esterni] Le cartelle vengono create automaticamente se non esistono. Non cancella nulla — tutto reversibile dall'utente. Returns: {'internal': N, 'external': N, 'errors': N} """ inbox = connector.namespace.GetDefaultFolder(6) # olFolderInbox folder_interni = connector.get_or_create_folder(inbox, '[Frank_Interni]') folder_esterni = connector.get_or_create_folder(inbox, '[Frank_Esterni]') counters = {'internal': 0, 'external': 0, 'errors': 0} for mail in emails: try: category = mail.get('category', 'external') target_folder = folder_interni if category == 'internal' else folder_esterni mail_item = connector.namespace.GetItemFromID(mail['entry_id']) mail_item.Move(target_folder) counters[category] += 1 except Exception as e: logger.warning(f"Errore spostando '{mail.get('subject', '?')}': {e}") counters['errors'] += 1 logger.info(f"Execute completato — {counters}") return counters def save_feed(self, emails: list[dict], path='data/outlook_feed.json'): """ Salva la lista di email in JSON. Crea la directory data/ se non esiste. """ Path(path).parent.mkdir(parents=True, exist_ok=True) feed = { 'generated_at': datetime.now().isoformat(), 'count': len(emails), 'emails': emails } with open(path, 'w', encoding='utf-8') as f: json.dump(feed, f, ensure_ascii=False, indent=2) logger.info(f"Feed salvato: {path} ({len(emails)} email)") def get_emails_since(self, connector, n: int = 1000) -> list[dict]: """ Legge le email dall'Inbox a partire da date_from usando Restrict(). Non modifica outlook_connector.py — accede direttamente al namespace MAPI. Args: connector: OutlookConnector già connesso n: limite massimo di email da caricare (default 1000) """ date_from = datetime.strptime(self.date_from, '%Y-%m-%d') mapi_date = date_from.strftime('%m/%d/%Y 12:00 AM') filter_str = f"[ReceivedTime] >= '{mapi_date}'" inbox = connector.namespace.GetDefaultFolder(6) # olFolderInbox items = inbox.Items items.Sort('[ReceivedTime]', False) # Più vecchie prima (backlog processing) try: items = items.Restrict(filter_str) logger.info(f"Restrict applicato: {filter_str}") except Exception as e: logger.error(f"Errore nel filtro Restrict: {e}") results = [] count = 0 for item in items: if count >= n: logger.warning(f"Limite raggiunto ({n} email) — aumenta n se necessario") break if item.Class != 43: # 43 = olMail continue try: body_preview = (item.Body or '')[:200].replace('\r\n', ' ').replace('\n', ' ') received = item.ReceivedTime received_str = received.strftime('%Y-%m-%d %H:%M') if hasattr(received, 'strftime') else str(received) # Risolvi indirizzo SMTP reale per mittenti Exchange interni. # SenderEmailAddress per utenti interni restituisce un DN Exchange # tipo "/O=EXCHANGELABS/..." invece di "nome@mediaset.it". sender_email = item.SenderEmailAddress or '' if not sender_email or sender_email.startswith('/O=') or '@' not in sender_email: try: sender_entry = item.Sender if sender_entry: exchange_user = sender_entry.GetExchangeUser() if exchange_user and exchange_user.PrimarySmtpAddress: sender_email = exchange_user.PrimarySmtpAddress except Exception: pass # fallback all'indirizzo originale results.append({ 'subject': item.Subject or '(nessun oggetto)', 'sender': item.SenderName or '', 'sender_email': sender_email, 'received': received_str, 'body_preview': body_preview, 'unread': item.UnRead, 'entry_id': item.EntryID }) count += 1 except Exception as e: logger.warning(f"Errore leggendo email: {e}") continue logger.info(f"Caricate {len(results)} email dal {self.date_from}") return results