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 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"<div style='background:#f3f3f3; padding:10px; border:1px dotted #666; margin:10px 0; font-family:Arial; font-size:12px;'>"
f"<b>📎 Allegato spostato su OneDrive ({date_prefix}):</b><br>"
f"<a href='file:///{dest_path}'>{att.FileName}</a></div>"
)
mail_item.HTMLBody = link_html + mail_item.HTMLBody
# RIMOZIONE FISICA DALLA MAIL
mail_item.Attachments.Remove(j)
except Exception as e:
print(f"Errore su allegato {j}: {e}")
continue
# --- INTERFACCIA GRAFICA (NICEGUI) ---
archiver = ArchiverGUI()
@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')
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')
+63 -63
View File
@@ -1,64 +1,64 @@
# --- 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 }
# --- 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 }
}
+183 -121
View File
@@ -1,122 +1,184 @@
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:
try:
locale.setlocale(locale.LC_TIME, "ita_ita")
except:
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:
arch_type = archive_root.Folders.Add(source_folder.Name)
try:
y_f = arch_type.Folders.Item(anno_str)
except:
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:
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:
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(f"\nOperazione conclusa con successo.")
if __name__ == "__main__":
import win32com.client
import os
import hashlib
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") # Per Windows/Linux moderni
except: # noqa: E722
try:
locale.setlocale(locale.LC_TIME, "ita_ita") # Specifica per Windows
except: # noqa: E722
print("Impossibile impostare il locale italiano, uso i nomi manuali.")
# --- CONFIGURAZIONE ---
ARCHIVE_NAME = "Archivio online - alessandro.battilani@intesasanpaolo.com"
ONEDRIVE_PATH = r"C:\Users\U086304\OneDrive - Intesa SanPaolo\Allegati_Outlook"
if not os.path.exists(ONEDRIVE_PATH):
os.makedirs(ONEDRIVE_PATH)
print(f"Cartella creata: {ONEDRIVE_PATH}")
MONTHS_LIMIT = 5
# ----------------------
def get_file_hash(file_path):
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()
def main():
if not os.path.exists(ONEDRIVE_PATH):
os.makedirs(ONEDRIVE_PATH)
print("Connessione a Outlook in corso...")
try:
outlook = win32com.client.Dispatch("Outlook.Application")
namespace = outlook.GetNamespace("MAPI")
inbox = namespace.GetDefaultFolder(6)
archive_root = namespace.Folders.Item(ARCHIVE_NAME)
except Exception as e:
print(f"Errore di connessione: {e}")
return
cutoff_date = datetime.now() - timedelta(days=MONTHS_LIMIT * 30)
filter_date_str = cutoff_date.strftime("%d/%m/%Y %H:%M")
filter_str = f"[ReceivedTime] < '{filter_date_str}'"
items = inbox.Items.Restrict(filter_str)
items.Sort("[ReceivedTime]", True)
total_items = items.Count
if total_items == 0:
print("Nessuna email da archiviare.")
return
print(f"Trovate {total_items} email vecchie. Inizio archiviazione...")
processed_files = {}
archived_count = 0
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"}
with tqdm(total=total_items, desc="Archiviazione", unit="mail", colour='green') as pbar:
for i in range(total_items, 0, -1):
try:
item = items.Item(i)
pbar.update(1)
if not hasattr(item, 'ReceivedTime'):
continue
rt = item.ReceivedTime
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"Mese: {nome_mese} {anno_str}")
# Cartelle
month_folder_name = f"{received_time.month:02d}-{nome_mese}"
try:
y_f = archive_root.Folders.Item(anno_str)
except: # noqa: E722
y_f = archive_root.Folders.Add(anno_str)
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 ---
archived_item = None
for tentativo in range(3):
try:
archived_item = item.Move(target_folder)
if archived_item:
archived_item.Save()
time.sleep(0.5) # Pausa vitale per il server
break
except: # noqa: E722
time.sleep(1) # Aspetta se il server è occupato
if not archived_item:
pbar.set_postfix(error="Move fallito")
continue
# --- GESTIONE ALLEGATI (SOLO FILE VERI) ---
if archived_item.Class == 43 and archived_item.Attachments.Count > 0:
has_changed = False
attachments = [archived_item.Attachments.Item(j) for j in range(1, archived_item.Attachments.Count + 1)]
for att in attachments:
try:
# 1. Filtro base: solo allegati di tipo "Valore" (file)
if att.Type != 1:
continue
# 2. FILTRO AVANZATO: Salta le immagini nelle firme (Inline Images)
# 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()
+98 -98
View File
@@ -1,99 +1,99 @@
# --- 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 }
# --- 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 }
}
+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]
name = "macro-outlook"
name = "archivemail"
version = "0.1.0"
description = "Add your description here"
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" },
]
[[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]]
name = "attrs"
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" },
]
[[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]]
name = "markdown2"
version = "2.5.4"