MailVault refaktorieren: vom Monolithen zu Modulen
MailVault ist schnell gewachsen. In den letzten Wochen haben wir Tastenkürzel, den gemeinsamen Posteingang, E-Mail-Vorlagen, Absender-Einblicke, automatische Bereinigungsregeln, Senden rückgängig, In Ordner verschieben und eine vollständige Transportschicht für die Microsoft Graph API ausgeliefert. Funktionen landeten, Nutzende waren zufrieden — und die Codebasis fing an, unter dem Gewicht zu ächzen.
Der zentrale State-Store — eine einzige Zustand-Datei namens mailStore.js — war auf über 4.300 Zeilen angewachsen. Sie kümmerte sich um alles: Kontoverwaltung, Laden von E-Mails, Caching, Suche, Threading, Verzweigung zur Graph-API, CONDSTORE-Delta-Sync, Massenoperationen und mehr. Die main.rs des Rust-Backends war auf demselben Weg. Komponenten wie SettingsPage und EmailViewer waren zu monolithischen JSX-Wänden aufgebläht. Es funktionierte alles noch, aber etwas Neues zu ergänzen hieß, sich durch ein Labyrinth zu bewegen.
Es war Zeit zu refaktorieren — nicht weil etwas kaputt war, sondern weil das Tempo, mit dem neue Funktionen entstanden, langsamer wurde. Dieser Beitrag beschreibt, was wir getan haben, warum, und wohin wir wollen.
Das Problem mit einem einzigen Riesen-Store
Zustand ist gut für die Zustandsverwaltung. Aber wenn eine einzelne Store-Datei über ein paar tausend Zeilen hinauswächst, fangen die praktischen Probleme an: jede Funktion fasst dieselbe Datei an, Merge-Konflikte werden zur Regel, und es ist schwer nachzuvollziehen, was wovon abhängt. In unserem mailStore war die Ladelogik für E-Mails mit dem Transportcode der Graph-API verwoben, die LRU-Cache-Verwaltung lag neben dem Suchzustand, und die Threading-Algorithmen steckten mitten im Kontowechsel.
Auf der Rust-Seite wiederholte sich dasselbe Muster. main.rs enthielt Maildir-Pfadhelfer, Keychain-Operationen, Cache-Verwaltung, Anhang-Handhabung, Benachrichtigungslogik und Systemwerkzeuge — alles in einer Datei. Im IMAP-Modul war das Connection-Pooling mit den Operationen auf Protokollebene verheddert.
E-Mail-Laden in zwei Strömen
Die größte architektonische Änderung in v2.2.0 war der Neuentwurf davon, wie E-Mails laden, wenn du Konto oder Ordner wechselst. Der alte Ansatz hatte sechs überlappende Phasen, die in einer fragilen Reihenfolge liefen: Cache prüfen, zwischengespeicherte Daten zeigen, mit IMAP verbinden, Header holen, Ergebnisse zusammenführen, Oberfläche aktualisieren. Wechselte man schnell das Konto, konnten sich diese Phasen verschränken, und kurz erschienen E-Mails des falschen Kontos — der „leere E-Mails“-Fehler.
Der neue Entwurf ist ein sauberes Zwei-Strom-Modell. Wenn du ein Konto aktivierst, starten zwei unabhängige Ströme parallel:
Strom 1 (lokal) lädt zwischengespeicherte Kopfzeilen und lokal gespeicherte E-Mails von der Festplatte. Das geht fast augenblicklich — die Oberfläche füllt sich binnen Millisekunden über den Cache headers.json und einen neuen local-index.json, der es überflüssig macht, jede .eml-Datei auf der Festplatte per MIME zu parsen.
Strom 2 (Server) verbindet sich mit IMAP (oder für Microsoft-Konten mit der Graph-API), holt den aktuellen Serverstand und führt alle Änderungen in die lokale Ansicht zusammen. Neue E-Mails erscheinen, gelöschte verschwinden, Gelesen-/Ungelesen-Markierungen aktualisieren sich — alles ohne Flackern und ohne das zu ersetzen, was schon auf dem Bildschirm steht.
Beide Ströme sind in einen AbortController eingepackt. Wenn du das Konto wechselst, bevor einer der Ströme fertig ist, wird die laufende Arbeit sauber abgebrochen. Keine Race Conditions, keine veralteten Daten, keine „leeren E-Mails“.
Module herauslösen
Die erste Phase des Refactorings zielte darauf, Code herauszulösen, der nichts im Store oder in der main.rs zu suchen hatte.
etwas zu suchen hatte. Auf der JavaScript-Seite haben wir herausgelöst:
Graph-Konfiguration — die Zuordnung der Microsoft-Graph-Ordnernamen (Inbox → INBOX, Sent Items → Sent usw.) war dreimal über den Code verteilt dupliziert. Sie liegt jetzt in einem einzigen Modul graphConfig.js.
LRU-Caches — der Kontencache (E-Mail-Zustand für bis zu 8 Konten, für sofortiges Umschalten) und der Text-Cache (E-Mail-Inhalte mit einstellbaren Speichergrenzen) wurden in ein eigenes Cache-Modul mit einer gemeinsamen LRU-Implementierung ausgelagert.
Suche — Suchzustand und Suchlogik steckten im Hauptstore verheddert, obwohl sie weitgehend unabhängig sind. Sie herauszuziehen machte sowohl den Store als auch den Suchcode leichter nachvollziehbar.
Performance-Tracing — wir hatten quer über den Ladecode verstreute console.time-Aufrufe. Sie wurden durch ein strukturiertes Werkzeug perfTrace ersetzt, das sich an- und abschalten lässt, was das Performance-Debugging deutlich sauberer macht.
Komponenten zerlegen
Zwei React-Komponenten taten viel zu viel: SettingsPage und EmailViewer.
SettingsPage renderte jede Einstellungs-Registerkarte in einer einzigen Datei — Konten, Erscheinungsbild, Tastenkürzel, Benachrichtigungen, Vorlagen, automatische Bereinigung und mehr. Wir haben sie in einzelne Registerkarten-Komponenten aufgeteilt, die jeweils in einem Unterverzeichnis settings/ liegen. Die übergeordnete Komponente kümmert sich jetzt nur noch um den Wechsel der Registerkarte und rendert die aktive.
EmailViewer war so weit gewachsen, dass er die Anzeige einzelner E-Mails, die Thread-Unterhaltungsansicht, das Rendern von Anhängen, DKIM-Prüfabzeichen, Absender-Einblicke sowie Antworten und Weiterleiten übernahm — alles in einer Komponente. Wir haben ThreadView, EmailHeader und AttachmentBar als fokussierte Komponenten in ein Unterverzeichnis email/ ausgelagert.
Das Ergebnis sind kleinere Dateien, in denen man sich leichter zurechtfindet, die leichter zu testen sind und die sich ändern lassen, ohne versehentlich etwas Unbeteiligtes zu brechen.
Der Fünf-Phasen-Plan
Was in v2.2.0 ausgeliefert wurde, war der Anfang, nicht das Ende. Wir haben einen Refactor-Plan in fünf Phasen entworfen, den wir schrittweise abarbeiten:
Phase 1: Gemeinsame Grundlagen — gemeinsame Konstanten und Hilfsfunktionen herauslösen (Graph-Konfiguration, Rust-Konstanten, Performance-Tracing). Erledigt.
Phase 2: Rust-Backend — main.rs in fokussierte Module aufteilen: Maildir-Operationen, Keychain-Zugriff, Cache-Verwaltung, Anhang-Handhabung, Benachrichtigungen und Systemwerkzeuge. Das IMAP-Modul wird in Operationen auf Protokollebene und die Verwaltung des Connection-Pools getrennt.
Phase 3: Zerlegung des Stores — eigene Stores useSearchStore und useThreadStore anlegen, den Transport der Graph-API, die Cache-Verwaltung und das Laden von E-Mails in Service-Module herauslösen. Das Ziel: mailStore schrumpft von 4.300 auf rund 2.000 Zeilen.
Phase 4: Herauslösen von Komponenten — große Komponenten weiter zerlegen. AccountModal wird in Unterkomponenten für Servererkennung und OAuth-Einrichtung aufgeteilt. Alle übrigen zu großen Komponenten bekommen dieselbe Behandlung.
Phase 5: Aufräumen — toten Code entfernen, doppelte Hilfsfunktionen zusammenführen (etwa hasValidCredentials, das an mehreren Stellen existierte) und das Datenbank-Hilfsmodul in fokussierte Dateien aufteilen.
Refaktorieren ohne Regression
Der schwerste Teil jedes Refactors ist sicherzustellen, dass nichts kaputtgeht. MailVault verwaltet die E-Mails von Menschen — wenn der Kontowechsel plötzlich die falschen Nachrichten zeigt oder eine Lade-Race-Condition zurückkommt, ist das ein echtes Problem.
Unser Ansatz: eine Grenze nach der anderen refaktorieren, prüfen, dass alles noch funktioniert, dann zur nächsten Grenze. Die über 600 automatisierten Tests auf vier Ebenen sind unser Sicherheitsnetz, aber wir testen nach jeder Phase auch von Hand — schnell die Konten wechseln, mitten im Laden offline gehen, mit dem Postfach mit 17.000 E-Mails testen, prüfen, dass sich Graph-API-Konten und gewöhnliche IMAP-Konten beide korrekt verhalten.
Außerdem haben wir bewusst über die Store-Architektur entschieden. Statt alles in viele winzige Stores aufzuteilen (was eigene Koordinationsprobleme schafft), haben wir einen hybriden Weg gewählt: eigenständige Stores für wirklich eigenständige Belange (Suche und Threading), während Konto- und E-Mail-Zustand zusammen im mailStore bleiben, weil sie eng gekoppelt sind. Das Ziel ist Klarheit, nicht Reinheit.
Was Nutzende davon haben
Refaktorieren ist für Nutzende absichtlich unsichtbar — die App soll genau gleich aussehen und sich gleich anfühlen. Die Wirkung ist aber real:
Schnellerer Kontowechsel — durch die Zwei-Strom-Architektur siehst du deine E-Mails beim Kontowechsel fast sofort, während die Serversynchronisierung still im Hintergrund läuft.
Keine Ladeaussetzer mehr — der Fehler mit den „leeren E-Mails“, der beim schnellen Kontowechsel auftreten konnte, ist weg, zusammen mit anderen Race Conditions im alten, sequenziellen Ladecode.
Schnellere Entwicklung neuer Funktionen — das ist der große Punkt. Aufgeräumter Code heißt, dass neue Funktionen schneller gebaut und ausgeliefert werden können. Die kommende Funktion „alles herunterladen“, geplante automatische Backups und andere häufig gewünschte Verbesserungen kommen früher, weil sich mit dem Code leichter arbeiten lässt.
Wir bauen MailVault, damit es hält. E-Mail ist eine jahrzehntealte Technik, die von modernem Werkzeug wenig Zuwendung bekommen hat, und die vorhandenen Backup-Lösungen sind entweder zu technisch oder zu klobig. Die Codebasis tragfähig zu halten ist unser Weg, dafür zu sorgen, dass die App weiter besser wird — nicht nur diesen Monat, sondern langfristig.