Vai al contenuto
Torna al blog

Refactoring di MailVault: dal monolite ai moduli

7 min di lettura

MailVault è cresciuto in fretta. Nelle ultime settimane abbiamo rilasciato le scorciatoie da tastiera, la casella unificata, i modelli di email, le informazioni sui mittenti, le regole di pulizia automatica, l'annulla invio, lo spostamento tra cartelle e un intero livello di trasporto per la Microsoft Graph API. Le funzioni arrivavano, gli utenti erano contenti — e il codice cominciava a scricchiolare sotto il peso.

Lo store centrale dello stato — un unico file Zustand chiamato mailStore.js — era arrivato a oltre 4.300 righe. Gestiva tutto: gestione degli account, caricamento delle email, cache, ricerca, threading, la biforcazione per la Graph API, la sincronizzazione delta CONDSTORE, le operazioni di massa e altro ancora. Il main.rs del backend Rust seguiva la stessa traiettoria. Componenti come SettingsPage ed EmailViewer si erano gonfiati fino a diventare muri monolitici di JSX. Tutto funzionava ancora, ma aggiungere qualcosa di nuovo significava attraversare un labirinto.

Era ora di rifattorizzare — non perché qualcosa fosse rotto, ma perché la velocità con cui rilasciavamo funzioni nuove stava calando. Questo post racconta che cosa abbiamo fatto, perché, e dove stiamo andando.

Il problema di un unico store gigantesco

Zustand è ottimo per la gestione dello stato. Ma quando un singolo file di store supera le poche migliaia di righe cominciano i problemi pratici: ogni funzione tocca lo stesso file, i conflitti di merge diventano la norma ed è difficile capire che cosa dipende da che cosa. Il nostro mailStore aveva la logica di caricamento delle email intrecciata con il codice di trasporto della Graph API, la gestione della cache LRU accanto allo stato della ricerca e gli algoritmi di threading mescolati al cambio di account.

Lo stesso schema si ripeteva sul lato Rust. main.rs conteneva gli helper per i percorsi Maildir, le operazioni sul portachiavi, la gestione della cache, la gestione degli allegati, la logica delle notifiche e le utility di sistema — tutto in un unico file. Nel modulo IMAP il pooling delle connessioni era ingarbugliato con le operazioni a livello di protocollo.

Caricamento delle email a due flussi

Il cambiamento architetturale più grande della v2.2.0 è stato ripensare come si caricano le email quando cambi account o cartella. Il vecchio approccio aveva sei fasi sovrapposte che giravano in una sequenza fragile: controlla la cache, mostra i dati in cache, connettiti a IMAP, recupera le intestazioni, unisci i risultati, aggiorna l'interfaccia. Se cambiavi account in fretta, queste fasi potevano intrecciarsi e per un istante vedevi le email dell'account sbagliato — il bug delle "email vuote".

Il nuovo disegno è un modello pulito a due flussi. Quando attivi un account partono in parallelo due flussi indipendenti:

Il flusso 1 (locale) carica dal disco le intestazioni in cache e le email salvate localmente. È quasi istantaneo — l'interfaccia si popola in pochi millisecondi grazie alla cache headers.json e a un nuovo local-index.json che elimina la necessità di analizzare il MIME di ogni file .eml su disco.

Il flusso 2 (server) si collega a IMAP (o alla Graph API per gli account Microsoft), recupera lo stato attuale del server e unisce le modifiche alla vista locale. Le email nuove compaiono, quelle eliminate spariscono, i contrassegni letto/da leggere si aggiornano — tutto senza sfarfallii e senza sostituire quello che è già a schermo.

Entrambi i flussi sono avvolti in un AbortController. Se cambi account prima che uno dei due finisca, il lavoro in corso viene annullato in modo pulito. Niente race condition, niente dati vecchi, niente "email vuote".

Estrazione dei moduli

La prima fase del refactoring si è concentrata sul tirare fuori il codice che non aveva motivo di stare dentro lo store o dentro main.rs.

Sul lato JavaScript abbiamo estratto:

Configurazione Graph — la mappatura dei nomi delle cartelle di Microsoft Graph (Inbox → INBOX, Sent Items → Sent, ecc.) era duplicata tre volte nel codice. Ora vive in un unico modulo graphConfig.js.

Cache LRU — la cache degli account (lo stato delle email di un massimo di 8 account, per un cambio istantaneo) e la cache dei corpi (il contenuto delle email, con limiti di memoria configurabili) sono state estratte in un modulo dedicato, con un'unica implementazione LRU condivisa.

Ricerca — lo stato e la logica della ricerca erano intrecciati nello store principale pur essendo in gran parte indipendenti. Tirarli fuori ha reso più leggibile sia lo store sia il codice della ricerca.

Tracciamento delle prestazioni — avevamo chiamate console.time improvvisate sparse nel codice di caricamento. Le abbiamo sostituite con una utility strutturata, perfTrace, che si può attivare e disattivare, rendendo molto più pulito il debug delle prestazioni.

