Compare commits

..

13 Commits

Author SHA1 Message Date
Alessandro Battilani
a35e0b118b disattivazione messaggi bat e timeout prima di uscire 2026-01-21 16:44:40 +01:00
eec665d231 Update ArchiviazioneMail.py 2026-01-21 10:44:19 +00:00
Alessandro Battilani
f14d032348 Merge branch 'main' of https://gitbat.duckdns.org/alex/ArchiveMail 2026-01-21 11:30:23 +01:00
Alessandro Battilani
49386bceca tolti warning ruff 2026-01-21 11:23:57 +01:00
Alessandro Battilani
c11f7eaa7b default n mesi 2026-01-20 09:21:49 +01:00
Alessandro Battilani
1081ba9e1b input mesi 2026-01-15 15:08:41 +01:00
Alessandro Battilani
7f3e047aff tolto locale che non serve più 2026-01-15 10:00:11 +01:00
Alessandro Battilani
7282352401 pre eliminazione gestione allegati 2026-01-15 09:53:03 +01:00
Alessandro Battilani
1868859800 cui native 2026-01-11 14:44:07 +01:00
Alessandro Battilani
0080fa8586 con gui 2026-01-11 14:36:28 +01:00
Alessandro Battilani
16c271349f gui v1 2026-01-10 16:20:08 +01:00
Alessandro Battilani
9cf6040a59 senza gui 2026-01-10 15:36:02 +01:00
Alessandro Battilani
2d9426e87f prima release 2026-01-09 19:01:12 +01:00
6 changed files with 287 additions and 618 deletions

3
ArchiviaEmail.bat Normal file
View File

@@ -0,0 +1,3 @@
@echo off
uv run .\ArchiviazioneMail.py
timeout /t 5

View File

