Skip to content
← All posts
StoryBuilding ConduitProduct

How hard is it to build an email client? The AI was the easy part

Sync, MIME, HTML rendering, OAuth verification, and a security assessment. What building an email client actually involves, from inside one.

Hard. Building an email client is hard in a way that is easy to misjudge from the outside. Not because of the AI, which is an API call with good context around it: the hard parts are sync, MIME, rendering a stranger’s HTML without getting owned, and the Google verification and independent security review you clear before you may touch anyone’s Gmail.

We build Conduit Mail, an AI-powered email client currently in early access, so this is a report from inside one rather than a survey of the field. The ranking below is ours.

The order surprised us. Nearly everything we wanted to build sat behind a wall of work that produces nothing a user can see. You cannot demo a sync engine. You can only demo the absence of one, and it demos badly.

Conduit Mail’s dark three-panel window: a folder and label sidebar on the left, the inbox list in the middle grouped into People, Notifications, Social, Newsletters, Promotions and Transactions, and the message “Q3 roadmap — final pass before Thursday” open in the reading pane on the right

The part everyone underestimates: sync

A mailbox is not a database you own. It is a large, mutating remote store held by someone else, and your job is to mirror it locally and keep it mirrored while the network, the tokens, and the user’s attention all fail independently.

The naive version is a loop: fetch messages, write them locally, repeat. It works beautifully on a test account with 400 messages. It falls over on a real one, for reasons that are all boring and all mandatory.

The mailbox is bigger than one session

A ten-year Gmail account is hundreds of thousands of messages. You cannot fetch that in one pass, and the user will close the tab in the middle of whichever pass you picked. Backfill has to be resumable: a durable position, written often enough that a reload, a token refresh, or a laptop lid closing at message 61,000 costs you seconds instead of hours. Conduit runs a full-mailbox backfill and then polls each connected account every 60 seconds for what changed since.

Change is a stream with a cursor, not a list

Gmail hands you a history feed — users.history.list — that reports what changed since a history ID you hold. It is the right primitive, and it comes with a rule that shapes the entire design: the history a cursor points into expires. Come back late enough and the server tells you your position no longer exists.

So every sync engine needs two modes, incremental and full, plus the logic to notice it has fallen off the edge and restart cleanly without duplicating a decade of mail. That third piece is where the bugs live. It is also, by definition, the code path you exercise least during development and most in production.

IMAP has the same problem with worse tools

Conduit also speaks IMAP and SMTP — Yahoo, AOL, iCloud, Fastmail, custom servers — and an IMAP sync engine gets none of Gmail’s conveniences. There is no history feed, so you diff UIDs against the ones you already hold. UIDVALIDITY can change on the server, and when it does every local UID is invalid at once: the same fall-off-the-edge case, arriving without warning. Staying current means holding an IDLE connection open per folder or polling on a timer, and flags drift constantly, because a flag is the cheapest thing in a mailbox to change.

Everything is a merge

Mail changes in places your app is not. Something gets read on a phone, archived in the Gmail web UI, or relabeled by a server-side filter at 3am. Your local copy is a cache with opinions, and every field on every message is a potential conflict. Deciding what wins is not a hard algorithm; remembering to decide it for each field, in each direction, is the work.

One writer, or you get duplicates

Two tabs syncing the same account at the same time will happily insert the same message twice. Conduit’s sync engine is a singleton for exactly that reason, which sounds like a footnote and is actually a load-bearing constraint on how the rest of the client is allowed to ask for data.

None of this is intellectually thrilling. All of it has to be right before the first message renders, and getting it wrong produces the worst failure an email client has: a message that exists on the server and not in your app. Missing mail looks exactly like no mail.

MIME, or why one email is fifteen files

An email is not a string. RFC 5322 defines the headers, and RFC 2045 and RFC 2046 define MIME, which turns one message into a tree.

A routine marketing email arrives as multipart/mixed wrapping a multipart/related wrapping a multipart/alternative holding a plain-text body and an HTML body, plus eight inline images, a PDF and a calendar invite. Fifteen parts is not an unusual message. It is a Tuesday.

