The Opt-In Mail Copy

Mail is read live from the provider by default. A person can switch a copy on for an account, on one installation, and that installation then keeps the last 90 days of the account's inbox as nodes in the person's own partition: searchable, instant to open, and — for real file attachments up to 25 MB — with the files stored and indexed.

Where it lives

Since the move into core (2026-10) the provider-neutral half is in MeshWeaver.Graph, namespace MeshWeaver.Graph.MailCopy. It was born in the Mail module (MeshWeaver.Mail.MicrosoftGraph) and holds no Microsoft Graph code:

Piece What it is
MailCopySource, MailMessageCopy, MailAttachmentCopy The stored shapes. The type names ARE the $type discriminators (MailCopySource, MailMessageCopy) and the property spellings are the stored ones — do not rename; every stored copy and the Executive Assistant read them.
MailCopyNodeTypes Paths ({owner}/_MailCopy/{key}/{digest}), ids, generations, the two node types (KeepsHistory = false: a switch-off must leave nothing behind) and AddMailCopyTypes().
IMailCopyStore / MeshMailCopyStore The copies as mesh nodes, written as System on the owner's behalf; generation-checked commits; deletes files and index before nodes.
MailAttachments, IMailAttachmentSource, IMailAttachmentStore The attachment rules (files only, up to 25 MB, not inline) and the seam a provider implements to list and fetch.
MailCopyEngine, IMailCopyFeed The generic sync: generations, the lease, the window, tombstones, attachments, the switch-off. A provider supplies a feed (its incremental change source).
IMailCopySwitch, IMailAccountCopy The per-installation, per-account switch (below).

A provider module (Mail for Outlook, Google for Gmail) owns only what talks to its provider: Outlook's engine (Graph delta), Gmail's feed (history id), the inbox legs that read the copy, and the settings tab.

The attachment contract

MailMessageCopy.Attachments is null while undecided and a list once decided (an empty list for a message without any). Each MailAttachmentCopy carries AttachmentId, Name, ContentType, Size, IsInline, Kind (file, item, reference), StoredPath and SkippedReason (inline, item, reference, too-large, fetch-failed). StoredPath = {copyPath}/attachments/{attachmentIdDigest}/{cleanedFileName}, read through the copy node's attachments collection (every node has one; a nested node has no content collection of its own). The shape is identical across providers.

Opt-in, per installation, per account

A copy exists only where the person switched it on, on that installation. A connected account never starts one by itself, and a Google or Microsoft grant held on a mother instance for a child implies no copy there. IMailCopySwitch.Offer(owner) lists the connected accounts with their switch (all off until someone says otherwise) — the call onboarding makes to ask "Keep a copy on this installation?" — and Enable / Disable / IsOn act per (provider, accountId). The caller must have established that the viewer is the owner.

Layout of an account's copy

The default account of a provider keeps {owner}/_MailCopy/{provider}; every other connected account is its own copy at {owner}/_MailCopy/{provider}.{accountId} (MailCopyNodeTypes.KeyFor).

Who may call the stores

MeshMailCopyStore and MeshMailAttachmentStore write as System on the owner's behalf, and every NodeType can reach them, so they authorize the caller before they elevate (Authorize as the Caller, Execute as System):

Reads that decide writes

What a write is decided on comes from the node's stream, never from the index: Read and Revision use the index only to learn that a copy EXISTS and then read it from its stream, and a copy is deleted with its files when its LIVE content may hold files. An index miss is not proof of absence: Remove leaves a copy the index does not list yet to the next sweep instead of deleting it without its files.

A tombstone keeps the "may hold files" marker (HasAttachments): once it lands, the copy's inventory is gone, so a cleanup that failed after the tombstone was written is repeated by the next attempt, which finds the marker on the tombstone it replaces.

Open: the conditional writes (Commit, ReplaceIfNewer, WriteIfUnchanged, SetAttachments) decide on the writer's node-stream mirror. The owner's three-way merge refuses a field the write changes when the owner's value moved since the mirror's base, which is what makes the lease safe across replicas; a field the predicate only READS (a generation, a version, Removed) is not checked by the owner. Closing that needs an owner-side precondition on a cross-hub update, which the platform does not have yet.

What a provider adds beyond the sync

A provider supplies an IMailCopyFeed (its account, its incremental changes, its attachments) and the engine does the rest. Three more things come from the engine, so the provider only maps copies to its own rows:

The engine's background paths declare System on the owner's behalf; the public surfaces (Enable, Kick, Sync, ServesFromCopy, FollowMove) are judged as the CODE that calls them by the in-mesh impersonation guard.