Aller au contenu
Retour au blog

Refactoriser MailVault : du monolithe aux modules

7 min de lecture

MailVault grandit vite. Ces dernières semaines, nous avons livré les raccourcis clavier, la boîte de réception unifiée, les modèles d'e-mail, les aperçus d'expéditeur, les règles de nettoyage automatique, l'annulation d'envoi, le déplacement vers un dossier et une couche de transport Microsoft Graph API complète. Les fonctionnalités arrivaient, les utilisateurs étaient contents — et le code commençait à grincer sous le poids.

Le store d'état central — un unique fichier Zustand nommé mailStore.js — dépassait les 4 300 lignes. Il gérait tout : les comptes, le chargement des e-mails, la mise en cache, la recherche, les fils de discussion, l'aiguillage vers l'API Graph, la synchronisation delta CONDSTORE, les opérations en masse, et le reste. Le main.rs du backend Rust suivait la même trajectoire. Des composants comme SettingsPage et EmailViewer avaient enflé en murs de JSX monolithiques. Tout fonctionnait encore, mais ajouter la moindre nouveauté revenait à traverser un labyrinthe.

Il était temps de refactoriser — pas parce que quelque chose était cassé, mais parce que le rythme de livraison des nouveautés commençait à ralentir. Ce billet raconte ce que nous avons fait, pourquoi, et où nous allons.

Le problème d'un store géant

Zustand est excellent pour la gestion d'état. Mais quand un seul fichier de store dépasse quelques milliers de lignes, les problèmes pratiques arrivent : chaque fonctionnalité touche au même fichier, les conflits de fusion deviennent la routine, et il devient difficile de savoir ce qui dépend de quoi. Notre mailStore entremêlait la logique de chargement des e-mails et le code de transport de l'API Graph, la gestion du cache LRU voisinait avec l'état de la recherche, et les algorithmes de regroupement en fils de discussion étaient mêlés au changement de compte.

Le même schéma se répétait côté Rust. main.rs contenait les utilitaires de chemins Maildir, les opérations de trousseau, la gestion du cache, le traitement des pièces jointes, la logique de notification et les utilitaires système — le tout dans un seul fichier. Dans le module IMAP, la mise en commun des connexions était emmêlée avec les opérations au niveau du protocole.

Le chargement des e-mails à deux flux

Le plus gros changement d'architecture de la v2.2.0 a été de repenser la façon dont les e-mails se chargent quand vous changez de compte ou de dossier. L'ancienne approche avait six phases qui se chevauchaient dans une séquence fragile : vérifier le cache, afficher les données en cache, se connecter en IMAP, récupérer les en-têtes, fusionner les résultats, mettre à jour l'interface. Si vous changiez de compte rapidement, ces phases pouvaient s'entrelacer et des e-mails du mauvais compte apparaissaient brièvement — le bug des « e-mails vides ».

Le nouveau modèle est un modèle propre à deux flux. Quand vous activez un compte, deux flux indépendants partent en parallèle :

Le flux 1 (local) charge depuis le disque les en-têtes en cache et les e-mails enregistrés localement. C'est quasi instantané — l'interface se remplit en quelques millisecondes grâce au cache headers.json et à un nouveau local-index.json qui évite d'avoir à analyser le MIME de chaque fichier .eml présent sur le disque.