Each of those parts brings its own small problem:

  • Encoding. Bodies arrive base64 or quoted-printable, and quoted-printable soft line breaks will eat characters if you decode in the wrong order.
  • Charsets that lie. A part is labeled us-ascii and contains a smart quote from Word. Trusting the label produces mojibake; ignoring the label produces different mojibake.
  • Filenames. Non-ASCII attachment names are encoded per RFC 2047 or RFC 2231, and senders in the wild use both, sometimes in the same message.
  • Inline images. HTML bodies reference embedded images by cid: URL, which you have to resolve against the Content-ID headers of sibling parts before anything renders. Conduit does this resolution so inline images appear in place, and handles attachments end to end — compose, display, and download.
  • Choosing a body. multipart/alternative means “here are two versions, pick one.” Picking wrong means the user reads a plain-text skeleton of a message that had a design.

The specs are clear enough. The problem is that thirty years of clients have implemented them with individual creativity, and you receive the output of all of them. A parser that handles the RFCs correctly still has to handle a 2009 Python script that emits a header line with no space after the colon, because that message is in someone’s inbox and they expect to read it.

This post is all inbound. Sending — SMTP, threading a reply so every other client agrees it is a reply, and reconciling what you sent with what the provider says you sent — is its own post.

Rendering a stranger’s HTML without getting owned

Email HTML is arbitrary markup, authored by parties you have not vetted, that you are obliged to display. You are running untrusted code from anyone who knows your address, on demand, as a feature.

Ours, stated plainly:

Email renders in a locked-down frame with scripts disabled and every tag sanitized.

Concretely: a sandboxed iframe, scripts disabled, and every message passed through DOMPurify before it goes anywhere near the DOM. On top of that:

A strict Content Security Policy blocks inline scripts and any origin we didn’t allow.

Three layers, because one of them will eventually have a bad day, and the blast radius of a sanitizer bypass in an email client is every message the user has ever received. Sanitizer bypasses are a live research area, not a solved problem, and an email client is the single best place on the internet to deliver one.

Then, having made it safe, you discover the second half of the problem: email HTML is also terrible. Nested tables from 2003. Fixed pixel widths that assume a 900px column.

Inline styles hardcode black text on a white background, then arrive in a dark interface where that renders as an invisible message or a glaring white rectangle. There is no correct answer here, only tradeoffs you pick and then defend — and every choice you make about legibility is a choice about how far you are willing to modify what the sender wrote.

We cover the day-to-day side of all this in what Conduit actually does with a Gmail account.

OAuth verification and a Tier 2 security assessment

Here is the part that stops most people, and it is not code.

Reading someone’s Gmail requires restricted scopes. Google gates those behind a verification process and an independent security assessment, because a restricted scope hands your application the contents of a stranger’s mailbox. Until you clear it, your OAuth client stays in test mode: you and up to 100 test users, and nobody else.

The assessment is CASA — Cloud Application Security Assessment. Tier 2 means an independent, Google-authorized lab reviews the application against it, rather than you self-attesting. Conduit’s was carried out by TAC Security, and restricted-scope access was granted on 31 July 2026, after a review that ran in rounds rather than in a single pass.

What the review actually asks about is architecture, not features. How are tokens stored. What is encrypted, and with what. Who can reach the database. What the content security policy allows. What happens to the data after you have it.

Answering well means the answers have to already be true, so the honest sequence is: build it correctly, then get reviewed. We audited internally against the OWASP ASVS Level 2 checklist first, and the assessment came after.

The design decisions that came out of it are on our security page in full, but the load-bearing ones:

Your tokens and mail-server passwords are encrypted with AES-256-GCM before they touch our database. Everything in transit is TLS.

Our database refuses direct browser access — every read and write goes through authenticated server code.

And the structural one, which is a privacy decision before it is a security decision:

Mail passes through our servers on its way to you; it isn’t stored there. Our database has no table for your mailbox. The exception is email a connected AI app asks us to send, which we hold briefly, with its contents encrypted.

Why there are so few email clients

You can build a working mail client against your own account in a weekend. You cannot let anyone else use it.

The gate between those two states is not engineering time. It is a review that runs on someone else’s queue, in rounds, on their schedule, and no amount of effort on your side moves it faster. That gate, more than the code, is why there are so few email clients.

What AI made easier, and what it didn’t

