Reading a mailbox is easy. Every provider has an API, the docs are good, and you can list a user's inbox in an afternoon. That is the part everyone estimates.
What nobody estimates is that "mail" is three different products wearing the same word, and that the gap between them does not close — it widens every time one of the three ships something.
Three models, not three endpoints
Gmail is a REST API over labels. A message belongs to many labels at once,
threads are a first-class object with their own id, and you track change with a
monotonic historyId. Ask for everything since a cursor and you get a list of
what changed.
Microsoft Graph is a different shape. Folders, not labels, and a message lives in exactly one. Conversations exist but are not Gmail's threads. Change tracking is a delta token per folder, so "what changed in this mailbox" is a question you ask once per folder and reassemble yourself.
IMAP is a protocol from 1986 and has neither. No thread id, no change feed, no notion of a label. What it has is a per-folder sequence of UIDs and a server that may, at any point, tell you those UIDs are no longer valid.
A feature written against one of these does not run against the other two. It runs against the one you wrote it for, and the other two get a branch, and then the branches get their own bugs.
The cursor that invalidates itself
IMAP folders carry a UIDVALIDITY value. If it changes, every UID you have
stored is meaningless and your cursor points at nothing. It changes for
ordinary reasons — a server migration, a folder rename on some implementations,
a restore from backup.
The naive handling is to notice the mismatch and resynchronise from scratch. That is correct and it is also a full re-download of the mailbox, which for a customer with fifteen years of mail is hours of work, a large bill, and a product that looks broken while it runs.
The handling you actually want is to detect the invalidation, keep what you already hold, and reconcile against it by message id rather than by position. That is a day of thinking and a lot of tests, and it is invisible until the first time it fires in production.
Threading you have to compute
Gmail hands you a thread id. Graph hands you a conversation id that does not
mean quite the same thing. IMAP hands you nothing, and you are left with
Message-ID, In-Reply-To and References headers that clients populate with
varying enthusiasm.
So you build a threading algorithm. Then you discover that your algorithm and Gmail's disagree about a particular forwarded chain, and a customer notices that the same conversation looks different depending on which account it came from — which is precisely the thing a unified API is supposed to prevent.
The answer is to compute threading yourself everywhere, including where the provider offers it, so that one implementation defines what a thread is.
Sockets that have to stay open
Near-real-time mail on IMAP means IDLE: a connection held open per folder
per account, waiting for the server to say something happened. One socket per
mailbox, re-established on every drop, with a watchdog because some servers
stop talking without closing.
This is the constraint that shapes your infrastructure rather than your code. A process holding thousands of long-lived sockets cannot scale to zero, cannot be a request-scoped function, and cannot be spread across instances without deciding which instance owns which mailbox. Most serverless platforms are the wrong shape for it, and you find that out after you have chosen one.
Quotas you must hold, not retry
Every provider meters something: a daily send count, a recipient ceiling, per-mailbox throttling, a connection limit. The instinct when you hit one is to retry with backoff.
The instinct is wrong. A retry against a provider that has just told you to stop is how an account goes from throttled to restricted. The correct behaviour is to track what each account has spent, hold the work before it crosses the line, and tell the caller what is left so their scheduler can plan instead of discovering.
That means a budget per account per window, persisted, and a queue that honours it — which is a different piece of software from the one that sends mail.
None of this is the feature
A mail client is a few weeks of work. The sync layer under it is most of a year, and then roughly half a person permanently, because the providers keep changing and the long tail of IMAP servers never stops surprising you.
That layer is what Aux0 is. One thread model across Gmail, Outlook, iCloud, Fastmail, Zoho and anything speaking IMAP, with the cursor recovery, the threading, the connection pooling and the quota pacing already built — so the thing you write is the client.