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 a partition of the person's OWN for their mail, {owner}_MailCopy: searchable, instant to open, and — for real file attachments up to 25 MB — with the files stored and indexed.

Where it lives

The provider-neutral half lives in the Mail package's module, MeshWeaver.Mail.MicrosoftGraph, folder MailCopy/, namespace MeshWeaver.Mail.MailCopy. It holds no Microsoft Graph code. It was born in this module, spent a while in core (MeshWeaver.Graph, namespace MeshWeaver.Graph.MailCopy), and came back because its only users are the two mail modules — core had no caller. The Google package's module (MeshWeaver.Mail.Gmail) binds it, so Google/index.json declares "requires": ["Mail@^1.25.0", …]; at run time Gmail's module context is served the Mail module's own copy of the assembly (another module's entry assembly), so both modules share one identity for every type below. The stored discriminators (MailCopySource, MailMessageCopy) carry no namespace and no assembly, so stored copies did not notice the move:

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 partition root and the owner's grant, the 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.

The copy partition, `

🚨 The copies never live in the person's home partition (owner, 2026-10-25: "i don't feel good downloading all this stuff to user space. let's see that we create a dedicated partition just for the inbox for every user"). Each owner's copies are their own top-level partition, named by the owner's id with _MailCopy appended — the old {owner}/_MailCopy with the slash replaced (MailCopyNodeTypes.NamespaceFor).

Copies filed before 2026-10-25 are retired and downloaded fresh

Before the change the copies lived at {owner}/_MailCopy/…. They are not moved: the owner decided (2026-10-25) to delete them and download fresh, because moving a live copy — while replicas on the previous image keep syncing and serving it during a roll, and the index trails every write — kept producing races, each a way to lose mail or bring a switched-off copy back. Every sync, switch-on and switch-off first runs IMailCopyStore.RetireLegacy(owner, key):

  1. Fence. The legacy source is read from its stream and a generation-checked commit at its owning hub takes its lease for this attempt (retire:{guid}, RetireLease 2 h) and marks it not backfilled: a sync of the legacy place, of either image, stands down on the lease, and a reader of either image reads the inbox live (readers honor Backfilled). A lease held already refuses the attempt; the next sync repeats it.
  2. Delete the switch-off way. Every legacy copy, from complete listing frames only: its attachment files and index first (IMailAttachmentStore.DeleteLegacy), then its node; the legacy source last. A file that cannot be deleted stops the retirement, and the node keeps the only handle on it.
  3. Confirm, prepare the fresh copy, then delete. The fence is confirmed at the owning hub by a lease renewal on the generation fenced — a source replaced or taken over meanwhile stops the retirement, and one gone meanwhile deletes and switches on nothing. Then (not for a switch-off) the FRESH copy is prepared in {owner}_MailCopy/{key} — a new generation with the fenced copy's window and account, no cursor, not backfilled — leased to this attempt, so nothing syncs it yet. An existing destination is adopted (rotated to a fresh generation, leased) only when it is the retirement's own leftover: the legacy generation (an interrupted #7161 move) or a lapsed retirement lease (a retry); a copy switched on independently is kept as it is. Only THEN is the legacy source deleted, so no failure leaves neither source.
  4. Settle. When this attempt's delete removed the legacy source, the fresh copy is released and the next sync downloads it (with the generation rebuild of #7166 the inbox reads live meanwhile, never blank). When the delete found it already gone — an old-image switch-off in the last moment — the fresh copy is ROLLED BACK: rotated off in one commit that applies only while it still carries this attempt's lease, cleaned the switch-off way (files first) with the lease kept throughout, and deleted.

However an attempt ends — an error, the sync's bound, a disposal — it releases a fence it still holds, and it RECONCILES a fresh copy it still holds: released when the legacy source is still there (the retirement did not finish and the next sync repeats it), rolled back when the legacy source is gone (a switch-off overtook it). Disable retires without switching a fresh copy on. The residual: an Undo or Delete applied by an old-image replica (which takes no lease) between the last complete pass and the legacy source's delete leaves that one copy's files, if it had any, under the home folder.

It is the portal, not the database migration Job, that retires them: the Job does not mount the content store, so it could delete the rows but never the files.

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.

Listing the copies that are on

IMailCopyStore.SourceKeys(owner) lists the keys of the owner's persisted copies whether or not their account is still connected, so a copy whose account is gone can still be shown and deleted. IMailCopyStore.EnabledSources() lists every copy that is on across owners (platform scope, refused to in-mesh code); it is what a keeper reads after a restart to resume them. Both are default interface members that answer none, so a store that cannot list changes nothing.

Rebuilding: alongside, never instead

A copy is rebuilt when its projection is older than the build's, when the provider refuses the stored cursor, or when the connected mailbox is another account. What is served meanwhile depends on which: