import win32com.client import os import hashlib import logging from datetime import datetime, timedelta from typing import Dict from enum import IntEnum import time from nicegui import ui, run # --- CONFIGURAZIONE LOGGING --- logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # --- COSTANTI OUTLOOK --- class OutlookConstants(IntEnum): """Costanti Outlook per migliorare leggibilità""" FOLDER_INBOX = 6 MAIL_CLASS = 43 ATTACHMENT_TYPE_FILE = 1 # --- LOGICA DI BACKEND --- def get_file_hash(file_path: str) -> str: """Calcola l'MD5 per identificare file identici ed evitare duplicati.""" hasher = hashlib.md5() with open(file_path, 'rb') as f: for chunk in iter(lambda: f.read(4096), b""): hasher.update(chunk) return hasher.hexdigest() class ArchiverGUI: """Gestisce lo stato e la logica di archiviazione delle email Outlook.""" MONTH_NAMES = { 1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile", 5: "Maggio", 6: "Giugno", 7: "Luglio", 8: "Agosto", 9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre" } def __init__(self): self.running: bool = False self.progress: float = 0.0 self.current_mail: str = "In attesa di avvio..." async def start_archiving(self, archive_name: str, onedrive_path: str, months_limit: int) -> None: """Avvia il processo di archiviazione in background.""" self.running = True self.progress = 0.0 ui.notify('Connessione a Outlook in corso...', color='info') # Avvia la logica in un thread dedicato per non bloccare l'interfaccia await run.io_bound(self.run_logic, archive_name, onedrive_path, months_limit) self.running = False if self.progress >= 1.0: self.current_mail = "Archiviazione completata con successo!" ui.notify('Processo terminato!', type='positive') def run_logic(self, archive_name: str, onedrive_path: str, months_limit: int) -> None: """Logica principale di archiviazione delle email.""" try: if not os.path.exists(onedrive_path): os.makedirs(onedrive_path) logger.info(f"Cartella OneDrive creata: {onedrive_path}") outlook = win32com.client.Dispatch("Outlook.Application") namespace = outlook.GetNamespace("MAPI") inbox = namespace.GetDefaultFolder(OutlookConstants.FOLDER_INBOX) try: archive_root = namespace.Folders.Item(archive_name) except Exception as e: error_msg = f"ERRORE: Archivio '{archive_name}' non trovato." logger.error(error_msg, exc_info=e) self.current_mail = error_msg self.running = False return # Calcolo data limite per il filtro cutoff_date = datetime.now() - timedelta(days=int(months_limit) * 30) filter_str = f"[ReceivedTime] < '{cutoff_date.strftime('%d/%m/%Y %H:%M')}'" items = inbox.Items.Restrict(filter_str) items.Sort("[ReceivedTime]", True) total = items.Count if total == 0: self.current_mail = "Nessuna mail trovata con i criteri selezionati." self.progress = 1.0 logger.info("Nessuna email da archiviare") return logger.info(f"Inizio archiviazione di {total} email") processed_files: Dict[str, str] = {} # Ciclo dal fondo verso l'inizio per non sballare gli indici di Outlook for i in range(total, 0, -1): if not self.running: self.current_mail = "Processo interrotto dall'utente." logger.info("Archiviazione interrotta dall'utente") break try: item = items.Item(i) rt = item.ReceivedTime received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute) current_idx = total - i + 1 subject_preview = item.Subject[:40] if item.Subject else "(nessun oggetto)" self.current_mail = f"[{current_idx}/{total}] {subject_preview}..." self.progress = float(current_idx / total) # 1. ELIMINAZIONE ALLEGATI E AGGIUNTA LINK self.process_attachments(item, onedrive_path, received_time, processed_files) item.Save() # 2. PREPARAZIONE CARTELLA DESTINAZIONE year_str = str(received_time.year) month_name = self.MONTH_NAMES[received_time.month] month_folder_name = f"{received_time.month:02d}-{month_name}" year_folder = self.get_or_create_folder(archive_root, year_str) target_folder = self.get_or_create_folder(year_folder, month_folder_name) # 3. SPOSTAMENTO DEFINITIVO item.Move(target_folder) # Piccola pausa per dare respiro al server Exchange time.sleep(0.1) except Exception as e: logger.error(f"Errore elaborazione email {i}: {e}") continue except Exception as e: error_msg = f"Errore critico: {str(e)}" logger.critical(error_msg, exc_info=e) self.current_mail = error_msg self.running = False def get_or_create_folder(self, parent, name: str): """Recupera una cartella se esiste, altrimenti la crea.""" try: return parent.Folders.Item(name) except Exception: logger.debug(f"Cartella '{name}' non trovata, creazione...") return parent.Folders.Add(name) def process_attachments(self, mail_item, onedrive_path: str, received_time: datetime, processed_files: Dict[str, str]) -> None: """Rimuove gli allegati reali, li salva su OneDrive e inserisce il link nella mail.""" if mail_item.Class != OutlookConstants.MAIL_CLASS: return date_prefix = received_time.strftime("%Y-%m-%d") # Usiamo un ciclo reverse sugli allegati per la rimozione sicura count = mail_item.Attachments.Count for j in range(count, 0, -1): try: att = mail_item.Attachments.Item(j) # Filtro: Solo file reali, escludendo immagini nelle firme (Inline) if att.Type != OutlookConstants.ATTACHMENT_TYPE_FILE: continue is_inline = False try: # Controlla il tag MAPI per il Content-ID delle immagini incorporate if att.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x3712001E"): is_inline = True except Exception: pass if is_inline: continue # Salvataggio fisico del file temp_path = os.path.join(os.environ['TEMP'], att.FileName) att.SaveAsFile(temp_path) f_hash = get_file_hash(temp_path) # Nome file univoco con Data e Hash corto unique_name = f"{date_prefix}_{f_hash[:6]}_{att.FileName}" dest_path = os.path.join(onedrive_path, unique_name) if f_hash not in processed_files: if not os.path.exists(dest_path): os.replace(temp_path, dest_path) processed_files[f_hash] = dest_path logger.debug(f"File salvato: {unique_name}") else: if os.path.exists(temp_path): os.remove(temp_path) logger.debug(f"File duplicato eliminato: {att.FileName}") # Inserimento link HTML nel corpo della mail link_html = ( f"
" f"📎 Allegato spostato su OneDrive ({date_prefix}):
" f"{att.FileName}
" ) mail_item.HTMLBody = link_html + mail_item.HTMLBody # RIMOZIONE FISICA DALLA MAIL mail_item.Attachments.Remove(j) except Exception as e: logger.error(f"Errore elaborazione allegato {j}: {e}") # --- INTERFACCIA GRAFICA (NICEGUI) --- archiver = ArchiverGUI() @ui.page('/') def main_page(): ui.query('body').style('background-color: #f0f2f5; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;') with ui.header().classes('items-center justify-between bg-blue-9 shadow-4 q-pa-md'): ui.label('Outlook Smart Archiver').classes('text-h5 text-white font-bold') ui.icon('inventory_2', size='lg', color='white') with ui.column().classes('w-full max-w-3xl mx-auto q-pa-lg gap-6'): # CARD CONFIGURAZIONE with ui.card().classes('w-full q-pa-md shadow-2 border-l-4 border-blue-9'): ui.label('Configurazione').classes('text-h6 text-blue-9 mb-2') arc_name = ui.input('Nome dell\'Archivio Online', value='Archivio Online - Nome.Cognome@intesasanpaolo.com').classes('w-full') od_path = ui.input('Percorso locale OneDrive per Allegati', value=r'C:\Users\--UTENZA--\OneDrive - Intesa SanPaolo\Allegati_Outlook').classes('w-full') with ui.row().classes('w-full items-center mt-4 bg-grey-2 q-pa-sm rounded'): ui.label('Mesi da mantenere nella Inbox:') slider = ui.slider(min=0, max=24, value=6).classes('col px-4') ui.badge().bind_text_from(slider, 'value').classes('text-lg bg-blue-9') # CARD PROGRESSO with ui.card().classes('w-full q-pa-md shadow-2'): ui.label('Avanzamento').classes('text-h6 mb-2') with ui.column().classes('w-full items-center'): ui.label().bind_text_from(archiver, 'current_mail').classes('text-grey-8 text-italic mb-2 text-center') # Contenitore della barra with ui.linear_progress().bind_value_from(archiver, 'progress').props('stripe size=40px color=blue-9').classes('rounded-borders relative shadow-1') as p: # Etichetta percentuale centrata e troncata ui.label().bind_text_from(archiver, 'progress', backward=lambda x: f'{x * 100:.2f}%') \ .classes('absolute-center text-blue text-weight-bold') \ .style('text-shadow: 1px 1px 3px rgba(0,0,0,0.4); font-size: 1.2rem;') with ui.row().classes('w-full justify-center mt-8 gap-4'): ui.button('AVVIA ARCHIVIAZIONE', icon='rocket_launch', color='green-8', on_click=lambda: archiver.start_archiving(arc_name.value, od_path.value, slider.value)) \ .classes('q-px-lg').bind_enabled_from(archiver, 'running', backward=lambda x: not x) ui.button('STOP', icon='block', color='red-8', on_click=lambda: setattr(archiver, 'running', False)) \ .classes('q-px-lg').bind_enabled_from(archiver, 'running') ui.markdown('--- \n *Ricorda: Chiudi Outlook prima di avviare il processo per evitare blocchi.*').classes('text-center text-grey-6 text-xs') # Avvio dell'applicazione ui.run(native=True, window_size=(800, 600), title='Outlook Archiver Pro')