Mail accounts — every account from the Mail app
A person finds every account their inbox reads in ONE place: the Mail app's accounts view,
/{user}/MailAccounts. Before it, setting up mail was spread over five places nobody could connect —
the setup wizard's Connect your accounts and Your mail cards, /{user}/AddMailAccount, the Google
account page and the Settings tab Google grants — and the inbox linked to none of them (maintainer,
2026-10-30: "this must all be reachable from mail app").
The view composes what already exists. It builds no connect flow of its own.
What the view shows
| Part | Where it comes from |
|---|---|
| One row per mail & calendar and messaging connection this installation offers — Microsoft 365, Google, Other email (IMAP/SMTP), iCloud, Other calendar, Teams, WhatsApp, iMessage | the registered IConnectionProviders and their state from ConnectionCatalog (MailAccountsArea.Rows) |
| Each row's state: Connected, Not connected, Needs attention (the provider refused the credential), Couldn't check | MailAccountsArea.StateOf; couldn't check is never advice to reconnect (#3433) |
| Each row's one action | not connected / refused → the provider's OWN connect flow (Connect), returning to the view; connected or couldn't check → where it is managed: the Google account the inbox READS (located through IMailAccountLocator, which the Google package's module registers: the reader's own selection applied to the hub's content-bearing synced query of the owner's accounts, so an account added, removed or edited in place moves the link even when no connection's health changes — never a guessed path), else the Settings app's Connections tab, where a credential set's stored accounts (imap--{suffix}) are listed with their own forms; a refused credential set is fixed there too |
| Add account | the AddMailAccount step (below) — offered where the Google package brings it |
A note that the assistant may create SharePoint files (never replace one) and why Microsoft asks for consent again — only where SharePoint:AgentWrite=Write |
MailAccountsArea.SharePointWritesNote, text MailAccountsTexts.AssistantCreatesSharePointFiles (EN/DE); see Microsoft Graph scopes → Writing to SharePoint |
| Your mail | the existing MailPreferences and MailCopySettings areas, embedded — their bindings are their own |
| The outcome of the last connect | read off the query the flow came back with (eaConnect, googleConnect + account), MailAccountsArea.Outcome. Nothing on the query is rendered as markup; an account is linked only when it is a path in the owner's own partition |
Add account
/{user}/AddMailAccount is the view's Add account step: one line, Which account do you want in your
inbox?, and one tile per provider in the Getting-started tiles' shape (OnboardingWizardArea.Tile): a
Skins.Card filling its grid cell, the provider's icon and short name, one quiet line of detail, and an
Accent button at the foot whose visible word is the action and whose accessible name names the provider
(AddMailAccount.TileButtonLabel). No address is typed for Google or Microsoft (maintainer, 2026-11-07:
asking for the address before the OAuth sign-in that provides it was friction):
- Google (Gmail and Google Workspace) — Connect starts the broker connect at once. Google asks
which account (
prompt=select_account consent, nologin_hint), and the account it verifies is the one connected; the installation writes that address back onto the account node on the return (AccountPaths.WithBinding, from the grant'semail). An address handed in on the link (?address=) still rides along as the login hint, and the mother then holds Google's choice to it as before. - Microsoft (Outlook and Microsoft 365) — Connect goes straight to the existing consent; Microsoft's sign-in page picks the mailbox.
- Other (IMAP) (Any mailbox over IMAP and SMTP. Sign-in only; not in your inbox yet.) — Set up
opens the EXISTING Other email credential form (
/{user}/ConnectionSetup/imap?return=/{user}/MailAccounts), the only way that asks for an address. There is no second IMAP form. 🚨 Nothing reads an IMAP mailbox into the inbox today: Other email saves and tests the sign-in, and only Google and Microsoft mailboxes are inbox sources. The tile says so, and so does How this works, so the step never promises mail it cannot deliver. An IMAP inbox reader is owed.
With no address given, a live Google connection already on the node is refused ("… is connected;
disconnect it first"): the consent would otherwise replace it with whichever account Google is signed in
with. A binding the mother no longer honours is residue and is cleared with any old address
(AddMailAccount.WithAddress(node, "")), so the new consent is never held to it.
In the apps. The press answers with a NavigationRequest with forceLoad to a server endpoint
(/api/google/connect/start, /auth/ea/connect). The React Native shell used to resolve that as a mesh
path, a page that does not exist, so Connect did nothing there. It now leaves the app (leavesTheApp, the twin
of ServerEndpointPaths): in the macOS desktop app through the same system sign-in sheet as the app's own
sign-in (openOutside, the memexAuth bridge, sharing the system browser's cookies), elsewhere in the
browser. The flow ends on the portal's accounts view inside the sheet, and the person closes it; the app's
page updates from the mesh. Closing the sheet by itself at the end needs a memex:// return on the connect
routes, which is still owed.
POP3 is not offered: no POP reader exists in this repository, and none was invented.
The long explanation sits in a collapsed How this works section (the platform's expander skin), set as
quiet hint prose on a transparent background (the Markdown view otherwise paints its own layer, which read as
a heavy dark box under the tiles). Back to your accounts is a Stealth button with a link
(WithNavigateToHref), not a click handler. The breadcrumb reads the area as words, Add mail account
(BreadcrumbArea.Label in the Blazor portal, areaLabel in the React Native shell), never the raw id.
Every consent ends on the accounts view: Microsoft's return URL is the view, and the Google connect's
start and return routes (BrokerEndpoints.BackUrl) redirect to /{user}/MailAccounts?googleConnect={outcome}&account={path}
— the outcome is still written on the account node, so the account page says it too. A refusal the step
itself decides (an address that is not one, another Google address already connected) is said on the step,
next to Back to your accounts.
The way in, from the inbox
Core's unified inbox embeds every UiContribution of context Inbox above its list. Mail contributes
Mail/Inbox/Accounts, which embeds the InboxAccounts area of the person's own user node:
- the Accounts button, at once;
- one note per source the inbox LISTS that is not connected — Microsoft Teams is not connected.
Connect — and one per source whose credential was refused, each leading to the accounts view
(
MailAccountsArea.Notes). "Lists" is the inbox's own rule: a leg that needs no connection, or whose connection query finds a node — and a query that cannot be read in time lists its source. A channel the inbox does not list is not mentioned.
The other places point INTO the view: the setup wizard's Connect your accounts card carries All your mail accounts → (its Your mail card is the handling preferences only, so the two cards never lead to the same page — 2026-11-07), the Google account page opens with ← Your mail accounts, and the Google grants tab has Your mail accounts.
Inbox fixes made with it
- Mail is ticked by default. The live Outlook and Gmail legs declare
DefaultKind = true; core's stored-mail leg shares the Mail kind, so a fresh inbox lists mail instead of No data to show!. ⚠️ A preferences node created BEFORE this change keeps the Mail tick it was seeded with (core seeds a kind an existing node lacks unticked, by design) — such a person ticks it once. - An unconnected Teams answers EMPTY, at once.
TeamsChatsQueryProviderdecides the connection FIRST, from the stored grants (no Graph call, no token): no connected grant → an empty answer. ATeamsNotConnectedExceptionfrom the token read is an empty answer too. A grant read that does not answer withinConnectionReadBudget(3 s) decides nothing — the token read still says whether the chats can be read (#3433). The verdict, rooms and label reads now run side by side; in sequence their 5 s bounds added up to the query fan-in's own 15 s, which is what producedQueryProviderStalledException … TeamsChatReader did not emit an Initial within … 15s. - A Teams chat whose messages Graph refuses opens in Teams. Graph answered a meeting chat's
GET /chats/{id}/messageswithInsufficientPrivilegeson a live install. The consent already holdsChat.Read— the documented delegated permission for listing a chat's messages — so this is not a missing scope and the consent was NOT widened. The reading view now shows one line and Teams' own deep link,https://teams.microsoft.com/l/chat/{chatId}/conversations(Microsoft's documented chat deep link); every Teams row and reading view carries that link when Graph gives none. Outlook falls back tohttps://outlook.office.com/mail/deeplink/read/{id}; Gmail linkshttps://mail.google.com/mail/u/0/#all/{threadId}, which still finds an archived thread.
What was not established
- Why the meeting chat's messages are refused although
Chat.Readis granted (likely: the person is no longer a member of the meeting chat, or the meeting belongs to another tenant). Not reproduced here. - Why the Gmail reader logged nothing for 15 minutes on the same install. Every step on its path is bounded (the credential read 20 s, the mother's token 15 s, each Gmail call 15 s); the reader was not reproduced. Core's per-source bound ("… did not answer") keeps one silent source from holding the whole list.