From 8b65d2dfb1a07abf106b19b5e9f7ba21fa423748 Mon Sep 17 00:00:00 2001 From: Alessandro Battilani Date: Fri, 6 Mar 2026 10:21:41 +0100 Subject: [PATCH] nuovo script per archiviare tutte le email ed eliminati ps1 --- Archive_all_email.bat | 3 + Archive_all_email.py | 374 +++++++++++++++++++++++++++++++++++++++ ArchiviaMailGui.py | 0 ArchiviazioneMail.ps1 | 64 ------- ArchiviazioneMailPst.ps1 | 99 ----------- main.py | 6 - 6 files changed, 377 insertions(+), 169 deletions(-) create mode 100644 Archive_all_email.bat create mode 100644 Archive_all_email.py delete mode 100644 ArchiviaMailGui.py delete mode 100644 ArchiviazioneMail.ps1 delete mode 100644 ArchiviazioneMailPst.ps1 delete mode 100644 main.py diff --git a/Archive_all_email.bat b/Archive_all_email.bat new file mode 100644 index 0000000..d164069 --- /dev/null +++ b/Archive_all_email.bat @@ -0,0 +1,3 @@ +@echo off +uv run .\Archive_all_email.py +timeout /t 5 \ No newline at end of file diff --git a/Archive_all_email.py b/Archive_all_email.py new file mode 100644 index 0000000..d1c181f --- /dev/null +++ b/Archive_all_email.py @@ -0,0 +1,374 @@ +import win32com.client +from datetime import datetime, timedelta +from tqdm import tqdm +import time +import locale + +# 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.") + +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" +} + +# Costanti Outlook per le cartelle speciali da escludere +FOLDER_DELETED_ITEMS = 3 # Posta eliminata +FOLDER_JUNK_EMAIL = 23 # Posta indesiderata +FOLDER_OUTBOX = 4 # Posta in uscita (bozze in transito) +FOLDER_DRAFTS = 16 # Bozze +EXCLUDED_DEFAULT_FOLDERS = {FOLDER_DELETED_ITEMS, FOLDER_JUNK_EMAIL, FOLDER_OUTBOX, FOLDER_DRAFTS} + +# Per la cartella Posta inviata usiamo SentOn invece di ReceivedTime +SENT_ITEMS_FOLDER = 5 + +# DefaultItemType == 0 → cartella di tipo Mail; altri valori → Calendar, Contacts, Tasks, ecc. +OL_MAIL_ITEM = 0 + + +def get_user_email(namespace): + """Recupera l'indirizzo email primario dell'utente corrente.""" + try: + exchange_user = namespace.CurrentUser.AddressEntry.GetExchangeUser() + if exchange_user: + return exchange_user.PrimarySmtpAddress + except Exception: + pass + # Fallback: legge dallo store predefinito + try: + return namespace.Stores.Item(1).DisplayName + except Exception: + pass + raise RuntimeError("Impossibile determinare l'email dell'utente corrente.") + + +def find_personal_store(namespace): + """ + Restituisce lo store della mailbox online (live) dell'utente. + Usa GetDefaultFolder(6) = Posta in arrivo, che appartiene sempre + alla mailbox primaria online, mai all'archivio. + """ + try: + inbox = namespace.GetDefaultFolder(6) + store = inbox.Store + print(f" Store personale (da Posta in arrivo): {store.DisplayName}") + return store + except Exception as e: + raise RuntimeError(f"Impossibile determinare lo store della mailbox online: {e}") + + +def get_all_folders(namespace, user_email, archive_root): + """ + Restituisce una lista di tuple (folder, filter_attr, time_attr) + SOLO per la mailbox personale dell'utente, escludendo: + - Cartelle non-mail (Calendar, Contacts, Tasks, ecc.) + - Posta eliminata, Posta indesiderata, Bozze, Posta in uscita + - La cartella archivio online stessa (e tutte le sue sottocartelle) + """ + # --- Individua lo store personale --- + personal_store = find_personal_store(namespace) + print(f" Store personale: {personal_store.DisplayName}") + + # --- EntryID dell'archivio online (passato direttamente da main, certo al 100%) --- + try: + archive_entry_id = archive_root.EntryID + except Exception: + archive_entry_id = None + + # --- EntryID cartelle speciali da escludere (via costanti Outlook) --- + excluded_entry_ids = set() + if archive_entry_id: + excluded_entry_ids.add(archive_entry_id) + for ftype in EXCLUDED_DEFAULT_FOLDERS: + try: + excluded_entry_ids.add(namespace.GetDefaultFolder(ftype).EntryID) + except Exception: + pass + + # --- EntryID Posta inviata (usa SentOn) --- + try: + sent_entry_id = namespace.GetDefaultFolder(SENT_ITEMS_FOLDER).EntryID + except Exception: + sent_entry_id = None + + result = [] + + def walk_folders(folder): + entry_id = folder.EntryID + + # Escludi per EntryID (archivio root, posta eliminata, ecc.) + # Nota: escludendo la root dell'archivio, walk_folders non scende mai + # nelle sue sottocartelle — non serve controllare i figli separatamente. + if entry_id in excluded_entry_ids: + return + + # Escludi cartelle nascoste di sistema tramite proprietà MAPI PR_ATTR_HIDDEN + try: + pa = folder.PropertyAccessor + hidden = pa.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x10F4000B") + if hidden: + return + except Exception: + pass + + # Escludi cartelle non-mail (Calendar, Contacts, Tasks, Notes, ecc.) + try: + if folder.DefaultItemType != OL_MAIL_ITEM: + return + except Exception: + pass + + # Attributo temporale corretto + if entry_id == sent_entry_id: + time_attr = "SentOn" + filter_attr = "[SentOn]" + else: + time_attr = "ReceivedTime" + filter_attr = "[ReceivedTime]" + + result.append((folder, filter_attr, time_attr)) + + # Ricorsione nelle sottocartelle + try: + for i in range(1, folder.Folders.Count + 1): + walk_folders(folder.Folders.Item(i)) + except Exception: + pass + + try: + root_folder = personal_store.GetRootFolder() + for i in range(1, root_folder.Folders.Count + 1): + walk_folders(root_folder.Folders.Item(i)) + except Exception as e: + print(f"Errore nella lettura delle cartelle: {e}") + + return result, personal_store + + +# MarkForDownload: 0=scaricato, 1=da scaricare, 2=solo header +OL_REMOTE_HEADER = 2 +OL_MARKED_FOR_DOWNLOAD = 1 + + +def force_download_folder(folder, namespace, timeout=120): + """ + Forza il download completo di tutti gli item che sono solo header (cache parziale). + 1. Marca tutti gli item header-only come "da scaricare" + 2. Triggera SendAndReceive + 3. Attende il completamento con timeout + """ + marked = 0 + try: + items = folder.Items + for i in range(1, items.Count + 1): + try: + item = items.Item(i) + if hasattr(item, "MarkForDownload") and item.MarkForDownload == OL_REMOTE_HEADER: + item.MarkForDownload = OL_MARKED_FOR_DOWNLOAD + item.Save() + marked += 1 + except Exception: + pass + except Exception: + pass + + if marked == 0: + return # Tutto già scaricato, niente da fare + + print(f" ↓ {marked} item da scaricare in '{folder.Name}', avvio sincronizzazione...") + + # Triggera SendAndReceive asincrono + try: + namespace.SendAndReceive(False) + except Exception: + pass + + # Aspetta che gli item siano tutti scaricati (polling con timeout) + deadline = time.time() + timeout + with tqdm(total=marked, desc=f" Download '{folder.Name}'", unit="mail", colour="cyan") as pbar: + last_remaining = marked + while time.time() < deadline: + try: + remaining = sum( + 1 for i in range(1, folder.Items.Count + 1) + if hasattr(folder.Items.Item(i), "MarkForDownload") + and folder.Items.Item(i).MarkForDownload != 0 + ) + except Exception: + remaining = 0 + done = marked - remaining + pbar.n = done + pbar.refresh() + if remaining == 0: + break + last_remaining = remaining + time.sleep(2) + else: + print(f" ⚠ Timeout: {last_remaining} item non ancora scaricati, si procede comunque.") + + +def get_or_create_folder(parent, name): + """Restituisce una sottocartella esistente o la crea.""" + try: + return parent.Folders.Item(name) + except Exception: + return parent.Folders.Add(name) + + +def archive_folder(source_folder, filter_attr, time_attr, + archive_root, cutoff_date, months_limit, root_entry_id): + """Archivia le mail più vecchie di cutoff_date dalla source_folder.""" + filter_date_str = cutoff_date.strftime("%d/%m/%Y %H:%M") + filter_str = f"{filter_attr} < '{filter_date_str}'" + + try: + items = source_folder.Items.Restrict(filter_str) + items.Sort(filter_attr, True) + except Exception as e: + print(f" Filtro non applicabile su '{source_folder.Name}': {e}") + return + + total_items = items.Count + if total_items == 0: + print(f" Nessuna mail da archiviare in '{source_folder.Name}'.") + return + + # --- Ricostruisce il percorso relativo rispetto alla root dello store --- + # Ci fermiamo esplicitamente quando il parent è la root (EntryID noto), + # evitando di includere il nome della mailbox (es. "utente@azienda.com") + path_parts = [] + f = source_folder + while True: + try: + parent = f.Parent + path_parts.insert(0, f.Name) + if parent.EntryID == root_entry_id: + break + f = parent + except Exception: + break + + archived_count = 0 + desc_label = "/".join(path_parts) if path_parts else source_folder.Name + + with tqdm(total=total_items, desc=f"Archiviazione '{desc_label}'", + 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 + + 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] + month_folder_name = f"{received_time.month:02d}-{nome_mese}" + + pbar.set_description(f"'{desc_label}' - {nome_mese} {anno_str}") + + # --- Costruisce la struttura nell'archivio --- + # Archivio → percorso relativo sorgente → Anno → Mese + target = archive_root + for part in path_parts: + target = get_or_create_folder(target, part) + target = get_or_create_folder(target, anno_str) + target = get_or_create_folder(target, month_folder_name) + + # Sposta con retry + archived_item = None + for tentativo in range(3): + try: + archived_item = item.Move(target) + if archived_item: + archived_item.Save() + time.sleep(0.4) + break + except Exception: + time.sleep(1) + + 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)[:20]) + + pbar.update(1) + + print(f" → {archived_count}/{total_items} mail archiviate da '{desc_label}'.") + + +def main(): + print("Connessione a Outlook in corso...") + try: + outlook = win32com.client.Dispatch("Outlook.Application") + namespace = outlook.GetNamespace("MAPI") + except Exception as e: + print(f"Errore connessione: {e}") + return + + # --- MODIFICA 1: ricava ARCHIVE_NAME dall'email dell'utente --- + try: + user_email = get_user_email(namespace) + ARCHIVE_NAME = f"Archivio online - {user_email}" + print(f"Utente rilevato: {user_email}") + print(f"Archivio target: {ARCHIVE_NAME}") + except RuntimeError as e: + print(f"Errore: {e}") + return + + try: + archive_root = namespace.Folders.Item(ARCHIVE_NAME) + except Exception: + print(f"Cartella archivio '{ARCHIVE_NAME}' non trovata in Outlook.") + return + + while True: + stringa_months_limit = input("\nQuanti mesi vuoi tenere in linea? (default 3): ").strip() or "3" + try: + MONTHS_LIMIT = int(stringa_months_limit) + if MONTHS_LIMIT > 0: + break + print(" Inserisci un numero intero maggiore di zero.") + except ValueError: + print(f" '{stringa_months_limit}' non è un numero valido. Riprova.") + cutoff_date = datetime.now() - timedelta(days=MONTHS_LIMIT * 30) + print(f"\nVerranno archiviate le mail precedenti al: {cutoff_date.strftime('%d/%m/%Y')}\n") + + # --- MODIFICA 2: recupera TUTTE le cartelle della mailbox --- + folders_to_process, personal_store = get_all_folders(namespace, user_email, archive_root) + print(f"Cartelle trovate da elaborare: {len(folders_to_process)}") + for f, _, _ in folders_to_process: + print(f" • {f.Name}") + + confirm = input("\nProcedere con l'archiviazione? (s/n): ").strip().lower() + if confirm != "s": + print("Operazione annullata.") + return + + root_entry_id = personal_store.GetRootFolder().EntryID + + for source_folder, filter_attr, time_attr in folders_to_process: + print(f"\n--- Cartella: {source_folder.Name} ---") + force_download_folder(source_folder, namespace) + archive_folder(source_folder, filter_attr, time_attr, + archive_root, cutoff_date, MONTHS_LIMIT, root_entry_id) + + print("\nOperazione conclusa con successo.") + + +if __name__ == "__main__": + main() diff --git a/ArchiviaMailGui.py b/ArchiviaMailGui.py deleted file mode 100644 index e69de29..0000000 diff --git a/ArchiviazioneMail.ps1 b/ArchiviazioneMail.ps1 deleted file mode 100644 index a3e5a89..0000000 --- a/ArchiviazioneMail.ps1 +++ /dev/null @@ -1,64 +0,0 @@ -# --- CONFIGURAZIONE --- -$archiveName = "Archivio online - alessandro.battilani@intesasanpaolo.com" -$monthsLimit = -8 -# ---------------------- - -try { - $outlook = New-Object -ComObject Outlook.Application - $namespace = $outlook.GetNamespace("MAPI") - - $inbox = $namespace.GetDefaultFolder(6) - $archiveRoot = $namespace.Folders.Item($archiveName) - - if ($null -eq $archiveRoot) { - Write-Host "ERRORE: Archivio non trovato." -ForegroundColor Red - exit - } - - # CALCOLO DATA TAGLIO - $cutoffDate = (Get-Date).AddMonths($monthsLimit) - Write-Host "OGGI: $((Get-Date).ToShortDateString())" -ForegroundColor White - Write-Host "ARCHIVIO TUTTO QUELLO CHE E' PRIMA DEL: $($cutoffDate.ToShortDateString())" -ForegroundColor Yellow - Write-Host "--------------------------------------------------" - - $items = $inbox.Items - $items.Sort("[ReceivedTime]", $true) # Ordine cronologico - - $spostate = 0 - Write-Host "Inizio archiviazione..." -ForegroundColor Yellow - - for ($i = $items.Count; $i -ge 1; $i--) { - $item = $items.Item($i) - - if ($item.Class -eq 43) { - # CONFRONTO ESPLICITO - if ($item.ReceivedTime -lt $cutoffDate) { - - $year = $item.ReceivedTime.Year.ToString() - $monthName = $item.ReceivedTime.ToString("MM-MMMM") - - # Cartelle - $yearFolder = $null - try { $yearFolder = $archiveRoot.Folders.Item($year) } catch { } - if ($null -eq $yearFolder) { $yearFolder = $archiveRoot.Folders.Add($year) } - - $monthFolder = $null - try { $monthFolder = $yearFolder.Folders.Item($monthName) } catch { } - if ($null -eq $monthFolder) { $monthFolder = $yearFolder.Folders.Add($monthName) } - - $item.Move($monthFolder) | Out-Null - $spostate++ - Write-Host "`rProcessate: $spostate (Ultima: $($item.ReceivedTime.ToShortDateString()))" -NoNewline - } - } - [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null - } - - Write-Host "`n`nCompletato! Spostate $spostate email." -ForegroundColor Green -} -catch { - Write-Host "`nErrore: $($_.Exception.Message)" -ForegroundColor Red -} -finally { - if ($outlook) { [System.Runtime.Interopservices.Marshal]::ReleaseComObject($outlook) | Out-Null } -} \ No newline at end of file diff --git a/ArchiviazioneMailPst.ps1 b/ArchiviazioneMailPst.ps1 deleted file mode 100644 index 5f3a7db..0000000 --- a/ArchiviazioneMailPst.ps1 +++ /dev/null @@ -1,99 +0,0 @@ -# --- CONFIGURAZIONE --- -$pstPath = "C:\Percorso\Tuo\File\archivio_vecchio.pst" -$archiveName = "IL NOME DEL TUO ARCHIVIO ONLINE" -$logFile = ".\Report_Migrazione_Totale.txt" -# ---------------------- - -# Inizializza Log e Memoria Duplicati -"REPORT MIGRAZIONE TOTALE PST - $(Get-Date)" | Out-File $logFile -"------------------------------------------" | Out-File $logFile -Append -$script:duplicatiSaltati = 0 -$script:spostateTotali = 0 -$script:mappaEmailEsistenti = @{} - -# Rinominata da Build-ExistMap a Initialize-ExistMap (Verbo approvato) -function Initialize-ExistMap($folder) { - foreach ($item in $folder.Items) { - if ($item.Class -eq 43) { - $key = "$($item.Subject)|$($item.ReceivedTime.Ticks)|$($item.SenderEmailAddress)" - $script:mappaEmailEsistenti[$key] = $true - } - } - foreach ($sub in $folder.Folders) { Initialize-ExistMap $sub } -} - -# Rinominata da Process-Folder a Invoke-FolderMigration (Verbo approvato) -function Invoke-FolderMigration($outlookFolder, $archiveRoot) { - $items = $outlookFolder.Items - $countInFolder = 0 - - if ($items.Count -gt 0) { - Write-Host "`nAnalisi cartella: $($outlookFolder.FolderPath)" -ForegroundColor Cyan - - for ($i = $items.Count; $i -ge 1; $i--) { - $item = $items.Item($i) - if ($item.Class -eq 43) { - $key = "$($item.Subject)|$($item.ReceivedTime.Ticks)|$($item.SenderEmailAddress)" - - if ($script:mappaEmailEsistenti.ContainsKey($key)) { - $script:duplicatiSaltati++ - continue - } - - $year = $item.ReceivedTime.Year.ToString() - $monthName = $item.ReceivedTime.ToString("MM-MMMM") - - $yearFolder = $null - try { $yearFolder = $archiveRoot.Folders.Item($year) } catch { } - if ($null -eq $yearFolder) { $yearFolder = $archiveRoot.Folders.Add($year) } - - $monthFolder = $null - try { $monthFolder = $yearFolder.Folders.Item($monthName) } catch { } - if ($null -eq $monthFolder) { $monthFolder = $yearFolder.Folders.Add($monthName) } - - $item.Move($monthFolder) | Out-Null - $script:spostateTotali++ - $countInFolder++ - Write-Host "`rSpostate: $script:spostateTotali | Duplicati: $script:duplicatiSaltati" -NoNewline - } - [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null - } - } - - if ($countInFolder -gt 0) { - "Spostate $countInFolder email da: $($outlookFolder.FolderPath)" | Out-File $script:logFile -Append - } - foreach ($subFolder in $outlookFolder.Folders) { Invoke-FolderMigration $subFolder $archiveRoot } -} - -try { - $outlook = New-Object -ComObject Outlook.Application - $namespace = $outlook.GetNamespace("MAPI") - - $archiveRoot = $namespace.Folders.Item($archiveName) - if ($null -eq $archiveRoot) { throw "Archivio Online '$archiveName' non trovato." } - - Write-Host "Mappatura email esistenti nell'archivio cloud..." -ForegroundColor Yellow - Initialize-ExistMap $archiveRoot - Write-Host "Mappatura completata. Email trovate: $($script:mappaEmailEsistenti.Count)" - - Write-Host "`nCaricamento PST: $pstPath" -ForegroundColor Yellow - $namespace.AddStore($pstPath) - $pstStore = $namespace.Stores | Where-Object { $_.FilePath -eq $pstPath } - $pstRoot = $pstStore.GetRootFolder() - - Invoke-FolderMigration $pstRoot $archiveRoot - - Write-Host "`n`nMigrazione terminata con successo!" -ForegroundColor Green - "------------------------------------------" | Out-File $logFile -Append - "TOTALE SPOSTATE: $script:spostateTotali" | Out-File $logFile -Append - "DUPLICATI SALTATI: $script:duplicatiSaltati" | Out-File $logFile -Append -} -catch { - Write-Host "`nErrore: $($_.Exception.Message)" -ForegroundColor Red - "ERRORE: $($_.Exception.Message)" | Out-File $logFile -Append -} -finally { - if ($pstRoot) { $namespace.RemoveStore($pstRoot) } - if ($outlook) { [System.Runtime.Interopservices.Marshal]::ReleaseComObject($outlook) | Out-Null } -} \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index 7f948e2..0000000 --- a/main.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from archivemail!") - - -if __name__ == "__main__": - main()