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"
"
- )
-
- # 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