From 16c271349f382811409feeeb22973cca423c6e73 Mon Sep 17 00:00:00 2001 From: Alessandro Battilani Date: Sat, 10 Jan 2026 16:20:08 +0100 Subject: [PATCH] gui v1 --- ArchiviaMailGui.py | 232 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 ArchiviaMailGui.py diff --git a/ArchiviaMailGui.py b/ArchiviaMailGui.py new file mode 100644 index 0000000..0afeccf --- /dev/null +++ b/ArchiviaMailGui.py @@ -0,0 +1,232 @@ +import win32com.client +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.""" + 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: + def __init__(self): + 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" + } + + async def start_archiving(self, archive_name, onedrive_path, months_limit): + self.running = True + self.progress = 0.0 + ui.notify('Connessione a Outlook in corso...', color='info') + + # Esegue la logica pesante in un thread separato + 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, onedrive_path, months_limit): + try: + if not os.path.exists(onedrive_path): + os.makedirs(onedrive_path) + + outlook = win32com.client.Dispatch("Outlook.Application") + namespace = outlook.GetNamespace("MAPI") + inbox = namespace.GetDefaultFolder(6) + + try: + archive_root = namespace.Folders.Item(archive_name) + except Exception: + self.current_mail = f"ERRORE: Archivio '{archive_name}' non trovato." + self.running = False + return + + # Calcolo data limite + 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." + return + + processed_files = {} + + for i in range(total, 0, -1): + if not self.running: + self.current_mail = "Processo interrotto dall'utente." + break + + 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 + + # Gestione Cartelle con Localizzazione Italiana + anno_str = str(received_time.year) + nome_mese = self.mesi_it[received_time.month] + month_folder_name = f"{received_time.month:02d}-{nome_mese}" + + 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: + continue + + except Exception as e: + self.current_mail = f"Errore: {str(e)}" + self.running = False + + def get_or_create_folder(self, parent, name): + try: + return parent.Folders.Item(name) + except: + return parent.Folders.Add(name) + + def process_attachments(self, archived_item, onedrive_path, received_time, processed_files): + if archived_item.Class != 43: + return + + # 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): + 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}") + continue + + # SALVATAGGIO DEFINITIVO: Senza questo, il Delete() non viene sincronizzato + if has_changed: + archived_item.Save() + +# --- INTERFACCIA GRAFICA --- + +archiver = ArchiverGUI() + +@ui.page('/') +def main_page(): + ui.query('body').style('background-color: #f0f2f5') + + 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.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') + + arc_name = ui.input('Nome Archivio Online (esatto)', + 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') + + with ui.row().classes('w-full items-center mt-4'): + 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') + + with ui.card().classes('w-full q-pa-md shadow-3'): + ui.label('Stato 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') + + # 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: + ui.label().bind_text_from(archiver, 'progress', backward=lambda x: f'{x * 100:.2f}%') \ + .classes('absolute-center text-blue text-weight-bold') + + with ui.row().classes('w-full justify-center mt-6 gap-4'): + ui.button('AVVIA PROCESSO', icon='play_arrow', color='green-7', + on_click=lambda: archiver.start_archiving(arc_name.value, od_path.value, slider.value)) \ + .bind_enabled_from(archiver, 'running', backward=lambda x: not x) + + ui.button('INTERROMPI', icon='stop', color='red-7', + on_click=lambda: setattr(archiver, 'running', False)) \ + .bind_enabled_from(archiver, 'running') + +ui.run(title='Outlook Archiver GUI', port=8080, reload=False, dark=False) \ No newline at end of file