How We Test MailVault: 600+ Tests, Real Emails, Real Servers
Email is one of those things that looks simple on the surface and is anything but. IMAP servers behave differently. MIME encoding has edge cases that predate most developers' careers. OAuth2 tokens expire at inconvenient times. Attachments can be nested three levels deep in multipart messages. If you're building an email client, you either test obsessively or you ship bugs.
MailVault currently has over 600 automated tests spread across four layers: Rust unit tests, JavaScript unit tests, integration tests against real IMAP servers, and end-to-end tests that drive the actual desktop app. Here's how it all fits together.
Layer 1: Rust Unit Tests (55 tests)
The core of MailVault is written in Rust — all IMAP connections, SMTP sending, MIME parsing, and OAuth2 token management run natively, not through a Node.js sidecar. The Rust layer has 55 unit tests covering the functions that are hardest to get right.
A good example is UID range compression. When MailVault needs to fetch headers for UIDs [1, 2, 3, 5, 6, 10], it compresses them into the IMAP range string "1:3,5:6,10" to minimize command size. The tests cover empty inputs, single UIDs, consecutive ranges, gaps, unsorted inputs, duplicates, large ranges, and the inverse operation (expanding ranges back to individual UIDs). There are also roundtrip tests that verify compress → expand returns the original set. These matter because chunked UID FETCH commands break if the ranges are wrong.
The Microsoft Graph API module has its own tests for parsing Graph JSON responses into MailVault's internal EmailHeader format, and the main module tests cover Maildir filename parsing, flag encoding, and file lookup by UID.
Layer 2: JavaScript Unit Tests (13 test suites)
The frontend has 13 dedicated unit test suites running on Vitest, plus additional store-level tests. These cover the logic that sits between the Rust backend and the React UI:
Email parsing is the most heavily tested area. The emailParser suite exercises correspondent extraction (parsing "John Doe <[email protected]>" into name and address), subject normalization (stripping all combinations of Re:, Fwd:, FW:, Re[N]:), signature detection, and quote-level identification. The threading algorithm — a simplified version of Jamie Zawinski's threading spec — has tests for In-Reply-To chains, References header walking, subject-based fallback grouping, and cycle detection for malformed headers.
Other suites test the background pipeline (concurrency control, retry with exponential backoff, cascade ordering between active and background accounts), OAuth2 token handling (proactive refresh before expiry, deduplication of concurrent refresh attempts), delta sync optimizations (the four-tier CONDSTORE decision tree), quick-load flow (cached headers shown instantly, background sync filling in changes), resilient loading (graceful degradation when IMAP connections drop mid-fetch), and state transitions across the Zustand store.
The store tests themselves verify LRU cache eviction (MailVault caches email bodies in memory with a configurable limit), field stripping (removing rawSource and attachment content before caching to save memory), and cache size estimation.
Layer 3: Integration Tests Against Real IMAP Servers
Unit tests with mocks can only tell you so much. MailVault's integration tests connect to real IMAP servers with real credentials and send real emails. We maintain two dedicated test accounts on a private domain, and every integration test run talks to those accounts over TLS.
The core email.test.js suite runs a full cycle: connect via IMAP, send an email via SMTP (using Nodemailer), wait for it to arrive, fetch and parse it with mailparser, verify the headers and body, then delete the test message during cleanup. This catches real-world issues that mocks miss — TLS negotiation quirks, server-specific IMAP extensions, encoding differences.
The IMAP optimizations suite tests MailVault's advanced protocol features against the live server: COMPRESS=DEFLATE negotiation (does the server actually support it? does the compressed session work?), CONDSTORE delta sync (does CHANGEDSINCE return the right changed flags?), and ESEARCH compact responses.
The cross-account email suite tests multi-account scenarios: sending from account A to account B, verifying the message appears in B's inbox, testing read/unread status propagation, and checking that the threading algorithm correctly links the conversation across both accounts.
The 500-Email Seed
To test realistically, we seed our test mailboxes with 500 emails — 250 in each direction between the two accounts. The seeder script generates emails with realistic subjects ("Q4 Revenue Report — Final Numbers", "Re: Bug in checkout flow", "Invoice #4821 — Due January 15") and cycles through five MIME types: plain text, HTML, multipart/alternative, HTML with a single attachment, and HTML with multiple attachments (CSV, JSON, and PNG).
About a third of the seeded emails are marked as unread. Attachments include generated PDF reports, CSV data files, JSON metadata, and PNG images. This gives us a mailbox that looks and behaves like a real inbox — mixed content types, varied read states, threaded conversations, and enough volume to surface performance issues.
Stress Testing
The mailbox-stress.test.js suite goes further: it injects 500 emails into a single mailbox (cycling through all five MIME types), then tests that MailVault's IMAP layer can handle the volume correctly. This catches issues with chunked UID FETCH (we split fetches into batches of 200 to avoid IMAP command-length limits), pagination, and memory usage under load.
Layer 4: End-to-End Tests in the Real App
The final layer drives the actual Tauri desktop app using WebdriverIO with the Tauri WebDriver. The app is compiled with a webdriver feature flag, launched as a real desktop window, and the tests interact with it the same way a user would — clicking buttons, typing in fields, waiting for emails to load.
We split E2E tests into two suites. The UI suite (6 test files) tests everything that doesn't need an IMAP connection: settings panels, sidebar interactions, keyboard shortcuts, layout and view switching, and compose templates. These run fast because they don't touch the network.
The connected suite (11 test files) tests the full app with real accounts — including one with over 17,000 emails. That's not a synthetic benchmark; it's a real mailbox with years of accumulated messages, mixed MIME types, nested threads, and all the messy edge cases that come with long-lived email accounts. Running E2E tests against a mailbox of that size keeps us honest about pagination, memory usage, and perceived performance at scale. These scenarios include:
Cold start — the app launches with no cache, adds an account, and measures how long it takes for emails to appear. This is a benchmark we track: first emails visible from a completely cold state.
Account management — adding accounts, switching between them, verifying that per-account state (last selected mailbox, cached email list) persists correctly.
Email list and viewer — scrolling through the email list, selecting emails, verifying that bodies load correctly (from cache, from Maildir, or from IMAP as fallback), checking that adjacent emails are pre-fetched.
Compose, reply, and forward — opening the compose modal, replying to an email (checking that the quoted body and recipients are correct), forwarding with attachments preserved.
Search — testing the search bar against the seeded mailbox, verifying result accuracy and performance.
Folder operations — moving emails between folders, archiving, deleting (which uses RFC 4315 UID EXPUNGE to only remove the targeted message).
Unified inbox — testing the multi-account unified view that merges emails from all accounts into a single chronological list.
Performance benchmarks — cold-start timing, email loading latency, threading performance. These output a summary table with p50, p95, and max timings so we can catch regressions.
Every connected E2E test has a safety mechanism: a whitelist of approved test accounts. If a test accidentally tries to modify an account that isn't in the whitelist, it fails immediately. This prevents test code from ever touching real user data during development.
Why This Matters
MailVault handles your email. That's personal, important data. When you add an account, switch folders, archive a message, or search your inbox, it has to work — not just in the happy path, but when the server is slow, when the connection drops, when an email has malformed headers, when an OAuth2 token expires mid-fetch.
The four-layer approach gives us confidence at every level: Rust tests verify the low-level primitives, JS unit tests verify the application logic, integration tests verify that real servers behave the way we expect, and E2E tests verify that the whole thing works as a desktop app. When we ship a release, all of these have passed.