I'm currently building RiiMail, a desktop email client designed around a simple idea: all your email accounts in one place, with AI features built directly into the experience.
The project actually started from something much smaller. We built an AI phishing detection system that could analyse incoming emails and flag suspicious messages. It was released as a Gmail add-on, but after seeing how useful it was, we started asking a different question:
Why not build on it as an email client and have these capabilities built in?
Quick app initialization
When building a user-facing application, the app's initialisation speed matters, and users are unforgiving of whatever it takes to get that to happen. Yet it's one of the most important considerations during development because it influences almost every architectural decision.
Every design choice eventually comes back to a simple question:
How quickly can the user get into the app and start using it?
That became the priority guiding our tradeoffs, design, and approach to everything.
What does a typical email app do?
An email client sounds straightforward until you think about everything that needs to happen before the user sees their inbox.
At a minimum, it needs to:
- Authenticate a user to an email provider through IMAP
- Fetch emails (potentially hundreds of thousands of messages)
- Store those emails locally
- Build indexes that allow fast querying
- Render the UI without freezing
- Continue receiving and sending emails
The first five are expected to happen during onboarding. That's not an easy feat.
Now imagine a user with ten years of email history spread across multiple folders. The application needs to pull all of that data, process it, store it, and make it searchable, all while still feeling responsive.
The obvious solution is to wait until everything is finished before opening the application.
The problem is that users hate waiting.
So the real question becomes:
How do you make the app load as quickly as possible while still ensuring the data is available and properly indexed for querying?
The architecture
RiiMail is built using the Tauri framework with Rust as its backend.
I chose Rust not just because it's a language I'm comfortable with, but because of the control it gives when dealing with heavy application initialisation and concurrent workloads.
At the core of this is Tokio.
Before going deeper into that, it’s worth understanding how IMAP is handled.
We use the
async-imap crate, which
provides asynchronous IMAP support. At the centre of it is
the IMAP Session, which
represents an authenticated connection to the mail server.
The Session is mutable and stateful. You can make it
thread-safe by wrapping it in a
Mutex and an
Arc, but that comes with
tradeoffs. If not carefully designed, you can end up with
lock contention or retained locks that slow the entire
system down in subtle ways.
In fact, our initial design did exactly that.
So instead of sharing the session everywhere, we changed the model completely and made the background worker the sole owner of it.
Keeping the main thread light
The basic principle became simple:
Keep the main thread as light as possible
The IMAP session lives exclusively inside a background worker.
The frontend and command handlers never touch it directly. Instead, they send commands to the worker via channels, and the worker executes them asynchronously.
Most operations are fire-and-forget.
The only exception is authentication.
During login, the relationship is reversed: the main thread waits for the worker to complete authentication and report back before proceeding to application initialisation.
The biggest bottleneck: email synchronisation
Our main command is
FetchEmails, and it's by
far the heaviest operation in the system.
This is not just "fetching emails". It includes:
- Downloading messages from the mail provider
- Parsing raw IMAP data
- Transforming it into structured email objects
- Indexing for fast lookup
- Writing everything into SQLite
And this raises a harder question:
At what point do we consider the application ready?
Loading the first page, not the entire mailbox
Taking advantage of Tauri’s event system, we changed the definition of "ready".
The user doesn't need their entire mailbox immediately.
They only need enough data to start interacting with the application.
So instead of waiting for full synchronisation, we set a threshold: 100 emails.
Once the worker has fetched and persisted at least 100 emails, it emits a signal back to the frontend.
At that point, the frontend is released to start querying SQLite and populating its cache.
Meanwhile, synchronisation continues in the background.
This means a user can start reading and searching emails while thousands of additional messages are still being downloaded and indexed.
The mailbox is still syncing, but the app is already usable.
Real-time progress updates
We use the same Tauri event system to stream synchronisation progress to the frontend.
As emails are fetched and written into SQLite, the backend emits progress updates continuously.
The frontend subscribes to these events and updates the UI in real time.
This keeps the system decoupled:
- The worker focuses on ingestion and storage
- The frontend focuses on presentation
Neither needs to know how the other is implemented internally.
Final thoughts
What started as a simple goal, making the application open faster, ended up shaping almost every part of the backend architecture.
The solution wasn't making email synchronisation faster.
It was making it invisible from the user's perspective.
By moving expensive work into background tasks, streaming early usable data, and decoupling UI readiness from full synchronisation, we can run heavy operations concurrently without compromising responsiveness.
The user sees an application that opens quickly.
Behind the scenes, Rust, Tokio, IMAP workers, SQLite, and background tasks are doing a lot more work than they realise.