Refatorando o MailVault: do monólito aos módulos
O MailVault vem crescendo rápido. Nas últimas semanas lançamos atalhos de teclado, caixa de entrada unificada, modelos de e-mail, informações sobre remetentes, regras de limpeza automática, desfazer envio, mover para pasta e uma camada de transporte completa da Microsoft Graph API. Os recursos chegavam, as pessoas gostavam — e o código começava a gemer sob o peso.
O store central de estado — um único arquivo Zustand chamado mailStore.js — tinha passado de 4.300 linhas. Ele cuidava de tudo: gerenciamento de contas, carregamento de e-mails, cache, busca, encadeamento, ramificação para a API do Graph, sincronização delta com CONDSTORE, operações em lote e mais. O main.rs do backend em Rust ia pelo mesmo caminho. Componentes como SettingsPage e EmailViewer tinham inchado até virar paredões monolíticos de JSX. As coisas ainda funcionavam, mas acrescentar qualquer novidade era percorrer um labirinto.
Era hora de refatorar — não porque algo estava quebrado, mas porque a velocidade de entregar recursos novos começava a cair. Este post conta o que fizemos, por quê e para onde vamos.
O problema de ter um store gigante
O Zustand é ótimo para gerenciar estado. Mas, quando um único arquivo de store passa de alguns milhares de linhas, começam os problemas práticos: todo recurso mexe no mesmo arquivo, conflitos de merge viram rotina e fica difícil entender o que depende do quê. Nosso mailStore tinha a lógica de carregamento de e-mails entremeada com o código de transporte da API do Graph, o gerenciamento do cache LRU do lado do estado da busca e os algoritmos de encadeamento misturados com a troca de contas.
O mesmo padrão se repetia do lado do Rust. O main.rs continha os auxiliares de caminho do Maildir, as operações de chaveiro, o gerenciamento de cache, o tratamento de anexos, a lógica de notificações e utilitários de sistema — tudo em um arquivo só. No módulo IMAP, o pool de conexões estava emaranhado com as operações no nível do protocolo.
Carregamento de e-mails em dois fluxos
A maior mudança de arquitetura na v2.2.0 foi redesenhar como os e-mails carregam quando você troca de conta ou de pasta. A abordagem antiga tinha seis fases sobrepostas que rodavam em uma sequência frágil: checar o cache, mostrar os dados em cache, conectar ao IMAP, buscar cabeçalhos, mesclar os resultados, atualizar a interface. Se você trocasse de conta rápido, essas fases podiam se intercalar e você via por um instante e-mails da conta errada — o bug dos "e-mails vazios".
O novo desenho é um modelo limpo de dois fluxos. Quando você ativa uma conta, dois fluxos independentes disparam em paralelo:
O fluxo 1 (local) carrega do disco os cabeçalhos em cache e os e-mails salvos localmente. Isso é quase instantâneo — a interface se preenche em milissegundos usando o cache headers.json e um novo local-index.json que elimina a necessidade de interpretar o MIME de cada arquivo .eml no disco.
O fluxo 2 (servidor) conecta ao IMAP (ou à API do Graph, nas contas Microsoft), busca o estado atual do servidor e mescla as mudanças na visão local. E-mails novos aparecem, os apagados somem, as marcas de lido/não lido se atualizam — tudo sem piscar nem substituir o que já está na tela.
Os dois fluxos são envolvidos por um AbortController. Se você trocar de conta antes de qualquer um deles terminar, o trabalho em andamento é cancelado de forma limpa. Sem condições de corrida, sem dados velhos, sem "e-mails vazios".
Extração de módulos
A primeira fase da refatoração se concentrou em tirar dali o código que não tinha nada que fazer dentro do store nem dentro do main.rs.
Do lado JavaScript, extraímos:
Configuração do Graph — o mapeamento dos nomes de pasta do Microsoft Graph (Inbox → INBOX, Sent Items → Sent etc.) estava duplicado três vezes na base de código. Agora ele vive em um único módulo graphConfig.js.
Caches LRU — o cache de contas (o estado de e-mail de até 8 contas, para troca instantânea) e o cache de corpos (o conteúdo das mensagens, com limites de memória configuráveis) foram extraídos para um módulo de cache dedicado, com uma implementação de LRU compartilhada.
Busca — o estado e a lógica da busca estavam emaranhados no store principal, mesmo sendo bastante independentes. Tirá-los dali deixou tanto o store quanto o código da busca mais fáceis de acompanhar.
Rastreamento de desempenho — tínhamos chamadas de console.time improvisadas espalhadas pelo código de carregamento. Elas foram substituídas por um utilitário perfTrace estruturado, que dá para ligar e desligar, o que deixa a depuração de desempenho bem mais limpa.
Decomposição de componentes
Dois componentes React faziam coisa demais: SettingsPage e EmailViewer.
O SettingsPage renderizava todas as abas de configurações em um arquivo só — contas, aparência, atalhos de teclado, notificações, modelos, limpeza automática e mais. Nós o dividimos em componentes de aba individuais, cada um no subdiretório settings/. O componente pai agora só cuida da troca de abas e renderiza a aba ativa.
O EmailViewer tinha crescido a ponto de cuidar da exibição de um único e-mail, da visão de conversa, da renderização de anexos, dos selos de verificação DKIM, das informações sobre o remetente e das ações de responder/encaminhar — tudo em um componente. Extraímos ThreadView, EmailHeader e AttachmentBar como componentes focados no subdiretório email/.
O resultado são arquivos menores, mais fáceis de navegar, de testar e de alterar sem quebrar sem querer alguma coisa não relacionada.
O plano em cinco fases
O que saiu na v2.2.0 foi o começo, não o fim. Desenhamos um plano de refatoração em cinco fases que estamos executando aos poucos:
Fase 1: fundações compartilhadas — extrair as constantes e os utilitários compartilhados (configuração do Graph, constantes do Rust, rastreamento de desempenho). Feito.
Fase 2: backend em Rust — dividir o main.rs em módulos focados: operações de Maildir, acesso ao chaveiro, gerenciamento de cache, tratamento de anexos, notificações e utilitários de sistema. O módulo IMAP é separado entre as operações no nível do protocolo e o gerenciamento do pool de conexões.
Fase 3: decomposição do store — criar stores dedicados useSearchStore e useThreadStore e extrair o transporte da API do Graph, o gerenciamento de cache e o carregamento de e-mails para módulos de serviço. O objetivo: o mailStore encolher de 4.300 linhas para cerca de 2.000.
Fase 4: extração de componentes — continuar quebrando os componentes grandes. O AccountModal se divide em subcomponentes de detecção de servidor e de configuração do OAuth. Qualquer outro componente grande demais recebe o mesmo tratamento.
Fase 5: limpeza — remover código morto, consolidar utilitários duplicados (como o hasValidCredentials, que existia em vários lugares) e dividir o módulo utilitário do banco de dados em arquivos focados.
Refatorar sem regressão
A parte mais difícil de qualquer refatoração é garantir que nada quebre. O MailVault cuida do e-mail das pessoas — se a troca de contas começar a mostrar as mensagens erradas ou se uma corrida no carregamento voltar, isso é um problema de verdade.
Nossa abordagem: refatorar uma fronteira por vez, verificar se tudo continua funcionando e só então passar para a próxima. Os mais de 600 testes automatizados em quatro camadas nos dão uma rede de segurança, mas também fazemos testes manuais depois de cada fase — trocando de conta rapidamente, ficando sem rede no meio do carregamento, testando com a caixa postal de 17.000 e-mails, conferindo se as contas da API do Graph e as contas IMAP comuns se comportam direito.
Também tomamos uma decisão deliberada sobre a arquitetura do store. Em vez de dividir tudo em muitos stores minúsculos (o que pode criar seus próprios problemas de coordenação), escolhemos uma abordagem híbrida: stores independentes para assuntos de fato independentes (busca e encadeamento), mantendo juntos no mailStore o estado de contas e de e-mails, porque estão fortemente acoplados. O objetivo é clareza, não pureza.
O que quem usa ganha
Refatoração é invisível para quem usa, por definição — o aplicativo deve parecer exatamente o mesmo. Mas os efeitos são reais:
Troca de contas mais rápida — a arquitetura de dois fluxos faz seus e-mails aparecerem quase na hora quando você troca de conta, com a sincronização com o servidor acontecendo em silêncio, em segundo plano.
Fim das falhas de carregamento — o bug dos "e-mails vazios", que podia aparecer quando você trocava de conta rápido, acabou, junto com outras condições de corrida do antigo código de carregamento sequencial.
Desenvolvimento de recursos mais rápido — esse é o grande ponto. Uma base de código mais limpa significa que recursos novos podem ser construídos e lançados mais rápido. O recurso de "baixar tudo" que está por vir, os backups automáticos agendados e outras melhorias muito pedidas vão chegar antes, porque o código ficou mais fácil de mexer.
Estamos construindo o MailVault para durar. E-mail é uma tecnologia de décadas que não recebeu muito carinho das ferramentas modernas, e as soluções de backup existentes são técnicas demais ou desajeitadas demais. Manter o código sustentável é como garantimos que o aplicativo continue melhorando — não só neste mês, mas a longo prazo.