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
+79 -38
View File
@@ -1,13 +1,30 @@
import win32com.client
import os import os
import hashlib import hashlib
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict
from enum import IntEnum
import time import time
from nicegui import ui, run 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 --- # --- LOGICA DI BACKEND ---
def get_file_hash(file_path): def get_file_hash(file_path: str) -> str:
"""Calcola l'MD5 per identificare file identici ed evitare duplicati.""" """Calcola l'MD5 per identificare file identici ed evitare duplicati."""
hasher = hashlib.md5() hasher = hashlib.md5()
with open(file_path, 'rb') as f: with open(file_path, 'rb') as f:
@@ -16,16 +33,21 @@ def get_file_hash(file_path):
return hasher.hexdigest() return hasher.hexdigest()
class ArchiverGUI: class ArchiverGUI:
def __init__(self): """Gestisce lo stato e la logica di archiviazione delle email Outlook."""
self.running = False
self.progress = 0.0 MONTH_NAMES = {
self.current_mail = "In attesa di avvio..." 1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile",
self.mesi_it = { 5: "Maggio", 6: "Giugno", 7: "Luglio", 8: "Agosto",
1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile", 5: "Maggio", 6: "Giugno", 9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre"
7: "Luglio", 8: "Agosto", 9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre"
} }
async def start_archiving(self, archive_name, onedrive_path, months_limit): 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.running = True
self.progress = 0.0 self.progress = 0.0
ui.notify('Connessione a Outlook in corso...', color='info') ui.notify('Connessione a Outlook in corso...', color='info')
@@ -38,19 +60,23 @@ class ArchiverGUI:
self.current_mail = "Archiviazione completata con successo!" self.current_mail = "Archiviazione completata con successo!"
ui.notify('Processo terminato!', type='positive') ui.notify('Processo terminato!', type='positive')
def run_logic(self, archive_name, onedrive_path, months_limit): def run_logic(self, archive_name: str, onedrive_path: str, months_limit: int) -> None:
"""Logica principale di archiviazione delle email."""
try: try:
if not os.path.exists(onedrive_path): if not os.path.exists(onedrive_path):
os.makedirs(onedrive_path) os.makedirs(onedrive_path)
logger.info(f"Cartella OneDrive creata: {onedrive_path}")
outlook = win32com.client.Dispatch("Outlook.Application") outlook = win32com.client.Dispatch("Outlook.Application")
namespace = outlook.GetNamespace("MAPI") namespace = outlook.GetNamespace("MAPI")
inbox = namespace.GetDefaultFolder(6) # 6 = OlFolderInbox inbox = namespace.GetDefaultFolder(OutlookConstants.FOLDER_INBOX)
try: try:
archive_root = namespace.Folders.Item(archive_name) archive_root = namespace.Folders.Item(archive_name)
except Exception: except Exception as e:
self.current_mail = f"ERRORE: Archivio '{archive_name}' non trovato." error_msg = f"ERRORE: Archivio '{archive_name}' non trovato."
logger.error(error_msg, exc_info=e)
self.current_mail = error_msg
self.running = False self.running = False
return return
@@ -65,14 +91,17 @@ class ArchiverGUI:
if total == 0: if total == 0:
self.current_mail = "Nessuna mail trovata con i criteri selezionati." self.current_mail = "Nessuna mail trovata con i criteri selezionati."
self.progress = 1.0 self.progress = 1.0
logger.info("Nessuna email da archiviare")
return return
processed_files = {} 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 # Ciclo dal fondo verso l'inizio per non sballare gli indici di Outlook
for i in range(total, 0, -1): for i in range(total, 0, -1):
if not self.running: if not self.running:
self.current_mail = "Processo interrotto dall'utente." self.current_mail = "Processo interrotto dall'utente."
logger.info("Archiviazione interrotta dall'utente")
break break
try: try:
@@ -80,44 +109,52 @@ class ArchiverGUI:
rt = item.ReceivedTime rt = item.ReceivedTime
received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute) received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute)
self.current_mail = f"[{total-i+1}/{total}] {item.Subject[:40]}..." current_idx = total - i + 1
self.progress = float((total - i + 1) / total) 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 (Mentre è ancora in Inbox) # 1. ELIMINAZIONE ALLEGATI E AGGIUNTA LINK
# Questo garantisce i permessi di scrittura necessari per cancellare i file
self.process_attachments(item, onedrive_path, received_time, processed_files) self.process_attachments(item, onedrive_path, received_time, processed_files)
item.Save() item.Save()
# 2. PREPARAZIONE CARTELLA DESTINAZIONE # 2. PREPARAZIONE CARTELLA DESTINAZIONE
anno_str = str(received_time.year) year_str = str(received_time.year)
nome_mese = self.mesi_it[received_time.month] month_name = self.MONTH_NAMES[received_time.month]
month_folder_name = f"{received_time.month:02d}-{nome_mese}" month_folder_name = f"{received_time.month:02d}-{month_name}"
y_f = self.get_or_create_folder(archive_root, anno_str) year_folder = self.get_or_create_folder(archive_root, year_str)
target_folder = self.get_or_create_folder(y_f, month_folder_name) target_folder = self.get_or_create_folder(year_folder, month_folder_name)
# 3. SPOSTAMENTO DEFINITIVO # 3. SPOSTAMENTO DEFINITIVO
item.Move(target_folder) item.Move(target_folder)
# Piccola pausa per dare respiro al server Exchange # Piccola pausa per dare respiro al server Exchange
time.sleep(0.1) time.sleep(0.1)
except Exception as e: except Exception as e:
print(f"Errore su singola mail: {e}") logger.error(f"Errore elaborazione email {i}: {e}")
continue continue
except Exception as e: except Exception as e:
self.current_mail = f"Errore critico: {str(e)}" error_msg = f"Errore critico: {str(e)}"
logger.critical(error_msg, exc_info=e)
self.current_mail = error_msg
self.running = False self.running = False
def get_or_create_folder(self, parent, name): def get_or_create_folder(self, parent, name: str):
"""Recupera una cartella se esiste, altrimenti la crea."""
try: try:
return parent.Folders.Item(name) return parent.Folders.Item(name)
except: except Exception:
logger.debug(f"Cartella '{name}' non trovata, creazione...")
return parent.Folders.Add(name) return parent.Folders.Add(name)
def process_attachments(self, mail_item, onedrive_path, received_time, processed_files): 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.""" """Rimuove gli allegati reali, li salva su OneDrive e inserisce il link nella mail."""
if mail_item.Class != 43: return # 43 = OlMail if mail_item.Class != OutlookConstants.MAIL_CLASS:
return
date_prefix = received_time.strftime("%Y-%m-%d") date_prefix = received_time.strftime("%Y-%m-%d")
@@ -127,8 +164,8 @@ class ArchiverGUI:
try: try:
att = mail_item.Attachments.Item(j) att = mail_item.Attachments.Item(j)
# Filtro: Solo file reali (Type 1), escludendo immagini nelle firme (Inline) # Filtro: Solo file reali, escludendo immagini nelle firme (Inline)
if att.Type != 1: if att.Type != OutlookConstants.ATTACHMENT_TYPE_FILE:
continue continue
is_inline = False is_inline = False
@@ -136,11 +173,12 @@ class ArchiverGUI:
# Controlla il tag MAPI per il Content-ID delle immagini incorporate # Controlla il tag MAPI per il Content-ID delle immagini incorporate
if att.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x3712001E"): if att.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x3712001E"):
is_inline = True is_inline = True
except: pass except Exception:
pass
if is_inline: if is_inline:
continue continue
print("da qui gestisco gli allegati")
# Salvataggio fisico del file # Salvataggio fisico del file
temp_path = os.path.join(os.environ['TEMP'], att.FileName) temp_path = os.path.join(os.environ['TEMP'], att.FileName)
att.SaveAsFile(temp_path) att.SaveAsFile(temp_path)
@@ -154,9 +192,11 @@ class ArchiverGUI:
if not os.path.exists(dest_path): if not os.path.exists(dest_path):
os.replace(temp_path, dest_path) os.replace(temp_path, dest_path)
processed_files[f_hash] = dest_path processed_files[f_hash] = dest_path
logger.debug(f"File salvato: {unique_name}")
else: else:
if os.path.exists(temp_path): if os.path.exists(temp_path):
os.remove(temp_path) os.remove(temp_path)
logger.debug(f"File duplicato eliminato: {att.FileName}")
# Inserimento link HTML nel corpo della mail # Inserimento link HTML nel corpo della mail
link_html = ( link_html = (
@@ -170,8 +210,7 @@ class ArchiverGUI:
mail_item.Attachments.Remove(j) mail_item.Attachments.Remove(j)
except Exception as e: except Exception as e:
print(f"Errore su allegato {j}: {e}") logger.error(f"Errore elaborazione allegato {j}: {e}")
continue
# --- INTERFACCIA GRAFICA (NICEGUI) --- # --- INTERFACCIA GRAFICA (NICEGUI) ---
@@ -217,9 +256,10 @@ def main_page():
.style('text-shadow: 1px 1px 3px rgba(0,0,0,0.4); font-size: 1.2rem;') .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'): with ui.row().classes('w-full justify-center mt-8 gap-4'):
ui.button('AVVIA ARCHIVIAZIONE', icon='rocket_launch', color='green-8', 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)) \ 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) .classes('q-px-lg')
start_btn.bind_enabled_from(archiver, 'running', backward=lambda x: not x)
ui.button('STOP', icon='block', color='red-8', ui.button('STOP', icon='block', color='red-8',
on_click=lambda: setattr(archiver, 'running', False)) \ on_click=lambda: setattr(archiver, 'running', False)) \
@@ -228,4 +268,5 @@ def main_page():
ui.markdown('--- \n *Ricorda: Chiudi Outlook prima di avviare il processo per evitare blocchi.*').classes('text-center text-grey-6 text-xs') 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 # Avvio dell'applicazione
if __name__ == '__main__':
ui.run(native=True, window_size=(800, 600), title='Outlook Archiver Pro') ui.run(native=True, window_size=(800, 600), title='Outlook Archiver Pro')
+126 -64
View File
@@ -1,122 +1,184 @@
import win32com.client import win32com.client
import os
import hashlib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from tqdm import tqdm from tqdm import tqdm
import time import time
import locale import locale
# --- CONFIGURAZIONE ---
ARCHIVE_NAME = "Archivio online - alessandro.battilani@intesasanpaolo.com"
# MONTHS_LIMIT = 3
# ----------------------
# Imposta la localizzazione in italiano # Imposta la localizzazione in italiano
try: try:
locale.setlocale(locale.LC_TIME, "it_IT.UTF-8") locale.setlocale(locale.LC_TIME, "it_IT.UTF-8") # Per Windows/Linux moderni
except: except: # noqa: E722
try: try:
locale.setlocale(locale.LC_TIME, "ita_ita") locale.setlocale(locale.LC_TIME, "ita_ita") # Specifica per Windows
except: except: # noqa: E722
print("Locale italiano non impostato, uso nomi manuali.") 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(): def main():
if not os.path.exists(ONEDRIVE_PATH):
os.makedirs(ONEDRIVE_PATH)
print("Connessione a Outlook in corso...") print("Connessione a Outlook in corso...")
try: try:
outlook = win32com.client.Dispatch("Outlook.Application") outlook = win32com.client.Dispatch("Outlook.Application")
namespace = outlook.GetNamespace("MAPI") namespace = outlook.GetNamespace("MAPI")
inbox = namespace.GetDefaultFolder(6)
archive_root = namespace.Folders.Item(ARCHIVE_NAME) 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: except Exception as e:
print(f"Errore connessione: {e}") print(f"Errore di connessione: {e}")
return 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) cutoff_date = datetime.now() - timedelta(days=MONTHS_LIMIT * 30)
filter_date_str = cutoff_date.strftime("%d/%m/%Y %H:%M") filter_date_str = cutoff_date.strftime("%d/%m/%Y %H:%M")
filter_str = f"[ReceivedTime] < '{filter_date_str}'"
mesi_it = {1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile", 5: "Maggio", 6: "Giugno", items = inbox.Items.Restrict(filter_str)
7: "Luglio", 8: "Agosto", 9: "Settembre", 10: "Ottobre", 11: "Novembre", 12: "Dicembre"} items.Sort("[ReceivedTime]", True)
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 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
print(f"Trovate {total_items} email vecchie. Inizio archiviazione...")
processed_files = {}
archived_count = 0 archived_count = 0
# tqdm posizionato esternamente per monitorare il progresso reale mesi_it = {1: "Gennaio", 2: "Febbraio", 3: "Marzo", 4: "Aprile", 5: "Maggio", 6: "Giugno",
with tqdm(total=total_items, desc=f"Archiviazione {source_folder.Name}", unit="mail", colour='green') as pbar: 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): for i in range(total_items, 0, -1):
try: try:
item = items.Item(i) item = items.Item(i)
if not hasattr(item, time_attr):
pbar.update(1) pbar.update(1)
if not hasattr(item, 'ReceivedTime'):
continue continue
# Recupero data dinamico rt = item.ReceivedTime
rt = getattr(item, time_attr)
received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute) received_time = datetime(rt.year, rt.month, rt.day, rt.hour, rt.minute)
anno_str = str(received_time.year) anno_str = str(received_time.year)
nome_mese = mesi_it[received_time.month] nome_mese = mesi_it[received_time.month]
pbar.set_description(f"Archiviazione {source_folder.Name} - Mese: {nome_mese} {anno_str}") pbar.set_description(f"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)
# Cartelle
month_folder_name = f"{received_time.month:02d}-{nome_mese}" 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: try:
target_folder = y_f.Folders.Item(month_folder_name) target_folder = y_f.Folders.Item(month_folder_name)
except: except: # noqa: E722
target_folder = y_f.Folders.Add(month_folder_name) target_folder = y_f.Folders.Add(month_folder_name)
# --- TENTA LO SPOSTAMENTO CON RETRY (Reintegrato) --- # --- TENTA LO SPOSTAMENTO CON RETRY ---
archived_item = None archived_item = None
for tentativo in range(3): for tentativo in range(3):
try: try:
archived_item = item.Move(target_folder) archived_item = item.Move(target_folder)
if archived_item: if archived_item:
archived_item.Save() archived_item.Save()
time.sleep(0.4) # Pausa vitale time.sleep(0.5) # Pausa vitale per il server
break break
except: except: # noqa: E722
time.sleep(1) # Attesa per server busy time.sleep(1) # Aspetta se il server è occupato
if archived_item: if not archived_item:
archived_count += 1
pbar.set_postfix(successo=archived_count)
else:
pbar.set_postfix(error="Move fallito") 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: except Exception as e:
pbar.set_postfix(err=str(e)[:15]) continue
# Update della barra sempre alla fine del ciclo i if has_changed:
pbar.update(1) archived_item.Save()
print(f"\nOperazione conclusa con successo.") 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__": if __name__ == "__main__":
main() main()
+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"