The features people assume are the hard part are the cheap part. Summaries, drafts in your voice, natural-language search that resolves a sentence into filter chips, sorting new mail into six categories: People, Notifications, Social, Newsletters, Promotions, Transactions. In engineering terms each of these is a well-shaped request with good context and a defined place to put the answer.

Writing the client went faster with AI in the loop. We will not attach a multiplier to that, because the parts it accelerated were never the parts that were slow.

What it did not help with:

Correctness under partial failure. No model is going to tell you that your history cursor expired while the user was on a plane, or that your merge logic drops a flag when two devices disagree. That is your test suite and your production traces.

Anything you have to be certain about. Sanitization, token storage, the boundary between browser and database: these get read line by line by a person, because a confident, plausible, subtly wrong answer is the specific failure mode you cannot afford there.

Provider review. No amount of AI shortens Google verification.

And the AI features carry their own hard part, which is not the model — it is the plumbing and the honesty. Categorization is the clearest example. Mail gets sorted into those six categories, and the results are inconsistent enough that an overhaul is queued. We would rather write that down than claim an accuracy number we do not have.

The privacy design around it took longer than the classifier. Everything AI does waits for you to ask, except categorization: a local classifier on your device does the sorting, and on paid plans the messages it is unsure about, plus every message it files under People, get a second look, with the sender, subject, a short preview, a few mailing-list headers and a category hint going to TypeSafe’s Jev model, never the body. On the free plan it stays entirely on device and sends nothing. What we keep on our servers for AI is short by design — your rules, your notes, and short writing samples from sent mail so drafts sound like you on every device — all of it visible in Settings and all of it yours to turn off or delete.

Your email is never used to train AI models. Not by us, not by Anthropic — their commercial API terms forbid it — and not by TypeSafe, the second AI provider that sorts your mail, which keeps none of it. Google’s restricted-scope rules bind us to it in writing.

What we would do differently

Build sync as if it will be interrupted, from the first commit. Resumability added later is not a patch, it is a rewrite of the thing every other subsystem already depends on.

Treat rendering as a security boundary before it is a design problem. Frame, sanitizer, and CSP first; typography second. Doing it the other way means re-testing every visual decision after you add the constraints.

Start the provider paperwork earlier than feels reasonable. Verification is calendar time, not engineering time. Work you can parallelize; a review queue you cannot.

Test the boring layer hardest. The parser and the sync engine deserve more test surface than the features, because their failures are silent and everyone else’s are loud.

Do not confuse “the AI works” with “the product works.” A summary of a message you failed to fetch is worth nothing.

If you want the origin story rather than the engineering one, it is in why we built Conduit Mail. If you are wondering about native apps, the current status of desktop and mobile lives on our download page, which we keep accurate rather than optimistic.

Conduit Mail is in early access at in.conduitmail.app. If you want the rest of the answers the CASA review asked for — what is encrypted, where it lives, what the browser can reach — they are on our security page.

Frequently asked questions

FAQ

Why are there so few email clients?

Because the boring parts are enormous. Sync that survives interruptions, MIME parsing, rendering arbitrary HTML safely, and provider access review all have to be finished before anyone can read a single message. None of it is a feature you can demo, and all of it has to work before the parts you wanted to build matter at all.

What is the hardest part of building an email client?

Sync. A mailbox is a large, mutating remote store you have to mirror locally without losing or duplicating anything, across interruptions, expired tokens and cursors that go stale. Rendering a stranger’s HTML safely comes second: a sandboxed iframe with scripts disabled, sanitizing every message, and a strict content security policy.

What is CASA Tier 2 verification?

CASA is Google’s Cloud Application Security Assessment, required for apps requesting restricted Gmail scopes. Tier 2 means an independent lab reviews the application against that assessment. Conduit’s was carried out by TAC Security, a Google-authorized lab, and restricted-scope access was granted on 31 July 2026. It is not a form you fill in: the answers about token storage, encryption and database access have to already be true.

Why does Google restrict Gmail API access?

Because restricted scopes hand an application the contents of someone’s mailbox. Google gates them behind a verification process and an independent security assessment, and limits what a verified app may do with the data afterwards. Clearing it is calendar time on someone else’s review queue, not engineering time, and it is the reason a weekend mail client never reaches real Gmail users.