Microsoft Graph mail
Microsoft Graph is the HTTP API for Microsoft 365, and mail is one part of it. A mailbox is reached through /me/messages or /users/{id}/mailFolders/..., incremental change comes from delta queries, and push comes from change notification subscriptions that expire and must be renewed.
Folders, not labels — and one message in one place#
Graph keeps the folder model that IMAP has, which makes it the easier of the two big provider APIs to port an IMAP client onto. A message lives in exactly one folder, moving it is a real move, and well-known folder names — inbox, sentitems, drafts, deleteditems — can be used in place of identifiers.
| Concern | Graph | Gmail API | IMAP |
|---|---|---|---|
| Message location | One folder | Many labels | One folder |
| Incremental change | Delta query with a delta token | History ID | CONDSTORE MODSEQ, if offered |
| Push | Webhook subscription, renewable, expires | Watch with a topic, expires | IDLE on a held connection |
| Conversation | conversationId | threadId | Reconstructed from headers |
| Identifiers | Opaque strings, can change on move | Stable message ids | UID within a UIDVALIDITY |
| Read state | isRead boolean property | UNREAD label | \Seen flag |
Delta queries and expiring subscriptions#
A delta query returns a page of changes and a token; the token is passed back next time to get what changed since. The pattern is the same shape as a Gmail history ID and fails in the same way — a token that is too old is rejected, and the recovery is a fresh full query rather than a retry.
@odata.nextLinkpaginates within one delta run. Follow it to the end before treating the run as complete.@odata.deltaLinkis the token to store for next time. Store it only after the whole run has been processed, for the same reason an IMAP cursor is stored last.- Change notifications expire. A subscription has a maximum lifetime measured in days for mail, and a client that does not renew simply stops receiving notifications. Nothing errors.
- Notifications are hints, not payloads. The usual pattern is to be told something changed and then fetch it, which means the delta query remains the source of truth.
- Validation is required at subscription time. The endpoint must echo a token back promptly or the subscription is never created.
Because a lapsed subscription is silent, push should never be the only mechanism. A periodic delta query as a floor turns a missed renewal into a delay rather than an outage, and it is the difference between a mailbox that goes quiet for an hour and one that goes quiet until somebody complains.
Does Connect use Microsoft Graph?#
Used, as one of the three mail transports a mailbox may be connected with — the others being the Gmail API and any IMAP/SMTP server. As with the others, providers.build() reads the mailbox's transport and config and nothing else, so a workspace can hold a Gmail mailbox, a Graph mailbox and an IMAP mailbox at once and every screen treats them identically.
That uniformity is a property of the data model rather than of discipline. Whatever the adapter fetched is stored in the tenant_* tables in the provider's own shape; the canonical threads, messages and contacts that the engine and the interface read are produced from them by connect_core.bridge_*. The engine never reads the provider tables, which is why a Graph-specific quirk stays inside the adapter.
Mailbox settings that a reader might expect to be transport-specific are not: role, signature, autonomy mode and the health verdicts live on the mailbox row regardless of provider, and one mailbox_console over one mailboxes.py policy serves both the Owner and a customer. There is no second implementation for either audience and none for either provider.
Permissions and the shape of failure#
- Delegated versus application permissions
- Delegated acts as the signed-in person and is bounded by what they can do. Application permissions act without a user and reach every mailbox in the tenant unless scoped — a much larger blast radius, and rarely what a per-mailbox integration wants.
- Admin consent
- Several mail scopes require a tenant administrator to approve them. A flow that works for the developer's own account and fails for a customer is usually this.
- Conditional access
- An organisation's policy can refuse a token on device or location grounds. The error arrives at authentication time and has nothing to do with the mailbox.
- Throttling
- Graph answers 429 with a
Retry-After. Honour the header rather than inventing a backoff; ignoring it lengthens the penalty. - Licence state
- A mailbox whose licence lapses stops answering in a way that reads like a permission error. Worth distinguishing before re-running consent.
Questions#
Is Microsoft Graph mail easier to adopt than the Gmail API?
For a codebase that already speaks IMAP, usually yes, because the folder model carries over directly and only the synchronisation mechanism changes. Gmail's label model requires rethinking anything that assumed one location per message.
What happens when a change-notification subscription expires?
Notifications stop and nothing reports an error, which is why a delta-query poll should run underneath as a floor. Renewal is the client's responsibility and needs to happen well before expiry, not at it.
Can one workspace mix Gmail, Graph and IMAP mailboxes?
Yes. Transport is a per-mailbox property, and everything downstream reads canonical records that carry no trace of which adapter produced them. Roles, signatures, autonomy and health work the same way for all three.