diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/ArchiviaMailGui.py b/ArchiviaMailGui.py index ca5d09a..e69de29 100644 --- a/ArchiviaMailGui.py +++ b/ArchiviaMailGui.py @@ -1,231 +0,0 @@ -import win32com.client -import os -import hashlib -from datetime import datetime, timedelta -import time -from nicegui import ui, run - -# --- LOGICA DI BACKEND --- - -def get_file_hash(file_path): - """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: - def __init__(self): - self.running = False - self.progress = 0.0 - self.current_mail = "In attesa di avvio..." - 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') - - # 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, 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) # 6 = OlFolderInbox - - 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 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 - 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." - break - - try: - item = items.Item(i) - rt = item.ReceivedTime - 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 = float((total - i + 1) / total) - - # 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}" - - y_f = self.get_or_create_folder(archive_root, anno_str) - target_folder = self.get_or_create_folder(y_f, 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: - print(f"Errore su singola mail: {e}") - continue - - except Exception as e: - self.current_mail = f"Errore critico: {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, 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) - - # Filtro: Solo file reali (Type 1), escludendo immagini nelle firme (Inline) - if att.Type != 1: - 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: pass - - if is_inline: - continue - 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) - - # 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; 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') \ No newline at end of file diff --git a/ArchiviazioneMail.py b/ArchiviazioneMail.py index 02ae6e8..e69de29 100644 --- a/ArchiviazioneMail.py +++ b/ArchiviazioneMail.py @@ -1,122 +0,0 @@ -import win32com.client -from datetime import datetime, timedelta -from tqdm import tqdm -import time -import locale - -# --- CONFIGURAZIONE --- -ARCHIVE_NAME = "Archivio online - alessandro.battilani@intesasanpaolo.com" -# MONTHS_LIMIT = 3 -# ---------------------- - -# Imposta la localizzazione in italiano -try: - locale.setlocale(locale.LC_TIME, "it_IT.UTF-8") -except: # noqa: E722 - try: - locale.setlocale(locale.LC_TIME, "ita_ita") - except: # noqa: E722 - print("Locale italiano non impostato, uso nomi manuali.") - -def main(): - print("Connessione a Outlook in corso...") - try: - outlook = win32com.client.Dispatch("Outlook.Application") - namespace = outlook.GetNamespace("MAPI") - archive_root = namespace.Folders.Item(ARCHIVE_NAME) - - # 6 = Inbox (ReceivedTime), 5 = Sent Items (SentOn) - folders_to_process = [ - (namespace.GetDefaultFolder(6), "[ReceivedTime]", "ReceivedTime"), - (namespace.GetDefaultFolder(5), "[SentOn]", "SentOn") - ] - except Exception as e: - print(f"Errore connessione: {e}") - return - - stringa_mounths_limit = input("\n Quanti mesi vuoi tenere in linea? (default 3)") or '3' - - # Convertiamo la stringa in un numero intero - MONTHS_LIMIT = int(stringa_mounths_limit) - - cutoff_date = datetime.now() - timedelta(days=MONTHS_LIMIT * 30) - filter_date_str = cutoff_date.strftime("%d/%m/%Y %H:%M") - - 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"} - - for source_folder, filter_attr, time_attr in folders_to_process: - print(f"\n--- Analisi cartella: {source_folder.Name} ---") - - filter_str = f"{filter_attr} < '{filter_date_str}'" - items = source_folder.Items.Restrict(filter_str) - items.Sort(filter_attr, True) - - total_items = items.Count - if total_items == 0: - print(f"Nessuna mail più vecchia di {MONTHS_LIMIT} mesi in {source_folder.Name}.") - continue - - archived_count = 0 - # tqdm posizionato esternamente per monitorare il progresso reale - with tqdm(total=total_items, desc=f"Archiviazione {source_folder.Name}", unit="mail", colour='green') as pbar: - for i in range(total_items, 0, -1): - try: - item = items.Item(i) - - if not hasattr(item, time_attr): - pbar.update(1) - continue - - # Recupero data dinamico - rt = getattr(item, time_attr) - received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute) - anno_str = str(received_time.year) - nome_mese = mesi_it[received_time.month] - pbar.set_description(f"Archiviazione {source_folder.Name} - Mese: {nome_mese} {anno_str}") - - # Gestione struttura cartelle (Source -> Anno -> Mese) - try: - arch_type = archive_root.Folders.Item(source_folder.Name) - except: # noqa: E722 - arch_type = archive_root.Folders.Add(source_folder.Name) - - try: - y_f = arch_type.Folders.Item(anno_str) - except: # noqa: E722 - y_f = arch_type.Folders.Add(anno_str) - - month_folder_name = f"{received_time.month:02d}-{nome_mese}" - try: - target_folder = y_f.Folders.Item(month_folder_name) - except: # noqa: E722 - target_folder = y_f.Folders.Add(month_folder_name) - - # --- TENTA LO SPOSTAMENTO CON RETRY (Reintegrato) --- - archived_item = None - for tentativo in range(3): - try: - archived_item = item.Move(target_folder) - if archived_item: - archived_item.Save() - time.sleep(0.4) # Pausa vitale - break - except: # noqa: E722 - time.sleep(1) # Attesa per server busy - - if archived_item: - archived_count += 1 - pbar.set_postfix(successo=archived_count) - else: - pbar.set_postfix(error="Move fallito") - - except Exception as e: - pbar.set_postfix(err=str(e)[:15]) - - # Update della barra sempre alla fine del ciclo i - pbar.update(1) - - print("\nOperazione conclusa con successo.") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..7f948e2 --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from archivemail!") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index eba2f34..6aa8d53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "macro-outlook" +name = "archivemail" version = "0.1.0" description = "Add your description here" readme = "README.md"