@@ -1,272 +0,0 @@
import os
import hashlib
import logging
from datetime import datetime, timedelta
from typing import Dict
from enum import IntEnum
import time
from nicegui import ui, run
import win32com.client
# --- CONFIGURAZIONE LOGGING ---
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# --- COSTANTI OUTLOOK ---
class OutlookConstants(IntEnum):
"""Costanti Outlook per migliorare leggibilità"""
FOLDER_INBOX = 6
MAIL_CLASS = 43
ATTACHMENT_TYPE_FILE = 1
# --- LOGICA DI BACKEND ---
def get_file_hash(file_path: str) -> str:
"""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:
"""Gestisce lo stato e la logica di archiviazione delle email Outlook."""
MONTH_NAMES = {
1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile",
5: "Maggio", 6: "Giugno", 7: "Luglio", 8: "Agosto",
9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre"
}
def __init__(self):
self.running: bool = False
self.progress: float = 0.0
self.current_mail: str = "In attesa di avvio..."
async def start_archiving(self, archive_name: str, onedrive_path: str, months_limit: int) -> None:
"""Avvia il processo di archiviazione in background."""
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: str, onedrive_path: str, months_limit: int) -> None:
"""Logica principale di archiviazione delle email."""
try:
if not os.path.exists(onedrive_path):
os.makedirs(onedrive_path)
logger.info(f"Cartella OneDrive creata: {onedrive_path}")
outlook = win32com.client.Dispatch("Outlook.Application")
namespace = outlook.GetNamespace("MAPI")
inbox = namespace.GetDefaultFolder(OutlookConstants.FOLDER_INBOX)
try:
archive_root = namespace.Folders.Item(archive_name)
except Exception as e:
error_msg = f"ERRORE: Archivio '{archive_name}' non trovato."
logger.error(error_msg, exc_info=e)
self.current_mail = error_msg
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
logger.info("Nessuna email da archiviare")
return
logger.info(f"Inizio archiviazione di {total} email")
processed_files: Dict[str, str] = {}
# 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."
logger.info("Archiviazione interrotta dall'utente")
break
try:
item = items.Item(i)
rt = item.ReceivedTime
received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute)
current_idx = total - i + 1
subject_preview = item.Subject[:40] if item.Subject else "(nessun oggetto)"
self.current_mail = f"[{current_idx}/{total}] {subject_preview}..."
self.progress = float(current_idx / total)
# 1. ELIMINAZIONE ALLEGATI E AGGIUNTA LINK
self.process_attachments(item, onedrive_path, received_time, processed_files)
item.Save()
# 2. PREPARAZIONE CARTELLA DESTINAZIONE
year_str = str(received_time.year)
month_name = self.MONTH_NAMES[received_time.month]
month_folder_name = f"{received_time.month:02d}-{month_name}"
year_folder = self.get_or_create_folder(archive_root, year_str)
target_folder = self.get_or_create_folder(year_folder, 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:
logger.error(f"Errore elaborazione email {i}: {e}")
continue
except Exception as e:
error_msg = f"Errore critico: {str(e)}"
logger.critical(error_msg, exc_info=e)
self.current_mail = error_msg
self.running = False
def get_or_create_folder(self, parent, name: str):
"""Recupera una cartella se esiste, altrimenti la crea."""
try:
return parent.Folders.Item(name)
except Exception:
logger.debug(f"Cartella '{name}' non trovata, creazione...")
return parent.Folders.Add(name)
def process_attachments(self, mail_item, onedrive_path: str, received_time: datetime,
processed_files: Dict[str, str]) -> None:
"""Rimuove gli allegati reali, li salva su OneDrive e inserisce il link nella mail."""
if mail_item.Class != OutlookConstants.MAIL_CLASS:
return
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, escludendo immagini nelle firme (Inline)
if att.Type != OutlookConstants.ATTACHMENT_TYPE_FILE:
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 Exception:
pass
if is_inline:
continue
# 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
logger.debug(f"File salvato: {unique_name}")
else:
if os.path.exists(temp_path):
os.remove(temp_path)
logger.debug(f"File duplicato eliminato: {att.FileName}")
# 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:
logger.error(f"Errore elaborazione allegato {j}: {e}")
# --- 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'):
start_btn = 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')
start_btn.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
if __name__ == '__main__':
ui.run(native=True, window_size=(800, 600), title='Outlook Archiver Pro')

View File

@@ -1,64 +1,64 @@
# --- CONFIGURAZIONE --- # --- CONFIGURAZIONE ---
$archiveName = "Archivio online - alessandro.battilani@intesasanpaolo.com" $archiveName = "Archivio online - alessandro.battilani@intesasanpaolo.com"
$monthsLimit = -8 $monthsLimit = -8
# ---------------------- # ----------------------
try { try {
$outlook = New-Object -ComObject Outlook.Application $outlook = New-Object -ComObject Outlook.Application
$namespace = $outlook.GetNamespace("MAPI") $namespace = $outlook.GetNamespace("MAPI")
$inbox = $namespace.GetDefaultFolder(6) $inbox = $namespace.GetDefaultFolder(6)
$archiveRoot = $namespace.Folders.Item($archiveName) $archiveRoot = $namespace.Folders.Item($archiveName)
if ($null -eq $archiveRoot) { if ($null -eq $archiveRoot) {
Write-Host "ERRORE: Archivio non trovato." -ForegroundColor Red Write-Host "ERRORE: Archivio non trovato." -ForegroundColor Red
exit exit
} }
# CALCOLO DATA TAGLIO # CALCOLO DATA TAGLIO
$cutoffDate = (Get-Date).AddMonths($monthsLimit) $cutoffDate = (Get-Date).AddMonths($monthsLimit)
Write-Host "OGGI: $((Get-Date).ToShortDateString())" -ForegroundColor White Write-Host "OGGI: $((Get-Date).ToShortDateString())" -ForegroundColor White
Write-Host "ARCHIVIO TUTTO QUELLO CHE E' PRIMA DEL: $($cutoffDate.ToShortDateString())" -ForegroundColor Yellow Write-Host "ARCHIVIO TUTTO QUELLO CHE E' PRIMA DEL: $($cutoffDate.ToShortDateString())" -ForegroundColor Yellow
Write-Host "--------------------------------------------------" Write-Host "--------------------------------------------------"
$items = $inbox.Items $items = $inbox.Items
$items.Sort("[ReceivedTime]", $true) # Ordine cronologico $items.Sort("[ReceivedTime]", $true) # Ordine cronologico
$spostate = 0 $spostate = 0
Write-Host "Inizio archiviazione..." -ForegroundColor Yellow Write-Host "Inizio archiviazione..." -ForegroundColor Yellow
for ($i = $items.Count; $i -ge 1; $i--) { for ($i = $items.Count; $i -ge 1; $i--) {
$item = $items.Item($i) $item = $items.Item($i)
if ($item.Class -eq 43) { if ($item.Class -eq 43) {
# CONFRONTO ESPLICITO # CONFRONTO ESPLICITO
if ($item.ReceivedTime -lt $cutoffDate) { if ($item.ReceivedTime -lt $cutoffDate) {
$year = $item.ReceivedTime.Year.ToString() $year = $item.ReceivedTime.Year.ToString()
$monthName = $item.ReceivedTime.ToString("MM-MMMM") $monthName = $item.ReceivedTime.ToString("MM-MMMM")
# Cartelle # Cartelle
$yearFolder = $null $yearFolder = $null
try { $yearFolder = $archiveRoot.Folders.Item($year) } catch { } try { $yearFolder = $archiveRoot.Folders.Item($year) } catch { }
if ($null -eq $yearFolder) { $yearFolder = $archiveRoot.Folders.Add($year) } if ($null -eq $yearFolder) { $yearFolder = $archiveRoot.Folders.Add($year) }
$monthFolder = $null $monthFolder = $null
try { $monthFolder = $yearFolder.Folders.Item($monthName) } catch { } try { $monthFolder = $yearFolder.Folders.Item($monthName) } catch { }
if ($null -eq $monthFolder) { $monthFolder = $yearFolder.Folders.Add($monthName) } if ($null -eq $monthFolder) { $monthFolder = $yearFolder.Folders.Add($monthName) }
$item.Move($monthFolder) | Out-Null $item.Move($monthFolder) | Out-Null
$spostate++ $spostate++
Write-Host "`rProcessate: $spostate (Ultima: $($item.ReceivedTime.ToShortDateString()))" -NoNewline Write-Host "`rProcessate: $spostate (Ultima: $($item.ReceivedTime.ToShortDateString()))" -NoNewline
} }
} }
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
} }
Write-Host "`n`nCompletato! Spostate $spostate email." -ForegroundColor Green Write-Host "`n`nCompletato! Spostate $spostate email." -ForegroundColor Green
} }
catch { catch {
Write-Host "`nErrore: $($_.Exception.Message)" -ForegroundColor Red Write-Host "`nErrore: $($_.Exception.Message)" -ForegroundColor Red
} }
finally { finally {
if ($outlook) { [System.Runtime.Interopservices.Marshal]::ReleaseComObject($outlook) | Out-Null } if ($outlook) { [System.Runtime.Interopservices.Marshal]::ReleaseComObject($outlook) | Out-Null }
} }

View File

@@ -1,184 +1,122 @@
import win32com.client import win32com.client
import os from datetime import datetime, timedelta
import hashlib from tqdm import tqdm
from datetime import datetime, timedelta import time
from tqdm import tqdm import locale
import time
import locale # --- CONFIGURAZIONE ---
ARCHIVE_NAME = "Archivio online - alessandro.battilani@intesasanpaolo.com"
# Imposta la localizzazione in italiano # MONTHS_LIMIT = 3
try: # ----------------------
locale.setlocale(locale.LC_TIME, "it_IT.UTF-8") # Per Windows/Linux moderni
except: # noqa: E722 # Imposta la localizzazione in italiano
try: try:
locale.setlocale(locale.LC_TIME, "ita_ita") # Specifica per Windows locale.setlocale(locale.LC_TIME, "it_IT.UTF-8")
except: # noqa: E722 except: # noqa: E722
print("Impossibile impostare il locale italiano, uso i nomi manuali.") try:
locale.setlocale(locale.LC_TIME, "ita_ita")
# --- CONFIGURAZIONE --- except: # noqa: E722
ARCHIVE_NAME = "Archivio online - alessandro.battilani@intesasanpaolo.com" print("Locale italiano non impostato, uso nomi manuali.")
ONEDRIVE_PATH = r"C:\Users\U086304\OneDrive - Intesa SanPaolo\Allegati_Outlook"
if not os.path.exists(ONEDRIVE_PATH): def main():
os.makedirs(ONEDRIVE_PATH) print("Connessione a Outlook in corso...")
print(f"Cartella creata: {ONEDRIVE_PATH}") try:
MONTHS_LIMIT = 5 outlook = win32com.client.Dispatch("Outlook.Application")
# ---------------------- namespace = outlook.GetNamespace("MAPI")
archive_root = namespace.Folders.Item(ARCHIVE_NAME)
def get_file_hash(file_path):
hasher = hashlib.md5() # 6 = Inbox (ReceivedTime), 5 = Sent Items (SentOn)
with open(file_path, 'rb') as f: folders_to_process = [
for chunk in iter(lambda: f.read(4096), b""): (namespace.GetDefaultFolder(6), "[ReceivedTime]", "ReceivedTime"),
hasher.update(chunk) (namespace.GetDefaultFolder(5), "[SentOn]", "SentOn")
return hasher.hexdigest() ]
except Exception as e:
def main(): print(f"Errore connessione: {e}")
if not os.path.exists(ONEDRIVE_PATH): return
os.makedirs(ONEDRIVE_PATH)
stringa_mounths_limit = input("\n Quanti mesi vuoi tenere in linea? (default 3)") or '3'
print("Connessione a Outlook in corso...")
try: # Convertiamo la stringa in un numero intero
outlook = win32com.client.Dispatch("Outlook.Application") MONTHS_LIMIT = int(stringa_mounths_limit)
namespace = outlook.GetNamespace("MAPI")
inbox = namespace.GetDefaultFolder(6) cutoff_date = datetime.now() - timedelta(days=MONTHS_LIMIT * 30)
archive_root = namespace.Folders.Item(ARCHIVE_NAME) filter_date_str = cutoff_date.strftime("%d/%m/%Y %H:%M")
except Exception as e:
print(f"Errore di connessione: {e}") mesi_it = {1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile", 5: "Maggio", 6: "Giugno",
return 7: "Luglio", 8: "Agosto", 9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre"}
cutoff_date = datetime.now() - timedelta(days=MONTHS_LIMIT * 30) for source_folder, filter_attr, time_attr in folders_to_process:
filter_date_str = cutoff_date.strftime("%d/%m/%Y %H:%M") print(f"\n--- Analisi cartella: {source_folder.Name} ---")
filter_str = f"[ReceivedTime] < '{filter_date_str}'"
filter_str = f"{filter_attr} < '{filter_date_str}'"
items = inbox.Items.Restrict(filter_str) items = source_folder.Items.Restrict(filter_str)
items.Sort("[ReceivedTime]", True) items.Sort(filter_attr, True)
total_items = items.Count total_items = items.Count
if total_items == 0: if total_items == 0:
print("Nessuna email da archiviare.") print(f"Nessuna mail più vecchia di {MONTHS_LIMIT} mesi in {source_folder.Name}.")
return continue
print(f"Trovate {total_items} email vecchie. Inizio archiviazione...") archived_count = 0
# tqdm posizionato esternamente per monitorare il progresso reale
processed_files = {} with tqdm(total=total_items, desc=f"Archiviazione {source_folder.Name}", unit="mail", colour='green') as pbar:
archived_count = 0 for i in range(total_items, 0, -1):
mesi_it = {1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile", 5: "Maggio", 6: "Giugno", try:
7: "Luglio", 8: "Agosto", 9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre"} item = items.Item(i)
with tqdm(total=total_items, desc="Archiviazione", unit="mail", colour='green') as pbar: if not hasattr(item, time_attr):
for i in range(total_items, 0, -1): pbar.update(1)
try: continue
item = items.Item(i)
pbar.update(1) # Recupero data dinamico
rt = getattr(item, time_attr)
if not hasattr(item, 'ReceivedTime'): received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute)
continue anno_str = str(received_time.year)
nome_mese = mesi_it[received_time.month]
rt = item.ReceivedTime pbar.set_description(f"Archiviazione {source_folder.Name} - Mese: {nome_mese} {anno_str}")
received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute)
anno_str = str(received_time.year) # Gestione struttura cartelle (Source -> Anno -> Mese)
nome_mese = mesi_it[received_time.month] try:
pbar.set_description(f"Mese: {nome_mese} {anno_str}") arch_type = archive_root.Folders.Item(source_folder.Name)
except: # noqa: E722
# Cartelle arch_type = archive_root.Folders.Add(source_folder.Name)
month_folder_name = f"{received_time.month:02d}-{nome_mese}"
try: try:
y_f = archive_root.Folders.Item(anno_str) y_f = arch_type.Folders.Item(anno_str)
except: # noqa: E722 except: # noqa: E722
y_f = archive_root.Folders.Add(anno_str) y_f = arch_type.Folders.Add(anno_str)
try:
target_folder = y_f.Folders.Item(month_folder_name) month_folder_name = f"{received_time.month:02d}-{nome_mese}"
except: # noqa: E722 try:
target_folder = y_f.Folders.Add(month_folder_name) target_folder = y_f.Folders.Item(month_folder_name)
except: # noqa: E722
# --- TENTA LO SPOSTAMENTO CON RETRY --- target_folder = y_f.Folders.Add(month_folder_name)
archived_item = None
for tentativo in range(3): # --- TENTA LO SPOSTAMENTO CON RETRY (Reintegrato) ---
try: archived_item = None
archived_item = item.Move(target_folder) for tentativo in range(3):
if archived_item: try:
archived_item.Save() archived_item = item.Move(target_folder)
time.sleep(0.5) # Pausa vitale per il server if archived_item:
break archived_item.Save()
except: # noqa: E722 time.sleep(0.4) # Pausa vitale
time.sleep(1) # Aspetta se il server è occupato break
except: # noqa: E722
if not archived_item: time.sleep(1) # Attesa per server busy
pbar.set_postfix(error="Move fallito")
continue if archived_item:
archived_count += 1
# --- GESTIONE ALLEGATI (SOLO FILE VERI) --- pbar.set_postfix(successo=archived_count)
if archived_item.Class == 43 and archived_item.Attachments.Count > 0: else:
has_changed = False pbar.set_postfix(error="Move fallito")
attachments = [archived_item.Attachments.Item(j) for j in range(1, archived_item.Attachments.Count + 1)]
except Exception as e:
for att in attachments: pbar.set_postfix(err=str(e)[:15])
try:
# 1. Filtro base: solo allegati di tipo "Valore" (file) # Update della barra sempre alla fine del ciclo i
if att.Type != 1: pbar.update(1)
continue
print("\nOperazione conclusa con successo.")
# 2. FILTRO AVANZATO: Salta le immagini nelle firme (Inline Images)
# Controlliamo se l'allegato ha un "Content-ID" (tipico delle immagini incorporate) if __name__ == "__main__":
try:
prop_accessor = att.PropertyAccessor
cid = prop_accessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x3712001E")
if cid: # Se ha un CID, è un'immagine nel testo/firma
continue
except:
# Se non riesce a leggere la proprietà, procediamo con cautela
# (spesso i file veri non hanno questa proprietà)
pass
# 3. FILTRO ESTENSIONI: Opzionale, se vuoi escludere png/jpg a prescindere
# ext = os.path.splitext(att.FileName)[1].lower()
# if ext in ['.png', '.jpg', '.jpeg', '.gif']: continue
# --- Procedura di salvataggio con DATA nel nome ---
temp_path = os.path.join(os.environ['TEMP'], att.FileName)
att.SaveAsFile(temp_path)
f_hash = get_file_hash(temp_path)
# Creiamo il prefisso con la data (es. 2025-02-09_)
date_prefix = received_time.strftime("%Y-%m-%d")
if f_hash not in processed_files:
# Il nome file sarà: DATA_HASH_NOMEORIGINALE.ext
# Usiamo l'hash corto (primi 6 caratteri) per non avere nomi infiniti
unique_name = f"{date_prefix}_{f_hash[:6]}_{att.FileName}"
dest_path = os.path.join(ONEDRIVE_PATH, unique_name)
if not os.path.exists(dest_path):
os.replace(temp_path, dest_path)
processed_files[f_hash] = dest_path
else:
dest_path = processed_files[f_hash]
os.remove(temp_path)
# Il link nella mail punterà al nuovo nome con la data
link_html = (
f"<div style='border:1px solid #ccc; padding:8px; margin:10px 0; "
f"background-color:#f3f3f3; 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>"
)
archived_item.HTMLBody = link_html + archived_item.HTMLBody
att.Delete()
has_changed = True
except Exception as e:
continue
if has_changed:
archived_item.Save()
archived_count += 1
pbar.set_postfix(archiviate=archived_count)
except Exception as e:
pbar.set_postfix(last_err=str(e)[:15])
continue
print(f"\nCompletato. Archiviate: {archived_count}")
if __name__ == "__main__":
main() main()

View File

@@ -1,99 +1,99 @@
# --- CONFIGURAZIONE --- # --- CONFIGURAZIONE ---
$pstPath = "C:\Percorso\Tuo\File\archivio_vecchio.pst" $pstPath = "C:\Percorso\Tuo\File\archivio_vecchio.pst"
$archiveName = "IL NOME DEL TUO ARCHIVIO ONLINE" $archiveName = "IL NOME DEL TUO ARCHIVIO ONLINE"
$logFile = ".\Report_Migrazione_Totale.txt" $logFile = ".\Report_Migrazione_Totale.txt"
# ---------------------- # ----------------------
# Inizializza Log e Memoria Duplicati # Inizializza Log e Memoria Duplicati
"REPORT MIGRAZIONE TOTALE PST - $(Get-Date)" | Out-File $logFile "REPORT MIGRAZIONE TOTALE PST - $(Get-Date)" | Out-File $logFile
"------------------------------------------" | Out-File $logFile -Append "------------------------------------------" | Out-File $logFile -Append
$script:duplicatiSaltati = 0 $script:duplicatiSaltati = 0
$script:spostateTotali = 0 $script:spostateTotali = 0
$script:mappaEmailEsistenti = @{} $script:mappaEmailEsistenti = @{}
# Rinominata da Build-ExistMap a Initialize-ExistMap (Verbo approvato) # Rinominata da Build-ExistMap a Initialize-ExistMap (Verbo approvato)
function Initialize-ExistMap($folder) { function Initialize-ExistMap($folder) {
foreach ($item in $folder.Items) { foreach ($item in $folder.Items) {
if ($item.Class -eq 43) { if ($item.Class -eq 43) {
$key = "$($item.Subject)|$($item.ReceivedTime.Ticks)|$($item.SenderEmailAddress)" $key = "$($item.Subject)|$($item.ReceivedTime.Ticks)|$($item.SenderEmailAddress)"
$script:mappaEmailEsistenti[$key] = $true $script:mappaEmailEsistenti[$key] = $true
} }
} }
foreach ($sub in $folder.Folders) { Initialize-ExistMap $sub } foreach ($sub in $folder.Folders) { Initialize-ExistMap $sub }
} }
# Rinominata da Process-Folder a Invoke-FolderMigration (Verbo approvato) # Rinominata da Process-Folder a Invoke-FolderMigration (Verbo approvato)
function Invoke-FolderMigration($outlookFolder, $archiveRoot) { function Invoke-FolderMigration($outlookFolder, $archiveRoot) {
$items = $outlookFolder.Items $items = $outlookFolder.Items
$countInFolder = 0 $countInFolder = 0
if ($items.Count -gt 0) { if ($items.Count -gt 0) {
Write-Host "`nAnalisi cartella: $($outlookFolder.FolderPath)" -ForegroundColor Cyan Write-Host "`nAnalisi cartella: $($outlookFolder.FolderPath)" -ForegroundColor Cyan
for ($i = $items.Count; $i -ge 1; $i--) { for ($i = $items.Count; $i -ge 1; $i--) {
$item = $items.Item($i) $item = $items.Item($i)
if ($item.Class -eq 43) { if ($item.Class -eq 43) {
$key = "$($item.Subject)|$($item.ReceivedTime.Ticks)|$($item.SenderEmailAddress)" $key = "$($item.Subject)|$($item.ReceivedTime.Ticks)|$($item.SenderEmailAddress)"
if ($script:mappaEmailEsistenti.ContainsKey($key)) { if ($script:mappaEmailEsistenti.ContainsKey($key)) {
$script:duplicatiSaltati++ $script:duplicatiSaltati++
continue continue
} }
$year = $item.ReceivedTime.Year.ToString() $year = $item.ReceivedTime.Year.ToString()
$monthName = $item.ReceivedTime.ToString("MM-MMMM") $monthName = $item.ReceivedTime.ToString("MM-MMMM")
$yearFolder = $null $yearFolder = $null
try { $yearFolder = $archiveRoot.Folders.Item($year) } catch { } try { $yearFolder = $archiveRoot.Folders.Item($year) } catch { }
if ($null -eq $yearFolder) { $yearFolder = $archiveRoot.Folders.Add($year) } if ($null -eq $yearFolder) { $yearFolder = $archiveRoot.Folders.Add($year) }
$monthFolder = $null $monthFolder = $null
try { $monthFolder = $yearFolder.Folders.Item($monthName) } catch { } try { $monthFolder = $yearFolder.Folders.Item($monthName) } catch { }
if ($null -eq $monthFolder) { $monthFolder = $yearFolder.Folders.Add($monthName) } if ($null -eq $monthFolder) { $monthFolder = $yearFolder.Folders.Add($monthName) }
$item.Move($monthFolder) | Out-Null $item.Move($monthFolder) | Out-Null
$script:spostateTotali++ $script:spostateTotali++
$countInFolder++ $countInFolder++
Write-Host "`rSpostate: $script:spostateTotali | Duplicati: $script:duplicatiSaltati" -NoNewline Write-Host "`rSpostate: $script:spostateTotali | Duplicati: $script:duplicatiSaltati" -NoNewline
} }
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
} }
} }
if ($countInFolder -gt 0) { if ($countInFolder -gt 0) {
"Spostate $countInFolder email da: $($outlookFolder.FolderPath)" | Out-File $script:logFile -Append "Spostate $countInFolder email da: $($outlookFolder.FolderPath)" | Out-File $script:logFile -Append
} }
foreach ($subFolder in $outlookFolder.Folders) { Invoke-FolderMigration $subFolder $archiveRoot } foreach ($subFolder in $outlookFolder.Folders) { Invoke-FolderMigration $subFolder $archiveRoot }
} }
try { try {
$outlook = New-Object -ComObject Outlook.Application $outlook = New-Object -ComObject Outlook.Application
$namespace = $outlook.GetNamespace("MAPI") $namespace = $outlook.GetNamespace("MAPI")
$archiveRoot = $namespace.Folders.Item($archiveName) $archiveRoot = $namespace.Folders.Item($archiveName)
if ($null -eq $archiveRoot) { throw "Archivio Online '$archiveName' non trovato." } if ($null -eq $archiveRoot) { throw "Archivio Online '$archiveName' non trovato." }
Write-Host "Mappatura email esistenti nell'archivio cloud..." -ForegroundColor Yellow Write-Host "Mappatura email esistenti nell'archivio cloud..." -ForegroundColor Yellow
Initialize-ExistMap $archiveRoot Initialize-ExistMap $archiveRoot
Write-Host "Mappatura completata. Email trovate: $($script:mappaEmailEsistenti.Count)" Write-Host "Mappatura completata. Email trovate: $($script:mappaEmailEsistenti.Count)"
Write-Host "`nCaricamento PST: $pstPath" -ForegroundColor Yellow Write-Host "`nCaricamento PST: $pstPath" -ForegroundColor Yellow
$namespace.AddStore($pstPath) $namespace.AddStore($pstPath)
$pstStore = $namespace.Stores | Where-Object { $_.FilePath -eq $pstPath } $pstStore = $namespace.Stores | Where-Object { $_.FilePath -eq $pstPath }
$pstRoot = $pstStore.GetRootFolder() $pstRoot = $pstStore.GetRootFolder()
Invoke-FolderMigration $pstRoot $archiveRoot Invoke-FolderMigration $pstRoot $archiveRoot
Write-Host "`n`nMigrazione terminata con successo!" -ForegroundColor Green Write-Host "`n`nMigrazione terminata con successo!" -ForegroundColor Green
"------------------------------------------" | Out-File $logFile -Append "------------------------------------------" | Out-File $logFile -Append
"TOTALE SPOSTATE: $script:spostateTotali" | Out-File $logFile -Append "TOTALE SPOSTATE: $script:spostateTotali" | Out-File $logFile -Append
"DUPLICATI SALTATI: $script:duplicatiSaltati" | Out-File $logFile -Append "DUPLICATI SALTATI: $script:duplicatiSaltati" | Out-File $logFile -Append
} }
catch { catch {
Write-Host "`nErrore: $($_.Exception.Message)" -ForegroundColor Red Write-Host "`nErrore: $($_.Exception.Message)" -ForegroundColor Red
"ERRORE: $($_.Exception.Message)" | Out-File $logFile -Append "ERRORE: $($_.Exception.Message)" | Out-File $logFile -Append
} }
finally { finally {
if ($pstRoot) { $namespace.RemoveStore($pstRoot) } if ($pstRoot) { $namespace.RemoveStore($pstRoot) }
if ($outlook) { [System.Runtime.Interopservices.Marshal]::ReleaseComObject($outlook) | Out-Null } if ($outlook) { [System.Runtime.Interopservices.Marshal]::ReleaseComObject($outlook) | Out-Null }
} }

4
uv.lock generated
View File

@@ -120,7 +120,7 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "nicegui" }, { name = "nicegui" },
{ name = "pywebview" }, { name = "pywebview" },
{ name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "pywin32" },
{ name = "tqdm" }, { name = "tqdm" },
] ]
@@ -128,7 +128,7 @@ dependencies = [
requires-dist = [ requires-dist = [
{ name = "nicegui", specifier = ">=3.5.0" }, { name = "nicegui", specifier = ">=3.5.0" },
{ name = "pywebview", specifier = ">=6.1" }, { name = "pywebview", specifier = ">=6.1" },
{ name = "pywin32", marker = "sys_platform == 'win32'", specifier = ">=311" }, { name = "pywin32", specifier = ">=311" },
{ name = "tqdm", specifier = ">=4.67.1" }, { name = "tqdm", specifier = ">=4.67.1" },
] ]