Le flux 2 (serveur) se connecte en IMAP (ou à l'API Graph pour les comptes Microsoft), récupère l'état actuel du serveur et fusionne les changements dans la vue locale. Les nouveaux e-mails apparaissent, ceux qui ont été supprimés disparaissent, les indicateurs lu/non lu se mettent à jour — le tout sans scintillement et sans remplacer ce qui est déjà à l'écran.

Les deux flux sont encapsulés dans un AbortController. Si vous changez de compte avant qu'ils aient terminé, le travail en cours est annulé proprement. Pas de situation de concurrence, pas de données périmées, pas d'« e-mails vides ».

L'extraction de modules

La première phase du refactoring a consisté à sortir le code qui n'avait rien à faire dans le store ni dans main.rs.

Côté JavaScript, nous avons extrait :

Configuration Graph — la correspondance des noms de dossiers Microsoft Graph (Inbox → INBOX, Sent Items → Sent, etc.) était dupliquée à trois endroits du code. Elle vit désormais dans un unique module graphConfig.js.

Caches LRU — le cache de comptes (jusqu'à l'état e-mail de 8 comptes, pour un changement instantané) et le cache de corps de message (le contenu des e-mails, avec des limites mémoire configurables) ont été extraits dans un module de cache dédié, avec une implémentation LRU partagée.

Recherche — l'état et la logique de recherche étaient emmêlés dans le store principal alors qu'ils en sont largement indépendants. Les en sortir a rendu le store comme le code de recherche plus faciles à suivre.

Traçage des performances — nous avions des appels console.time disséminés au petit bonheur dans le code de chargement. Ils ont été remplacés par un utilitaire perfTrace structuré, que l'on peut activer et désactiver, ce qui rend le débogage des performances bien plus propre.

La décomposition des composants

Deux composants React en faisaient beaucoup trop : SettingsPage et EmailViewer.

SettingsPage affichait tous les onglets de réglages dans un seul fichier — comptes, apparence, raccourcis clavier, notifications, modèles, nettoyage automatique, et le reste. Nous l'avons découpé en composants d'onglet distincts, chacun dans un sous-répertoire settings/. Le composant parent ne fait plus que gérer le changement d'onglet et afficher l'onglet actif.

EmailViewer en était venu à gérer l'affichage d'un e-mail seul, la vue conversation d'un fil, le rendu des pièces jointes, les badges de vérification DKIM, les aperçus d'expéditeur et les actions répondre/transférer — le tout dans un seul composant. Nous en avons extrait ThreadView, EmailHeader et AttachmentBar, des composants ciblés placés dans un sous-répertoire email/.

Le résultat, ce sont des fichiers plus petits, plus faciles à parcourir, à tester et à modifier sans casser par accident quelque chose de sans rapport.

Le plan en cinq phases

Ce qui a été livré en v2.2.0 est le début, pas la fin. Nous avons conçu un plan de refactoring en cinq phases que nous exécutons progressivement :

Phase 1 : fondations partagées — extraire les constantes et utilitaires partagés (configuration Graph, constantes Rust, traçage des performances). Fait.

Phase 2 : backend Rust — découper main.rs en modules ciblés : opérations Maildir, accès au trousseau, gestion du cache, traitement des pièces jointes, notifications et utilitaires système. Le module IMAP est séparé entre les opérations au niveau du protocole et la gestion du pool de connexions.

Phase 3 : décomposition du store — créer des stores useSearchStore et useThreadStore dédiés, extraire le transport de l'API Graph, la gestion du cache et le chargement des e-mails dans des modules de service. Objectif : faire passer mailStore de 4 300 lignes à environ 2 000.

Phase 4 : extraction de composants — poursuivre le découpage des gros composants. AccountModal se scinde en sous-composants de détection de serveur et de configuration OAuth. Tous les composants encore surdimensionnés subiront le même traitement.

Phase 5 : nettoyage — supprimer le code mort, regrouper les utilitaires en double (comme hasValidCredentials, qui existait à plusieurs endroits) et découper le module utilitaire de base de données en fichiers ciblés.

Refactoriser sans régression

Le plus dur dans un refactoring, c'est de s'assurer que rien ne casse. MailVault manipule le courrier de gens — si le changement de compte se met à montrer les mauvais messages ou si une condition de course au chargement réapparaît, c'est un vrai problème.

Notre approche : refactoriser une frontière à la fois, vérifier que tout fonctionne encore, puis passer à la suivante. Les plus de 600 tests automatisés répartis sur quatre couches nous servent de filet, mais nous testons aussi à la main après chaque phase — changements de compte rapides, passage hors ligne en plein chargement, essais avec la boîte de 17 000 e-mails, vérification que les comptes API Graph et les comptes IMAP classiques se comportent tous deux correctement.

Nous avons aussi pris une décision délibérée sur l'architecture du store. Plutôt que de tout éclater en une multitude de petits stores, ce qui crée ses propres problèmes de coordination, nous avons choisi une approche hybride : des stores indépendants pour les préoccupations réellement indépendantes (la recherche et les fils de discussion), en gardant ensemble l'état des comptes et celui des e-mails dans mailStore, parce qu'ils sont étroitement liés. L'objectif est la clarté, pas la pureté.

Ce que les utilisateurs y gagnent

Un refactoring est invisible pour les utilisateurs, par construction — l'application doit avoir exactement la même allure. Mais les effets sont réels :

Changement de compte plus rapide — grâce à l'architecture à deux flux, vos e-mails s'affichent presque instantanément quand vous changez de compte, la synchronisation avec le serveur se faisant sans bruit en arrière-plan.

Fini les à-coups au chargement — le bug des « e-mails vides » qui pouvait apparaître en changeant de compte rapidement a disparu, ainsi que les autres situations de concurrence de l'ancien code de chargement séquentiel.

Développement des fonctionnalités plus rapide — c'est le point principal. Un code plus propre veut dire des fonctionnalités conçues et livrées plus vite. La fonction « tout télécharger » à venir, les sauvegardes automatiques programmées et d'autres améliorations souvent demandées arriveront plus tôt, parce que le code est plus facile à manipuler.

Nous construisons MailVault pour durer. L'e-mail est une technologie vieille de plusieurs décennies, à laquelle l'outillage moderne s'est peu intéressé, et les solutions de sauvegarde existantes sont soit trop techniques, soit trop lourdes. Rendre le code soutenable, c'est notre façon de garantir que l'application continue de s'améliorer — pas seulement ce mois-ci, mais sur la durée.