Scomposizione dei componenti

Due componenti React facevano decisamente troppo: SettingsPage ed EmailViewer.

SettingsPage disegnava tutte le schede delle impostazioni in un solo file — account, aspetto, scorciatoie da tastiera, notifiche, modelli, pulizia automatica e altro ancora. L'abbiamo diviso in singoli componenti per scheda, ciascuno in una sottocartella settings/. Ora il componente padre si occupa solo del cambio di scheda e disegna la scheda attiva.

EmailViewer era cresciuto fino a gestire la visualizzazione della singola email, la vista conversazione del thread, il rendering degli allegati, i badge di verifica DKIM, le informazioni sui mittenti e le azioni di risposta e inoltro — tutto in un unico componente. Abbiamo estratto ThreadView, EmailHeader e AttachmentBar in componenti mirati, dentro una sottocartella email/.

Il risultato sono file più piccoli, più facili da percorrere, da testare e da modificare senza rompere per sbaglio qualcosa che non c'entra.

Il piano in cinque fasi

Quello che è uscito nella v2.2.0 era l'inizio, non la fine. Abbiamo disegnato un piano di refactoring in cinque fasi che stiamo eseguendo un pezzo alla volta:

Fase 1: fondamenta condivise — estrarre costanti e utility condivise (configurazione Graph, costanti Rust, tracciamento delle prestazioni). Fatto.

Fase 2: backend Rust — dividere main.rs in moduli mirati: operazioni Maildir, accesso al portachiavi, gestione della cache, gestione degli allegati, notifiche e utility di sistema. Il modulo IMAP viene diviso in operazioni a livello di protocollo e gestione del pool di connessioni.

Fase 3: decomposizione dello store — creare store dedicati useSearchStore e useThreadStore, estrarre in moduli di servizio il trasporto della Graph API, la gestione della cache e il caricamento delle email. L'obiettivo: portare mailStore da 4.300 righe a circa 2.000.

Fase 4: estrazione dei componenti — continuare a smontare i componenti grandi. AccountModal si divide in sottocomponenti per il rilevamento del server e per la configurazione OAuth. Gli altri componenti fuori misura ricevono lo stesso trattamento.

Fase 5: pulizia — rimuovere il codice morto, unificare le utility duplicate (come hasValidCredentials, che esisteva in più punti) e dividere il modulo di utility del database in file mirati.

Rifattorizzare senza regressioni

La parte più difficile di qualsiasi refactoring è assicurarsi che non si rompa niente. MailVault gestisce la posta delle persone — se il cambio account inizia a mostrare i messaggi sbagliati o ricompare una race condition nel caricamento, è un problema vero.

Il nostro approccio: rifattorizzare un confine alla volta, verificare che tutto funzioni ancora, poi passare al confine successivo. Gli oltre 600 test automatici distribuiti su quattro livelli ci fanno da rete di sicurezza, ma dopo ogni fase facciamo anche test manuali — cambiare account in rapida successione, andare offline a metà caricamento, provare con la casella di posta da 17.000 email, verificare che gli account Graph API e i normali account IMAP si comportino entrambi correttamente.

Abbiamo anche preso una decisione deliberata sull'architettura degli store. Invece di dividere tutto in tanti store minuscoli (cosa che può creare problemi di coordinamento tutti suoi), abbiamo scelto un approccio ibrido: store indipendenti per le questioni davvero indipendenti (ricerca e threading), tenendo però insieme in mailStore lo stato degli account e quello delle email, perché sono strettamente accoppiati. L'obiettivo è la chiarezza, non la purezza.

Che cosa ne ricava chi usa l'app

Un refactoring è per definizione invisibile a chi usa l'app — che deve sembrare esattamente la stessa. Ma gli effetti ci sono:

Cambio di account più rapido — l'architettura a due flussi fa sì che tu veda le tue email quasi subito quando cambi account, mentre la sincronizzazione con il server avviene in silenzio in background.

Basta intoppi di caricamento — il bug delle "email vuote" che poteva comparire cambiando account in fretta è sparito, insieme alle altre race condition del vecchio codice di caricamento sequenziale.

Sviluppo di nuove funzioni più rapido — è questo il punto principale. Un codice più pulito significa che le funzioni nuove si possono costruire e rilasciare più in fretta. La funzione "scarica tutto" in arrivo, i backup automatici pianificati e altri miglioramenti richiesti spesso arriveranno prima, perché il codice è più facile da maneggiare.

Stiamo costruendo MailVault perché duri. La posta elettronica è una tecnologia vecchia di decenni che non ha ricevuto molte attenzioni dagli strumenti moderni, e le soluzioni di backup esistenti sono o troppo tecniche o troppo goffe. Rendere il codice sostenibile è il modo in cui ci assicuriamo che l'app continui a migliorare — non solo questo mese, ma a lungo termine.