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 |
| 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 three equal choices with the providers' icons:
- Google (Gmail / Workspace) — the address box appears now, and only here; Connect Google runs the
existing broker connect. An address on
gmail.com/googlemail.compre-selects Google, one on a consumer Microsoft domain pre-selects Microsoft and offers Use Microsoft instead (AddMailAccount.Detect,Preselect). An address handed in on the link (?address=) pre-selects the same way. - Microsoft (Outlook / 365) — straight to the existing consent; Microsoft's sign-in page picks the mailbox, so no address is asked.
- Other mailbox (IMAP/SMTP) — sign-in only, not in the inbox yet — the EXISTING Other email
credential form (
/{user}/ConnectionSetup/imap?return=/{user}/MailAccounts). 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 choice says so in its label and in How this works, so the step never promises mail it cannot deliver. An IMAP inbox reader is 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).
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 Your mail card and its Connect your accounts card carry All your mail accounts →, 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.