Compare commits

..

3 Commits

Author SHA1 Message Date
alex 16c23c059a ripulito x win 2026-01-11 15:05:48 +01:00
alex c175ca6fe2 ref code 2026-01-11 14:57:32 +01:00
alex 509e557453 iniziale 2026-01-11 14:51:15 +01:00
9 changed files with 652 additions and 534 deletions
+10
View File
@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
-1
View File
@@ -1 +0,0 @@
uv run .\ArchiviazioneMail.py
+272 -231
View File
@@ -1,231 +1,272 @@
import win32com.client import os
import os import hashlib
import hashlib import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
import time from typing import Dict
from nicegui import ui, run from enum import IntEnum
import time
# --- LOGICA DI BACKEND --- from nicegui import ui, run
import win32com.client
def get_file_hash(file_path):
"""Calcola l'MD5 per identificare file identici ed evitare duplicati.""" # --- CONFIGURAZIONE LOGGING ---
hasher = hashlib.md5() logging.basicConfig(
with open(file_path, 'rb') as f: level=logging.INFO,
for chunk in iter(lambda: f.read(4096), b""): format='%(asctime)s - %(levelname)s - %(message)s'
hasher.update(chunk) )
return hasher.hexdigest() logger = logging.getLogger(__name__)
class ArchiverGUI: # --- COSTANTI OUTLOOK ---
def __init__(self): class OutlookConstants(IntEnum):
self.running = False """Costanti Outlook per migliorare leggibilità"""
self.progress = 0.0 FOLDER_INBOX = 6
self.current_mail = "In attesa di avvio..." MAIL_CLASS = 43
self.mesi_it = { ATTACHMENT_TYPE_FILE = 1
1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile", 5: "Maggio", 6: "Giugno",
7: "Luglio", 8: "Agosto", 9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre" # --- LOGICA DI BACKEND ---
}
def get_file_hash(file_path: str) -> str:
async def start_archiving(self, archive_name, onedrive_path, months_limit): """Calcola l'MD5 per identificare file identici ed evitare duplicati."""
self.running = True hasher = hashlib.md5()
self.progress = 0.0 with open(file_path, 'rb') as f:
ui.notify('Connessione a Outlook in corso...', color='info') for chunk in iter(lambda: f.read(4096), b""):
hasher.update(chunk)
# Avvia la logica in un thread dedicato per non bloccare l'interfaccia return hasher.hexdigest()
await run.io_bound(self.run_logic, archive_name, onedrive_path, months_limit)
class ArchiverGUI:
self.running = False """Gestisce lo stato e la logica di archiviazione delle email Outlook."""
if self.progress >= 1.0:
self.current_mail = "Archiviazione completata con successo!" MONTH_NAMES = {
ui.notify('Processo terminato!', type='positive') 1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile",
5: "Maggio", 6: "Giugno", 7: "Luglio", 8: "Agosto",
def run_logic(self, archive_name, onedrive_path, months_limit): 9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre"
try: }
if not os.path.exists(onedrive_path):
os.makedirs(onedrive_path) def __init__(self):
self.running: bool = False
outlook = win32com.client.Dispatch("Outlook.Application") self.progress: float = 0.0
namespace = outlook.GetNamespace("MAPI") self.current_mail: str = "In attesa di avvio..."
inbox = namespace.GetDefaultFolder(6) # 6 = OlFolderInbox
async def start_archiving(self, archive_name: str, onedrive_path: str, months_limit: int) -> None:
try: """Avvia il processo di archiviazione in background."""
archive_root = namespace.Folders.Item(archive_name) self.running = True
except Exception: self.progress = 0.0
self.current_mail = f"ERRORE: Archivio '{archive_name}' non trovato." ui.notify('Connessione a Outlook in corso...', color='info')
self.running = False
return # 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)
# Calcolo data limite per il filtro
cutoff_date = datetime.now() - timedelta(days=int(months_limit) * 30) self.running = False
filter_str = f"[ReceivedTime] < '{cutoff_date.strftime('%d/%m/%Y %H:%M')}'" if self.progress >= 1.0:
self.current_mail = "Archiviazione completata con successo!"
items = inbox.Items.Restrict(filter_str) ui.notify('Processo terminato!', type='positive')
items.Sort("[ReceivedTime]", True)
def run_logic(self, archive_name: str, onedrive_path: str, months_limit: int) -> None:
total = items.Count """Logica principale di archiviazione delle email."""
if total == 0: try:
self.current_mail = "Nessuna mail trovata con i criteri selezionati." if not os.path.exists(onedrive_path):
self.progress = 1.0 os.makedirs(onedrive_path)
return logger.info(f"Cartella OneDrive creata: {onedrive_path}")
processed_files = {} outlook = win32com.client.Dispatch("Outlook.Application")
namespace = outlook.GetNamespace("MAPI")
# Ciclo dal fondo verso l'inizio per non sballare gli indici di Outlook inbox = namespace.GetDefaultFolder(OutlookConstants.FOLDER_INBOX)
for i in range(total, 0, -1):
if not self.running: try:
self.current_mail = "Processo interrotto dall'utente." archive_root = namespace.Folders.Item(archive_name)
break except Exception as e:
error_msg = f"ERRORE: Archivio '{archive_name}' non trovato."
try: logger.error(error_msg, exc_info=e)
item = items.Item(i) self.current_mail = error_msg
rt = item.ReceivedTime self.running = False
received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute) return
self.current_mail = f"[{total-i+1}/{total}] {item.Subject[:40]}..." # Calcolo data limite per il filtro
self.progress = float((total - i + 1) / total) cutoff_date = datetime.now() - timedelta(days=int(months_limit) * 30)
filter_str = f"[ReceivedTime] < '{cutoff_date.strftime('%d/%m/%Y %H:%M')}'"
# 1. ELIMINAZIONE ALLEGATI E AGGIUNTA LINK (Mentre è ancora in Inbox)
# Questo garantisce i permessi di scrittura necessari per cancellare i file items = inbox.Items.Restrict(filter_str)
self.process_attachments(item, onedrive_path, received_time, processed_files) items.Sort("[ReceivedTime]", True)
item.Save()
total = items.Count
# 2. PREPARAZIONE CARTELLA DESTINAZIONE if total == 0:
anno_str = str(received_time.year) self.current_mail = "Nessuna mail trovata con i criteri selezionati."
nome_mese = self.mesi_it[received_time.month] self.progress = 1.0
month_folder_name = f"{received_time.month:02d}-{nome_mese}" logger.info("Nessuna email da archiviare")
return
y_f = self.get_or_create_folder(archive_root, anno_str)
target_folder = self.get_or_create_folder(y_f, month_folder_name) logger.info(f"Inizio archiviazione di {total} email")
processed_files: Dict[str, str] = {}
# 3. SPOSTAMENTO DEFINITIVO
item.Move(target_folder) # Ciclo dal fondo verso l'inizio per non sballare gli indici di Outlook
for i in range(total, 0, -1):
# Piccola pausa per dare respiro al server Exchange if not self.running:
time.sleep(0.1) self.current_mail = "Processo interrotto dall'utente."
except Exception as e: logger.info("Archiviazione interrotta dall'utente")
print(f"Errore su singola mail: {e}") break
continue
try:
except Exception as e: item = items.Item(i)
self.current_mail = f"Errore critico: {str(e)}" rt = item.ReceivedTime
self.running = False received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute)
def get_or_create_folder(self, parent, name): current_idx = total - i + 1
try: subject_preview = item.Subject[:40] if item.Subject else "(nessun oggetto)"
return parent.Folders.Item(name) self.current_mail = f"[{current_idx}/{total}] {subject_preview}..."
except: self.progress = float(current_idx / total)
return parent.Folders.Add(name)
# 1. ELIMINAZIONE ALLEGATI E AGGIUNTA LINK
def process_attachments(self, mail_item, onedrive_path, received_time, processed_files): self.process_attachments(item, onedrive_path, received_time, processed_files)
"""Rimuove gli allegati reali, li salva su OneDrive e inserisce il link nella mail.""" item.Save()
if mail_item.Class != 43: return # 43 = OlMail
# 2. PREPARAZIONE CARTELLA DESTINAZIONE
date_prefix = received_time.strftime("%Y-%m-%d") year_str = str(received_time.year)
month_name = self.MONTH_NAMES[received_time.month]
# Usiamo un ciclo reverse sugli allegati per la rimozione sicura month_folder_name = f"{received_time.month:02d}-{month_name}"
count = mail_item.Attachments.Count
for j in range(count, 0, -1): year_folder = self.get_or_create_folder(archive_root, year_str)
try: target_folder = self.get_or_create_folder(year_folder, month_folder_name)
att = mail_item.Attachments.Item(j)
# 3. SPOSTAMENTO DEFINITIVO
# Filtro: Solo file reali (Type 1), escludendo immagini nelle firme (Inline) item.Move(target_folder)
if att.Type != 1:
continue # Piccola pausa per dare respiro al server Exchange
time.sleep(0.1)
is_inline = False
try: except Exception as e:
# Controlla il tag MAPI per il Content-ID delle immagini incorporate logger.error(f"Errore elaborazione email {i}: {e}")
if att.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x3712001E"): continue
is_inline = True
except: pass except Exception as e:
error_msg = f"Errore critico: {str(e)}"
if is_inline: logger.critical(error_msg, exc_info=e)
continue self.current_mail = error_msg
print("da qui gestisco gli allegati") self.running = False
# Salvataggio fisico del file
temp_path = os.path.join(os.environ['TEMP'], att.FileName) def get_or_create_folder(self, parent, name: str):
att.SaveAsFile(temp_path) """Recupera una cartella se esiste, altrimenti la crea."""
f_hash = get_file_hash(temp_path) try:
return parent.Folders.Item(name)
# Nome file univoco con Data e Hash corto except Exception:
unique_name = f"{date_prefix}_{f_hash[:6]}_{att.FileName}" logger.debug(f"Cartella '{name}' non trovata, creazione...")
dest_path = os.path.join(onedrive_path, unique_name) return parent.Folders.Add(name)
if f_hash not in processed_files: def process_attachments(self, mail_item, onedrive_path: str, received_time: datetime,
if not os.path.exists(dest_path): processed_files: Dict[str, str]) -> None:
os.replace(temp_path, dest_path) """Rimuove gli allegati reali, li salva su OneDrive e inserisce il link nella mail."""
processed_files[f_hash] = dest_path if mail_item.Class != OutlookConstants.MAIL_CLASS:
else: return
if os.path.exists(temp_path):
os.remove(temp_path) date_prefix = received_time.strftime("%Y-%m-%d")
# Inserimento link HTML nel corpo della mail # Usiamo un ciclo reverse sugli allegati per la rimozione sicura
link_html = ( count = mail_item.Attachments.Count
f"<div style='background:#f3f3f3; padding:10px; border:1px dotted #666; margin:10px 0; font-family:Arial; font-size:12px;'>" for j in range(count, 0, -1):
f"<b>📎 Allegato spostato su OneDrive ({date_prefix}):</b><br>" try:
f"<a href='file:///{dest_path}'>{att.FileName}</a></div>" att = mail_item.Attachments.Item(j)
)
mail_item.HTMLBody = link_html + mail_item.HTMLBody # Filtro: Solo file reali, escludendo immagini nelle firme (Inline)
if att.Type != OutlookConstants.ATTACHMENT_TYPE_FILE:
# RIMOZIONE FISICA DALLA MAIL continue
mail_item.Attachments.Remove(j)
is_inline = False
except Exception as e: try:
print(f"Errore su allegato {j}: {e}") # Controlla il tag MAPI per il Content-ID delle immagini incorporate
continue if att.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x3712001E"):
is_inline = True
# --- INTERFACCIA GRAFICA (NICEGUI) --- except Exception:
pass
archiver = ArchiverGUI()
if is_inline:
@ui.page('/') continue
def main_page():
ui.query('body').style('background-color: #f0f2f5; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;') # Salvataggio fisico del file
temp_path = os.path.join(os.environ['TEMP'], att.FileName)
with ui.header().classes('items-center justify-between bg-blue-9 shadow-4 q-pa-md'): att.SaveAsFile(temp_path)
ui.label('Outlook Smart Archiver').classes('text-h5 text-white font-bold') f_hash = get_file_hash(temp_path)
ui.icon('inventory_2', size='lg', color='white')
# Nome file univoco con Data e Hash corto
with ui.column().classes('w-full max-w-3xl mx-auto q-pa-lg gap-6'): unique_name = f"{date_prefix}_{f_hash[:6]}_{att.FileName}"
dest_path = os.path.join(onedrive_path, unique_name)
# CARD CONFIGURAZIONE
with ui.card().classes('w-full q-pa-md shadow-2 border-l-4 border-blue-9'): if f_hash not in processed_files:
ui.label('Configurazione').classes('text-h6 text-blue-9 mb-2') if not os.path.exists(dest_path):
os.replace(temp_path, dest_path)
arc_name = ui.input('Nome dell\'Archivio Online', processed_files[f_hash] = dest_path
value='Archivio Online - Nome.Cognome@intesasanpaolo.com').classes('w-full') logger.debug(f"File salvato: {unique_name}")
else:
od_path = ui.input('Percorso locale OneDrive per Allegati', if os.path.exists(temp_path):
value=r'C:\Users\--UTENZA--\OneDrive - Intesa SanPaolo\Allegati_Outlook').classes('w-full') os.remove(temp_path)
logger.debug(f"File duplicato eliminato: {att.FileName}")
with ui.row().classes('w-full items-center mt-4 bg-grey-2 q-pa-sm rounded'):
ui.label('Mesi da mantenere nella Inbox:') # Inserimento link HTML nel corpo della mail
slider = ui.slider(min=0, max=24, value=6).classes('col px-4') link_html = (
ui.badge().bind_text_from(slider, 'value').classes('text-lg bg-blue-9') 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>"
# CARD PROGRESSO f"<a href='file:///{dest_path}'>{att.FileName}</a></div>"
with ui.card().classes('w-full q-pa-md shadow-2'): )
ui.label('Avanzamento').classes('text-h6 mb-2') mail_item.HTMLBody = link_html + mail_item.HTMLBody
with ui.column().classes('w-full items-center'): # RIMOZIONE FISICA DALLA MAIL
ui.label().bind_text_from(archiver, 'current_mail').classes('text-grey-8 text-italic mb-2 text-center') mail_item.Attachments.Remove(j)
# Contenitore della barra except Exception as e:
with ui.linear_progress().bind_value_from(archiver, 'progress').props('stripe size=40px color=blue-9').classes('rounded-borders relative shadow-1') as p: logger.error(f"Errore elaborazione allegato {j}: {e}")
# Etichetta percentuale centrata e troncata
ui.label().bind_text_from(archiver, 'progress', backward=lambda x: f'{x * 100:.2f}%') \ # --- INTERFACCIA GRAFICA (NICEGUI) ---
.classes('absolute-center text-blue text-weight-bold') \
.style('text-shadow: 1px 1px 3px rgba(0,0,0,0.4); font-size: 1.2rem;') archiver = ArchiverGUI()
with ui.row().classes('w-full justify-center mt-8 gap-4'): @ui.page('/')
ui.button('AVVIA ARCHIVIAZIONE', icon='rocket_launch', color='green-8', def main_page():
on_click=lambda: archiver.start_archiving(arc_name.value, od_path.value, slider.value)) \ ui.query('body').style('background-color: #f0f2f5; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;')
.classes('q-px-lg').bind_enabled_from(archiver, 'running', backward=lambda x: not x)
with ui.header().classes('items-center justify-between bg-blue-9 shadow-4 q-pa-md'):
ui.button('STOP', icon='block', color='red-8', ui.label('Outlook Smart Archiver').classes('text-h5 text-white font-bold')
on_click=lambda: setattr(archiver, 'running', False)) \ ui.icon('inventory_2', size='lg', color='white')
.classes('q-px-lg').bind_enabled_from(archiver, 'running')
with ui.column().classes('w-full max-w-3xl mx-auto q-pa-lg gap-6'):
ui.markdown('--- \n *Ricorda: Chiudi Outlook prima di avviare il processo per evitare blocchi.*').classes('text-center text-grey-6 text-xs')
# CARD CONFIGURAZIONE
# Avvio dell'applicazione with ui.card().classes('w-full q-pa-md shadow-2 border-l-4 border-blue-9'):
ui.run(native=True, window_size=(800, 600), title='Outlook Archiver Pro') 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')
+63 -63
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 }
} }
+183 -121
View File
@@ -1,122 +1,184 @@
import win32com.client import win32com.client
from datetime import datetime, timedelta import os
from tqdm import tqdm import hashlib
import time from datetime import datetime, timedelta
import locale from tqdm import tqdm
import time
# --- CONFIGURAZIONE --- import locale
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") # Per Windows/Linux moderni
# Imposta la localizzazione in italiano except: # noqa: E722
try: try:
locale.setlocale(locale.LC_TIME, "it_IT.UTF-8") locale.setlocale(locale.LC_TIME, "ita_ita") # Specifica per Windows
except: except: # noqa: E722
try: print("Impossibile impostare il locale italiano, uso i nomi manuali.")
locale.setlocale(locale.LC_TIME, "ita_ita")
except: # --- CONFIGURAZIONE ---
print("Locale italiano non impostato, uso nomi manuali.") ARCHIVE_NAME = "Archivio online - alessandro.battilani@intesasanpaolo.com"
ONEDRIVE_PATH = r"C:\Users\U086304\OneDrive - Intesa SanPaolo\Allegati_Outlook"
def main(): if not os.path.exists(ONEDRIVE_PATH):
print("Connessione a Outlook in corso...") os.makedirs(ONEDRIVE_PATH)
try: print(f"Cartella creata: {ONEDRIVE_PATH}")
outlook = win32com.client.Dispatch("Outlook.Application") MONTHS_LIMIT = 5
namespace = outlook.GetNamespace("MAPI") # ----------------------
archive_root = namespace.Folders.Item(ARCHIVE_NAME)
def get_file_hash(file_path):
# 6 = Inbox (ReceivedTime), 5 = Sent Items (SentOn) hasher = hashlib.md5()
folders_to_process = [ with open(file_path, 'rb') as f:
(namespace.GetDefaultFolder(6), "[ReceivedTime]", "ReceivedTime"), for chunk in iter(lambda: f.read(4096), b""):
(namespace.GetDefaultFolder(5), "[SentOn]", "SentOn") hasher.update(chunk)
] return hasher.hexdigest()
except Exception as e:
print(f"Errore connessione: {e}") def main():
return if not os.path.exists(ONEDRIVE_PATH):
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...")
# Convertiamo la stringa in un numero intero try:
MONTHS_LIMIT = int(stringa_mounths_limit) outlook = win32com.client.Dispatch("Outlook.Application")
namespace = outlook.GetNamespace("MAPI")
cutoff_date = datetime.now() - timedelta(days=MONTHS_LIMIT * 30) inbox = namespace.GetDefaultFolder(6)
filter_date_str = cutoff_date.strftime("%d/%m/%Y %H:%M") archive_root = namespace.Folders.Item(ARCHIVE_NAME)
except Exception as e:
mesi_it = {1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile", 5: "Maggio", 6: "Giugno", print(f"Errore di connessione: {e}")
7: "Luglio", 8: "Agosto", 9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre"} return
for source_folder, filter_attr, time_attr in folders_to_process: cutoff_date = datetime.now() - timedelta(days=MONTHS_LIMIT * 30)
print(f"\n--- Analisi cartella: {source_folder.Name} ---") filter_date_str = cutoff_date.strftime("%d/%m/%Y %H:%M")
filter_str = f"[ReceivedTime] < '{filter_date_str}'"
filter_str = f"{filter_attr} < '{filter_date_str}'"
items = source_folder.Items.Restrict(filter_str) items = inbox.Items.Restrict(filter_str)
items.Sort(filter_attr, True) items.Sort("[ReceivedTime]", True)
total_items = items.Count total_items = items.Count
if total_items == 0: if total_items == 0:
print(f"Nessuna mail più vecchia di {MONTHS_LIMIT} mesi in {source_folder.Name}.") print("Nessuna email da archiviare.")
continue return
archived_count = 0 print(f"Trovate {total_items} email vecchie. Inizio archiviazione...")
# 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: processed_files = {}
for i in range(total_items, 0, -1): archived_count = 0
try: mesi_it = {1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile", 5: "Maggio", 6: "Giugno",
item = items.Item(i) 7: "Luglio", 8: "Agosto", 9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre"}
if not hasattr(item, time_attr): with tqdm(total=total_items, desc="Archiviazione", unit="mail", colour='green') as pbar:
pbar.update(1) for i in range(total_items, 0, -1):
continue try:
item = items.Item(i)
# Recupero data dinamico pbar.update(1)
rt = getattr(item, time_attr)
received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute) if not hasattr(item, 'ReceivedTime'):
anno_str = str(received_time.year) continue
nome_mese = mesi_it[received_time.month]
pbar.set_description(f"Archiviazione {source_folder.Name} - Mese: {nome_mese} {anno_str}") rt = item.ReceivedTime
received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute)
# Gestione struttura cartelle (Source -> Anno -> Mese) anno_str = str(received_time.year)
try: nome_mese = mesi_it[received_time.month]
arch_type = archive_root.Folders.Item(source_folder.Name) pbar.set_description(f"Mese: {nome_mese} {anno_str}")
except:
arch_type = archive_root.Folders.Add(source_folder.Name) # Cartelle
month_folder_name = f"{received_time.month:02d}-{nome_mese}"
try: try:
y_f = arch_type.Folders.Item(anno_str) y_f = archive_root.Folders.Item(anno_str)
except: except: # noqa: E722
y_f = arch_type.Folders.Add(anno_str) y_f = archive_root.Folders.Add(anno_str)
try:
month_folder_name = f"{received_time.month:02d}-{nome_mese}" target_folder = y_f.Folders.Item(month_folder_name)
try: except: # noqa: E722
target_folder = y_f.Folders.Item(month_folder_name) target_folder = y_f.Folders.Add(month_folder_name)
except:
target_folder = y_f.Folders.Add(month_folder_name) # --- TENTA LO SPOSTAMENTO CON RETRY ---
archived_item = None
# --- TENTA LO SPOSTAMENTO CON RETRY (Reintegrato) --- for tentativo in range(3):
archived_item = None try:
for tentativo in range(3): archived_item = item.Move(target_folder)
try: if archived_item:
archived_item = item.Move(target_folder) archived_item.Save()
if archived_item: time.sleep(0.5) # Pausa vitale per il server
archived_item.Save() break
time.sleep(0.4) # Pausa vitale except: # noqa: E722
break time.sleep(1) # Aspetta se il server è occupato
except:
time.sleep(1) # Attesa per server busy if not archived_item:
pbar.set_postfix(error="Move fallito")
if archived_item: continue
archived_count += 1
pbar.set_postfix(successo=archived_count) # --- GESTIONE ALLEGATI (SOLO FILE VERI) ---
else: if archived_item.Class == 43 and archived_item.Attachments.Count > 0:
pbar.set_postfix(error="Move fallito") has_changed = False
attachments = [archived_item.Attachments.Item(j) for j in range(1, archived_item.Attachments.Count + 1)]
except Exception as e:
pbar.set_postfix(err=str(e)[:15]) for att in attachments:
try:
# Update della barra sempre alla fine del ciclo i # 1. Filtro base: solo allegati di tipo "Valore" (file)
pbar.update(1) if att.Type != 1:
continue
print(f"\nOperazione conclusa con successo.")
# 2. FILTRO AVANZATO: Salta le immagini nelle firme (Inline Images)
if __name__ == "__main__": # Controlliamo se l'allegato ha un "Content-ID" (tipico delle immagini incorporate)
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()
+98 -98
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 }
} }
+6
View File
@@ -0,0 +1,6 @@
def main():
print("Hello from archivemail!")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -1,5 +1,5 @@
[project] [project]
name = "macro-outlook" name = "archivemail"
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
Generated
+19 -19
View File
@@ -113,6 +113,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
] ]
[[package]]
name = "archivemail"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "nicegui" },
{ name = "pywebview" },
{ name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "tqdm" },
]
[package.metadata]
requires-dist = [
{ name = "nicegui", specifier = ">=3.5.0" },
{ name = "pywebview", specifier = ">=6.1" },
{ name = "pywin32", marker = "sys_platform == 'win32'", specifier = ">=311" },
{ name = "tqdm", specifier = ">=4.67.1" },
]
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "25.4.0" version = "25.4.0"
@@ -355,25 +374,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
] ]
[[package]]
name = "macro-outlook"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "nicegui" },
{ name = "pywebview" },
{ name = "pywin32" },
{ name = "tqdm" },
]
[package.metadata]
requires-dist = [
{ name = "nicegui", specifier = ">=3.5.0" },
{ name = "pywebview", specifier = ">=6.1" },
{ name = "pywin32", specifier = ">=311" },
{ name = "tqdm", specifier = ">=4.67.1" },
]
[[package]] [[package]]
name = "markdown2" name = "markdown2"
version = "2.5.4" version = "2.5.4"