Connect by JBRH Open Connect

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.

Status
Reference What this means
Audience
both, developer
Last verified
Product version
6.3.2

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.

ConcernGraphGmail APIIMAP
Message locationOne folderMany labelsOne folder
Incremental changeDelta query with a delta tokenHistory IDCONDSTORE MODSEQ, if offered
PushWebhook subscription, renewable, expiresWatch with a topic, expiresIDLE on a held connection
ConversationconversationIdthreadIdReconstructed from headers
IdentifiersOpaque strings, can change on moveStable message idsUID within a UIDVALIDITY
Read stateisRead boolean propertyUNREAD 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.nextLink paginates within one delta run. Follow it to the end before treating the run as complete.
  • @odata.deltaLink is 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.