diff --git a/ArchiviaMailGui.py b/ArchiviaMailGui.py index 0afeccf..318b490 100644 --- a/ArchiviaMailGui.py +++ b/ArchiviaMailGui.py @@ -3,13 +3,12 @@ import os import hashlib from datetime import datetime, timedelta import time -import asyncio from nicegui import ui, run # --- LOGICA DI BACKEND --- def get_file_hash(file_path): - """Calcola l'hash per identificare file identici.""" + """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""): @@ -21,7 +20,6 @@ class ArchiverGUI: self.running = False self.progress = 0.0 self.current_mail = "In attesa di avvio..." - # Dizionario per la localizzazione italiana self.mesi_it = { 1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile", 5: "Maggio", 6: "Giugno", 7: "Luglio", 8: "Agosto", 9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre" @@ -32,7 +30,7 @@ class ArchiverGUI: self.progress = 0.0 ui.notify('Connessione a Outlook in corso...', color='info') - # Esegue la logica pesante in un thread separato + # 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 @@ -47,7 +45,7 @@ class ArchiverGUI: outlook = win32com.client.Dispatch("Outlook.Application") namespace = outlook.GetNamespace("MAPI") - inbox = namespace.GetDefaultFolder(6) + inbox = namespace.GetDefaultFolder(6) # 6 = OlFolderInbox try: archive_root = namespace.Folders.Item(archive_name) @@ -56,7 +54,7 @@ class ArchiverGUI: self.running = False return - # Calcolo data limite + # 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')}'" @@ -66,10 +64,12 @@ class ArchiverGUI: total = items.Count if total == 0: self.current_mail = "Nessuna mail trovata con i criteri selezionati." + self.progress = 1.0 return processed_files = {} + # 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." @@ -78,13 +78,17 @@ class ArchiverGUI: try: item = items.Item(i) rt = item.ReceivedTime - # Convertiamo il tempo di Outlook in oggetto datetime Python received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute) self.current_mail = f"[{total-i+1}/{total}] {item.Subject[:40]}..." - self.progress = (total - i + 1) / total + self.progress = float((total - i + 1) / total) - # Gestione Cartelle con Localizzazione Italiana + # 1. ELIMINAZIONE ALLEGATI E AGGIUNTA LINK (Mentre è ancora in Inbox) + # Questo garantisce i permessi di scrittura necessari per cancellare i file + self.process_attachments(item, onedrive_path, received_time, processed_files) + item.Save() + + # 2. PREPARAZIONE CARTELLA DESTINAZIONE anno_str = str(received_time.year) nome_mese = self.mesi_it[received_time.month] month_folder_name = f"{received_time.month:02d}-{nome_mese}" @@ -92,18 +96,17 @@ class ArchiverGUI: y_f = self.get_or_create_folder(archive_root, anno_str) target_folder = self.get_or_create_folder(y_f, month_folder_name) - # Spostamento - archived_item = item.Move(target_folder) - if archived_item: - archived_item.Save() - # Passiamo processed_files per evitare duplicati nella stessa sessione - self.process_attachments(archived_item, onedrive_path, received_time, processed_files) - time.sleep(0.3) # Pausa per sincronizzazione Exchange - except Exception: + # 3. SPOSTAMENTO DEFINITIVO + item.Move(target_folder) + + # Piccola pausa per dare respiro al server Exchange + time.sleep(0.1) + except Exception as e: + print(f"Errore su singola mail: {e}") continue except Exception as e: - self.current_mail = f"Errore: {str(e)}" + self.current_mail = f"Errore critico: {str(e)}" self.running = False def get_or_create_folder(self, parent, name): @@ -112,121 +115,117 @@ class ArchiverGUI: except: return parent.Folders.Add(name) - def process_attachments(self, archived_item, onedrive_path, received_time, processed_files): - if archived_item.Class != 43: - return + def process_attachments(self, mail_item, onedrive_path, received_time, processed_files): + """Rimuove gli allegati reali, li salva su OneDrive e inserisce il link nella mail.""" + if mail_item.Class != 43: return # 43 = OlMail + + 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) - # Controlliamo il conteggio iniziale - count = archived_item.Attachments.Count - if count == 0: - return - - has_changed = False - date_prefix = received_time.strftime("%Y-%m-%d") - - # IMPORTANTE: Cicliamo al contrario (da count a 1) - # Quando si eliminano oggetti da una collezione, bisogna sempre andare dall'ultimo al primo - for j in range(count, 0, -1): + # Filtro: Solo file reali (Type 1), escludendo immagini nelle firme (Inline) + if att.Type != 1: + continue + + is_inline = False try: - att = archived_item.Attachments.Item(j) - - # Filtri (Tipo e Immagini Inline) - if att.Type != 1: continue - try: - if att.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x3712001E"): - continue - except: pass - - # Salvataggio su OneDrive - temp_path = os.path.join(os.environ['TEMP'], att.FileName) - att.SaveAsFile(temp_path) - f_hash = get_file_hash(temp_path) - - 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 - else: - if os.path.exists(temp_path): os.remove(temp_path) - - # Prepariamo il link HTML - link_html = ( - f"
" - f"📎 Allegato archiviato ({date_prefix}):
" - f"{att.FileName}
" - ) - - # Applichiamo le modifiche - archived_item.HTMLBody = link_html + archived_item.HTMLBody - - # ELIMINAZIONE FORZATA - att.Delete() - has_changed = True - - except Exception as e: - print(f"Errore allegato: {e}") + # 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: pass + + if is_inline: continue - - # SALVATAGGIO DEFINITIVO: Senza questo, il Delete() non viene sincronizzato - if has_changed: - archived_item.Save() + print("da qui gestisco gli allegati") + # 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 + else: + if os.path.exists(temp_path): + os.remove(temp_path) -# --- INTERFACCIA GRAFICA --- + # 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: + print(f"Errore su allegato {j}: {e}") + continue + +# --- INTERFACCIA GRAFICA (NICEGUI) --- archiver = ArchiverGUI() @ui.page('/') def main_page(): - ui.query('body').style('background-color: #f0f2f5') + 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'): - ui.label('Outlook Smart Archiver').classes('text-h5 q-ml-md') - ui.icon('archive', size='lg').classes('q-mr-md') + 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'): - with ui.card().classes('w-full q-pa-md shadow-3'): - ui.label('Parametri di Configurazione').classes('text-h6 mb-2') + + # 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 Archivio Online (esatto)', - value='Archivio Online - nome.cognome@intesasanpaolo.com').classes('w-full') + arc_name = ui.input('Nome dell\'Archivio Online', + value='Archivio Online - Nome.Cognome@intesasanpaolo.com').classes('w-full') - od_path = ui.input('Cartella Destinazione OneDrive', - value=r'C:\Users\utente\OneDrive - Intesa SanPaolo\Allegati_Outlook').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'): + 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') + ui.badge().bind_text_from(slider, 'value').classes('text-lg bg-blue-9') - with ui.card().classes('w-full q-pa-md shadow-3'): - ui.label('Stato Avanzamento').classes('text-h6 mb-2') + # 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'): - # Titolo della mail - ui.label().bind_text_from(archiver, 'current_mail').classes('text-blue-9 text-weight-medium mb-1') + ui.label().bind_text_from(archiver, 'current_mail').classes('text-grey-8 text-italic mb-2 text-center') - # Barra di avanzamento - # Nota: rimosso bind_content_from che causava l'errore - prog = ui.linear_progress().bind_value_from(archiver, 'progress') \ - .props('stripe size=35px') \ - .classes('rounded-borders relative') - - # Usiamo un'etichetta separata che "si appoggia" alla barra - # o iniettiamo il valore direttamente così: - with prog: + # 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') + .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-6 gap-4'): - ui.button('AVVIA PROCESSO', icon='play_arrow', color='green-7', + 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)) \ - .bind_enabled_from(archiver, 'running', backward=lambda x: not x) + .classes('q-px-lg').bind_enabled_from(archiver, 'running', backward=lambda x: not x) - ui.button('INTERROMPI', icon='stop', color='red-7', + ui.button('STOP', icon='block', color='red-8', on_click=lambda: setattr(archiver, 'running', False)) \ - .bind_enabled_from(archiver, 'running') + .classes('q-px-lg').bind_enabled_from(archiver, 'running') -ui.run(title='Outlook Archiver GUI', port=8080, reload=False, dark=False) \ No newline at end of file + 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(title='Outlook Archiver Pro', port=8080, reload=False, dark=False) \ No newline at end of file