Compare commits
11 Commits
16c23c059a
...
f14d032348
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f14d032348 | ||
|
|
49386bceca | ||
|
|
c11f7eaa7b | ||
|
|
1081ba9e1b | ||
|
|
7f3e047aff | ||
|
|
7282352401 | ||
|
|
1868859800 | ||
|
|
0080fa8586 | ||
|
|
16c271349f | ||
|
|
9cf6040a59 | ||
|
|
2d9426e87f |
1
ArchiviaEmail.bat
Normal file
1
ArchiviaEmail.bat
Normal file
@@ -0,0 +1 @@
|
||||
uv run .\ArchiviazioneMail.py
|
||||
@@ -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')
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
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()
|
||||
@@ -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 }
|
||||
}
|
||||
38
uv.lock
generated
38
uv.lock
generated
@@ -113,25 +113,6 @@ 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"
|
||||
@@ -374,6 +355,25 @@ 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"
|
||||
|
||||
Reference in New Issue
Block a user