This commit is contained in:
Alessandro Battilani
2026-01-11 14:36:28 +01:00
parent 16c271349f
commit 0080fa8586

View File

@@ -3,13 +3,12 @@ import os
import hashlib import hashlib
from datetime import datetime, timedelta from datetime import datetime, timedelta
import time import time
import asyncio
from nicegui import ui, run from nicegui import ui, run
# --- LOGICA DI BACKEND --- # --- LOGICA DI BACKEND ---
def get_file_hash(file_path): 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() hasher = hashlib.md5()
with open(file_path, 'rb') as f: with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b""): for chunk in iter(lambda: f.read(4096), b""):
@@ -21,7 +20,6 @@ class ArchiverGUI:
self.running = False self.running = False
self.progress = 0.0 self.progress = 0.0
self.current_mail = "In attesa di avvio..." self.current_mail = "In attesa di avvio..."
# Dizionario per la localizzazione italiana
self.mesi_it = { self.mesi_it = {
1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile", 5: "Maggio", 6: "Giugno", 1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile", 5: "Maggio", 6: "Giugno",
7: "Luglio", 8: "Agosto", 9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre" 7: "Luglio", 8: "Agosto", 9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre"
@@ -32,7 +30,7 @@ class ArchiverGUI:
self.progress = 0.0 self.progress = 0.0
ui.notify('Connessione a Outlook in corso...', color='info') 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) await run.io_bound(self.run_logic, archive_name, onedrive_path, months_limit)
self.running = False self.running = False
@@ -47,7 +45,7 @@ class ArchiverGUI:
outlook = win32com.client.Dispatch("Outlook.Application") outlook = win32com.client.Dispatch("Outlook.Application")
namespace = outlook.GetNamespace("MAPI") namespace = outlook.GetNamespace("MAPI")
inbox = namespace.GetDefaultFolder(6) inbox = namespace.GetDefaultFolder(6) # 6 = OlFolderInbox
try: try:
archive_root = namespace.Folders.Item(archive_name) archive_root = namespace.Folders.Item(archive_name)
@@ -56,7 +54,7 @@ class ArchiverGUI:
self.running = False self.running = False
return return
# Calcolo data limite # Calcolo data limite per il filtro
cutoff_date = datetime.now() - timedelta(days=int(months_limit) * 30) cutoff_date = datetime.now() - timedelta(days=int(months_limit) * 30)
filter_str = f"[ReceivedTime] < '{cutoff_date.strftime('%d/%m/%Y %H:%M')}'" filter_str = f"[ReceivedTime] < '{cutoff_date.strftime('%d/%m/%Y %H:%M')}'"
@@ -66,10 +64,12 @@ class ArchiverGUI:
total = items.Count total = items.Count
if total == 0: if total == 0:
self.current_mail = "Nessuna mail trovata con i criteri selezionati." self.current_mail = "Nessuna mail trovata con i criteri selezionati."
self.progress = 1.0
return return
processed_files = {} processed_files = {}
# Ciclo dal fondo verso l'inizio per non sballare gli indici di Outlook
for i in range(total, 0, -1): for i in range(total, 0, -1):
if not self.running: if not self.running:
self.current_mail = "Processo interrotto dall'utente." self.current_mail = "Processo interrotto dall'utente."
@@ -78,13 +78,17 @@ class ArchiverGUI:
try: try:
item = items.Item(i) item = items.Item(i)
rt = item.ReceivedTime 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) 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.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) anno_str = str(received_time.year)
nome_mese = self.mesi_it[received_time.month] nome_mese = self.mesi_it[received_time.month]
month_folder_name = f"{received_time.month:02d}-{nome_mese}" 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) y_f = self.get_or_create_folder(archive_root, anno_str)
target_folder = self.get_or_create_folder(y_f, month_folder_name) target_folder = self.get_or_create_folder(y_f, month_folder_name)
# Spostamento # 3. SPOSTAMENTO DEFINITIVO
archived_item = item.Move(target_folder) item.Move(target_folder)
if archived_item:
archived_item.Save() # Piccola pausa per dare respiro al server Exchange
# Passiamo processed_files per evitare duplicati nella stessa sessione time.sleep(0.1)
self.process_attachments(archived_item, onedrive_path, received_time, processed_files) except Exception as e:
time.sleep(0.3) # Pausa per sincronizzazione Exchange print(f"Errore su singola mail: {e}")
except Exception:
continue continue
except Exception as e: except Exception as e:
self.current_mail = f"Errore: {str(e)}" self.current_mail = f"Errore critico: {str(e)}"
self.running = False self.running = False
def get_or_create_folder(self, parent, name): def get_or_create_folder(self, parent, name):
@@ -112,121 +115,117 @@ class ArchiverGUI:
except: except:
return parent.Folders.Add(name) return parent.Folders.Add(name)
def process_attachments(self, archived_item, onedrive_path, received_time, processed_files): def process_attachments(self, mail_item, onedrive_path, received_time, processed_files):
if archived_item.Class != 43: """Rimuove gli allegati reali, li salva su OneDrive e inserisce il link nella mail."""
return 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 # Filtro: Solo file reali (Type 1), escludendo immagini nelle firme (Inline)
count = archived_item.Attachments.Count if att.Type != 1:
if count == 0: continue
return
is_inline = False
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: try:
att = archived_item.Attachments.Item(j) # Controlla il tag MAPI per il Content-ID delle immagini incorporate
if att.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x3712001E"):
# Filtri (Tipo e Immagini Inline) is_inline = True
if att.Type != 1: continue except: pass
try:
if att.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x3712001E"): if is_inline:
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"<div style='background:#f3f3f3; padding:10px; border:1px solid #ccc; margin:10px 0; font-family:Arial;'>"
f"<b>📎 Allegato archiviato ({date_prefix}):</b><br>"
f"<a href='file:///{dest_path}'>{att.FileName}</a></div>"
)
# 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 continue
print("da qui gestisco gli allegati")
# SALVATAGGIO DEFINITIVO: Senza questo, il Delete() non viene sincronizzato # Salvataggio fisico del file
if has_changed: temp_path = os.path.join(os.environ['TEMP'], att.FileName)
archived_item.Save() 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"<div style='background:#f3f3f3; padding:10px; border:1px dotted #666; margin:10px 0; font-family:Arial; font-size:12px;'>"
f"<b>📎 Allegato spostato su OneDrive ({date_prefix}):</b><br>"
f"<a href='file:///{dest_path}'>{att.FileName}</a></div>"
)
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() archiver = ArchiverGUI()
@ui.page('/') @ui.page('/')
def main_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'): with ui.header().classes('items-center justify-between bg-blue-9 shadow-4 q-pa-md'):
ui.label('Outlook Smart Archiver').classes('text-h5 q-ml-md') ui.label('Outlook Smart Archiver').classes('text-h5 text-white font-bold')
ui.icon('archive', size='lg').classes('q-mr-md') 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.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)', arc_name = ui.input('Nome dell\'Archivio Online',
value='Archivio Online - nome.cognome@intesasanpaolo.com').classes('w-full') value='Archivio Online - Nome.Cognome@intesasanpaolo.com').classes('w-full')
od_path = ui.input('Cartella Destinazione OneDrive', od_path = ui.input('Percorso locale OneDrive per Allegati',
value=r'C:\Users\utente\OneDrive - Intesa SanPaolo\Allegati_Outlook').classes('w-full') 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:') ui.label('Mesi da mantenere nella Inbox:')
slider = ui.slider(min=0, max=24, value=6).classes('col px-4') 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'): # CARD PROGRESSO
ui.label('Stato Avanzamento').classes('text-h6 mb-2') 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'): with ui.column().classes('w-full items-center'):
# Titolo della mail ui.label().bind_text_from(archiver, 'current_mail').classes('text-grey-8 text-italic mb-2 text-center')
ui.label().bind_text_from(archiver, 'current_mail').classes('text-blue-9 text-weight-medium mb-1')
# Barra di avanzamento # Contenitore della barra
# Nota: rimosso bind_content_from che causava l'errore with ui.linear_progress().bind_value_from(archiver, 'progress').props('stripe size=40px color=blue-9').classes('rounded-borders relative shadow-1') as p:
prog = ui.linear_progress().bind_value_from(archiver, 'progress') \ # Etichetta percentuale centrata e troncata
.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}%') \ 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'): with ui.row().classes('w-full justify-center mt-8 gap-4'):
ui.button('AVVIA PROCESSO', icon='play_arrow', color='green-7', ui.button('AVVIA ARCHIVIAZIONE', icon='rocket_launch', color='green-8',
on_click=lambda: archiver.start_archiving(arc_name.value, od_path.value, slider.value)) \ 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)) \ 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) 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)