Account setup: provider presets, settings discovery, and progressive disclosure - #393
Merged
Conversation
…cover Adding an account asked for IMAP host, port, SSL, cert, SMTP host, port, SSL, and cert even when QuickMail already knew every one of those values. The only provider knowledge that existed was hardcoded domain-sniffing in two methods of AddAccountViewModel, so adding a provider meant editing them. Now the dialog asks for a provider, an email address, and a password. Everything else is derived, and the full manual form is one collapsed expander away. - ProviderCatalog: Gmail, Outlook.com / Microsoft 365, Yahoo, iCloud, Other. Each MailProvider carries hosts, ports, SSL modes, default auth, and an app-password hint plus the URL where the user creates one. AccountModel gains ProviderId; Resolve falls back to matching the IMAP host, so accounts created before the catalog need no migration. - AutoDiscoverService: local catalog, then Mozilla autoconfig (domain only), then the domain's own Exchange Autodiscover endpoint. HTTPS only, no redirect following, per-tier and overall timeouts, and every failure falls through rather than propagating. Off switch: AutoDiscoverOnline in config.ini. This Autodiscover client is also the prerequisite for shared mailboxes (#31). - Gmail defaults to an app password with a prominent hint, since Google OAuth sign-in is blocked for new accounts (#369, #226). Google sign-in stays available under Advanced settings. - Add Account and Manage Accounts both move server settings behind an Advanced settings expander, matching the pattern already used by the rule editor. Microsoft's IMAP-vs-Graph choice becomes "Connection method" inside it, still defaulting to IMAP so no new account silently changes backend. - Test Connection now probes SMTP as well as IMAP and reports each leg separately: a working inbox with a misconfigured send server used to pass and then fail on first send. Graph accounts are probed via GET /me instead of being told to press a different button. First test coverage this command has ever had. - ProviderCatalog.IsICloud replaces an ImapHost == "imap.mail.me.com" comparison that was duplicated across six files. Announcements follow the category rules: provider consequences and app-password requirements are Hints, lookup progress is Status, lookup and verification outcomes are Results. No instructional text is baked into any AutomationProperties.Name, and MailProvider overrides ToString so the provider combo announces "Gmail" rather than its type name. 70 new tests; full suite 1741 passing in Release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Security - Discovery never accepts a cleartext configuration. UseSsl=false maps to MailKit's StartTlsWhenAvailable, which connects in plaintext and authenticates anyway when the server advertises no STARTTLS — so a socketType of "plain", an Autodiscover <SSL>off</SSL>, or SMTP on port 25 would have let whoever answers for a typosquatted domain name a server that harvests the password in the clear, with the user seeing only "Settings found" because Advanced settings stays collapsed. All three are now rejected. A user who types a plaintext host into Advanced settings themselves is unaffected. - "Settings found" now names the discovered hosts. They are about to receive a password and they arrived over the network, so they are stated rather than applied silently. - Cap discovery responses at 512 KB. - Follow 30x responses manually, HTTPS-only, max 3 hops. Automatic redirects stay off (a hop could downgrade to HTTP) but treating every redirect as "nothing here" made the Microsoft 365 custom-domain path — a 302 to autodiscover-s.outlook.com — dead. Correctness - ProviderCatalog.Resolve no longer treats a persisted "other" as an answer. It meant an iCloud account added via Other with the Apple host typed by hand resolved as Other forever, so IsICloud said false, contact and calendar sync silently never ran, and the checkboxes vanished from Manage Accounts so it could not be fixed. - Correcting a typo'd address now drops the old provider. Typing kelly@gmail.com and then editing it left Gmail selected, saved the account against imap.gmail.com, and skipped the settings lookup entirely because a provider was already chosen. - MailServiceRouter routes an unregistered account by its BackendKind. Test Connection's probe account uses a throwaway Guid, so the Graph branch was probing the IMAP backend against a host it had just cleared and reporting the IMAP error as a Graph failure. - Discovery is invokable while a lookup is in flight (AsyncRelayCommand reported CanExecute=false, silently dropping the second lookup so the first one's servers landed on the corrected address), and a result whose domain no longer matches is discarded. - Switching Connection method back to IMAP no longer collapses the expander the user is standing in, which stranded keyboard focus on the window with no announcement. - Port and SSL changes count as user edits, not just host names. - Add Account refuses to save without servers and moves focus to what is missing; the fields are behind a collapsed expander now, so this was silently saveable. - The View is told when the VM clears the password, so the PasswordBox stops showing dots for a password that no longer exists. Accessibility - Manage Accounts' provider value is announced. An explicit AutomationProperties.Name overrides a TextBlock's automatic name, so it read "Provider" twice and never the value. Also dropped Focusable, which put it last in the tab order despite being visually first. - App-password link gets a TabIndex; it was landing after the signature box. - Resolved three new access-key collisions (SSL/calendar, method/Microsoft, signature/Google). - Trimmed AutomationProperties.Name values back to short labels. 35 new tests, one per fix; full suite 1776 passing in Release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tab order Opening Advanced settings put every control it revealed BEHIND the header that revealed them, so reaching them meant Shift+Tab backwards. Cause: the Expander's header button takes WPF's default TabIndex of int.MaxValue, while the content had explicit 10-20, so the content sorted first. Measured, not guessed — the order was ... DisplayNameBox -> AuthTypeCombo -> ImapHostBox -> ... -> SignatureBox -> HeaderSite -> Cancel Removing the explicit indices inside both expanders leaves header and content tied at the default, and WPF falls back to tree order: ... DisplayNameBox -> HeaderSite -> AuthTypeCombo -> ImapHostBox -> ... -> Cancel AddAccountTabOrderTests walks real WPF focus traversal (MoveFocus, not TabIndex values) so this cannot silently come back. Microsoft 365 custom domains A work account on its own domain — the common shape for business mail — fell all the way through to "enter your IMAP host", for an account whose only way in is a browser sign-in. Verified against a real tenant: ISPDB 404s, and BOTH Autodiscover hosts fail to resolve, so no amount of redirect handling reaches them. Added a fourth tier: Microsoft's getuserrealm lookup, queried with a fixed "discover@" local part so only the domain leaves the machine. On a hit, the Microsoft provider is selected and Sign in with Microsoft is put in front of the user — which is what the previous experience offered and this one had lost. It is deliberately a suggestion, not a configuration. The lookup answers "does this domain have an Entra ID tenant", which a company running its own mail but using Microsoft 365 internally also answers yes to, so the wording is "appears to be" and changing the provider is one keystroke. Managed/Federated counts only when DomainName matches the domain asked about: gmail.com and yahoo.com both answer Federated with DomainName live.com, and must not be read as tenants. When even that misses, the not-found message now names the sign-in routes instead of only demanding an IMAP host. Also: per-request timeout 5s -> 4s and overall 12s -> 15s, so four tiers fit. Live checks against real domains live in QuickMail.IntegrationTests, gated behind QUICKMAIL_LIVE_DISCOVERY=1. All four pass: the tenant domain resolves to Microsoft, a domain with no mail service returns nothing, a catalog domain never touches the network, and nothing is sent at all when AutoDiscoverOnline is off. Full suite 1788 passing in Release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dialog opens with "Other" selected, so having it last in the list left Down arrow with nowhere to go — the real providers were unreachable by keyboard from the position the dialog puts you in. Moved it to the front. Also adopts the useful half of CityDweller's PR #388: Manage Accounts kept the Test Connection button visible with no account selected, because OnSelectedAccountChanged returns early on a null selection and leaves BackendKind at its default. ShowTestConnection => IsEditing fixes that. Deliberately NOT gated on IsImapBackend as #388 has it. That gate was right while Test Connection could only probe IMAP; this branch made it probe Graph accounts too, via the backend's GET /me, so the gate would now hide the button exactly where it just started working. Same reason the Add Account dialog leaves it visible rather than taking #388's IsImapBackend binding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signing in with a Microsoft 365 work address ended at "your administrator needs to make a change" — the same error the IMAP+OAuth path gave back when Graph was behind the feature gate. The live app does not do this, because there you pick "Microsoft 365 / Outlook.com" by hand, which selects the Graph backend. Cause is scope selection (OAuthService.DefaultScopesFor): BackendKind != MicrosoftGraph -> outlook.office.com/IMAP.AccessAsUser.All + SMTP.Send BackendKind == MicrosoftGraph -> graph.microsoft.com/.default .default resolves to exactly the permissions the app registration declares and a tenant admin has already consented to. Most tenants have never consented to the IMAP/SMTP pair, and many disable IMAP outright. So the Microsoft provider's DefaultBackend of ImapSmtp — kept deliberately to avoid changing behaviour — was the wrong default for precisely the accounts the new realm tier now finds. A Microsoft account whose domain is NOT one of Microsoft's own consumer domains is a work or school tenant, so it now moves to the Graph backend. Consumer Outlook.com/Hotmail/Live accounts are the opposite case — no admin-consent model, IMAP+OAuth works — and keep the IMAP default, so nothing changes for them. With the Graph gate off there is nothing better available and the account stays on IMAP. Two things fixed along the way: - ApplyDiscovered called ApplyProvider again after assigning SelectedProvider. The assignment already applies the provider and any backend preference layered on top, so the second call re-applied the provider's DEFAULT backend and silently undid the Graph switch. Caught by the end-to-end discovery test. - Only a provider the typed address selected may be reset when the address stops matching. Choosing Microsoft 365 by hand and then typing a work address was throwing that choice away mid-typing. Scope-level tests assert both directions, because the scopes are where this actually bites. Full suite 1803 passing in Release; live discovery checks still pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ists Adding a theideaplace.net account failed with SMTP 535. The domain was on Exchange Online until March; the mail moved away but the Entra ID tenant stayed. Tier 4 asked Microsoft "does this domain have a tenant", got yes, filled in outlook.office365.com / smtp-mail.outlook.com, and the real password for the real mail host was offered to Microsoft's SMTP server. 535. The general fault is that the tier asked the wrong question. "Does this organisation have a Microsoft tenant" is not "is this domain's mail in Exchange Online", and the two answers diverge in at least two ordinary situations: - a domain whose mail has moved away but whose tenant remains (theideaplace.net) - a company running its own mail while using Microsoft 365 internally (fastmail.com) Both answered yes. Both would have been given Microsoft's servers. Tier 4 now reads the domain's public DNS instead, which states where the mail actually goes: MX under mail.protection.outlook.com -> Exchange Online MX under aspmx.l.google.com -> Google Workspace autodiscover CNAME -> autodiscover.outlook.com The CNAME is checked as well as MX because organisations commonly front Exchange Online with a filtering gateway, which replaces the MX while the mailboxes stay put. Verified against real domains: icanbrew.com resolves to Microsoft, theideaplace.net and fastmail.com are correctly left alone, wikipedia.org returns nothing. Uses DNS-over-HTTPS through the existing HttpClient, so no DNS dependency is added, and only the domain is sent — the same privacy bar as the ISPDB tier, and better than the realm lookup it replaces, which received an address-shaped string. Google Workspace detection falls out of the same signal, so custom domains on Google now resolve to Gmail's servers instead of dead-ending. Full suite 1809 passing in Release; six live discovery checks pass, including both false-positive cases as explicit regression tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Arrowing Other -> Gmail wrote "Gmail" into the account name. Arrowing on to Microsoft did not correct it, because the box was no longer blank — so the account ended up named Gmail while pointing at Microsoft, unless the user noticed and fixed it. Worse for anyone not looking at the field, since selecting a provider fires for every item arrowed past, so passing through a provider was enough to name the account after it. Writing into one field as a side effect of changing another is the fault. Removed, along with the compensating logic that existed only to undo it when the address stopped matching. It bought nothing: AccountModel.AccountLabel already falls back to the email address when the name is blank, which is a better default anyway for anyone with two accounts at the same provider. The account-name field says so in its help text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AutomationProperties.HelpText is read by the screen reader directly, so it ignores QuickMail's announcement preferences entirely — a user who has turned hints off still hears every one. AccessibilityHelper.Announce routes through AnnounceHints, which is the point of having the setting. This is the same rule the Accessibility Checklist states for AutomationProperties.Name; HelpText is the same mistake wearing a different attribute. Both account dialogs now carry no HelpText at all, and speak their field hints as AnnouncementCategory.Hint when the field takes keyboard focus, via one bubbling GotKeyboardFocus handler per dialog. Guarded by tests that read the XAML: no HelpText in either dialog, the focus handler is wired in both, and no AutomationProperties.Name is a sentence — a sentence in a Name is an instruction wearing a label's clothes and bypasses the preferences the same way. Full suite 1816 passing in Release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three defects found by review, each one a way for whoever answers a lookup to end up holding the account's password. 1. The redirect follower re-sent the address to any HTTPS host. SendFollowingHttpsRedirectsAsync rebuilt the request from newRequest() on every hop, so the Autodiscover POST — whose body carries <EMailAddress>the user's full address</EMailAddress> — went out again at each new URL, and only the scheme was checked. A 302 from autodiscover.<domain> to any HTTPS host therefore POSTed the address there, and that host's <Server> values then became the account's IMAP and SMTP hosts: not a redirect, a handover. A hop is now allowed only to the host just asked, to the queried domain or a subdomain of it, or to an explicit Microsoft list (outlook.com, office365.com, microsoftonline.com) — which exists because an M365 custom domain legitimately 302s to autodiscover-s.outlook.com, the case redirect support was added for. Anything else is refused and logged (domain and refused host; never the address). 301/302/303 additionally downgrade to a bodyless GET, as browsers and HttpClientHandler do; 307/308 may keep the method and body, to an allowed host. ISPDB and DNS-over-HTTPS pass their own fixed service host as the scope, so a hop off autoconfig.thunderbird.net or cloudflare-dns.com is refused too. The comment claiming redirects are not followed was false and is corrected. 2. The cleartext refusal blocked a label, not the downgrade. ImapUseSsl / SmtpUseSsl false mapped to MailKit's StartTlsWhenAvailable, which connects in PLAINTEXT and authenticates anyway when the server advertises no STARTTLS. Rejecting socketType "plain" and <SSL>off</SSL> removed the label while leaving the mechanism: ISPDB socketType STARTTLS, Autodiscover IMAP on 143, and Autodiscover SMTP on 587 all still produced UseSsl == false, so a hostile or typosquatted responder could name a host that simply never offers STARTTLS and collect the password in the clear, while the dialog said only "Settings found". AccountModel gains a persisted RequireStartTls (default false, so accounts already in accounts.json are unchanged), and the new MailSecurity maps UseSsl to SslOnConnect, else RequireStartTls to StartTls — which fails rather than falling back — else StartTlsWhenAvailable as before. It is set for settings QuickMail chose on the user's behalf: the built-in catalog and every discovery tier. Editing a server field by hand clears it, because from then on the settings are the user's and the permissive behaviour is their choice to make. Carried through DiscoveredSettings, ApplyProvider, ApplyDiscovered, ToAccountModel, the Manage Accounts load/save, the Test Connection probe, and the IMAP connection-pool identity. AutodiscoverOnPort143IsAcceptedAsStartTlsNotImplicitSsl asserted only that port 143 came back with ImapUseSsl false — which was exactly the hole — so it is replaced rather than extended. 3. MatchesAnySuffix claimed every host under google.com. GoogleMailHostSuffixes held a bare "google.com", so an MX of not-really-mail.google.com was read as "this domain's mail is Gmail" and filled in Gmail's servers plus its app-password requirement. Google Workspace delivers under l.google.com; the bare entry is dropped. Verified by reverting each fix in turn: 5 tests fail without the redirect restrictions, 8 without the STARTTLS requirement, 2 without the suffix fix. Full suite 1840 passing; the live discovery checks still pass, and the M365 custom-domain path is unaffected — autodiscover-s.outlook.com answers 401 to an anonymous POST as well as to a GET, and that detection comes from the DNS tier in practice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… router leak A. A personal Microsoft account on a vanity domain was routed to Graph. The backend was chosen from the domain alone: "not a Microsoft consumer domain, therefore a work tenant, therefore Graph". A personal account on a custom domain is indistinguishable from a work tenant by that test, and on Graph it draws work scopes that under-deliver for it (#217/#233/#239); on IMAP it worked. MSAL reports the truth in the token, which is why #233 added IsPersonalMicrosoftAccount, and sign-in happens before the account is created. AccountEditorViewModel now calls a new OnMicrosoftSignInCompleted hook after sign-in; AddAccountViewModel overrides it to move a personal account back to the IMAP option, which restores the provider hosts via OnSelectedBackendChanged rather than leaving them blank. The domain rule now also refuses to send a known-personal account to Graph at all. B. Typing "kelly@outlook.com" ended on Graph with blank hosts. UsernameBox binds with UpdateSourceTrigger=PropertyChanged, so the decision ran on every keystroke and the prefix "kelly@o" matched no consumer domain: the form switched to Graph and ClearHostsForGraph blanked the hosts and the password mid-word. The finished address then re-selected the SAME provider, so MatchProviderFromUsername returned early, and the rule only ever moved toward Graph, so nothing came back. Two changes: the decision is no longer made per keystroke but at the point the address is finished (a new CommitUsername, called from UsernameBox_LostFocus -- the same moment the settings lookup already waits for -- and from AddButton_Click, since Enter on the default button never takes focus out of the field); and it now chooses IMAP as readily as Graph, because re-selecting the same provider is not a path that can restore anything. C. A hand-picked connection method was undone by the next keystroke. _providerCameFromUsernameMatch protected a user-picked provider; nothing protected a user-picked backend, so a work account the user moved to IMAP in Advanced settings was forced back to Graph by editing the address -- with no feedback, the combo being inside a collapsed expander. Added the symmetric guard, _backendUserChosen, set unless the assignment came from SetBackendInternally. The personal-account correction in A deliberately overrides it: the tenant id is ground truth, not an inference. D. ApplyProvider's Graph branch assigned the password field directly, so the ClearPassword call below it hit its already-empty early return and PasswordCleared never fired. A PasswordBox cannot be data-bound, so the box would keep showing dots for a password that was gone. Uses ClearPassword. E. Manage Accounts tab order. The New / Delete / Set Default buttons and the account list carried no TabIndex, so they defaulted to int.MaxValue along with the Advanced-settings contents, while the form fields kept low explicit indices -- putting the left column in the middle of the form, between the password box and Advanced settings. The left column is now 0-3 and the form 4-13. ProviderText, which lost Focusable when an incorrect AutomationProperties.Name was removed from it, is reachable again and sits with the fields rather than at the end. F. The SMTP SSL checkbox lost its port information. Shortening the AutomationProperties.Name to a label was right, but an explicit Name OVERRIDES the visible Content, so "port 465 / STARTTLS on 587" survived for sighted users only. Added a HintFor entry in each dialog, spoken as AnnouncementCategory.Hint on focus, where AnnounceHints applies. G. OnFieldFocused re-announced the hint whenever keyboard focus returned to the same control through no action of the user (window re-activation after OAuth, a dropdown closing). It now tracks the last-hinted element and skips a repeat. And AddButton_Click, refusing to save, announced its reason as Status: a refused save is an action outcome, so a user with background progress turned off got silence while focus jumped to a field. It is announced as Result. H. Tests that could not fail. - AccountDialogHintTests were file-text greps ("no HelpText", "a focus handler is wired up") that would both have passed if the hints had been DELETED rather than moved. Replaced with behaviour tests that focus each named control in a real window and assert a non-empty Hint announcement, via a new internal AccessibilityHelper.AnnouncementObserver seam. The two negative greps are kept alongside -- they assert an anti-pattern is absent, which no behaviour test covers. - DiscoveryStaysInvokableWhileAnEarlierLookupIsStillRunning used a stub that completed synchronously, so CanExecute was asked of an already-finished command and the test passed with or without AllowConcurrentExecutions (verified). The stub now blocks until released. - The two provider tests that assigned a whole address in one shot are rewritten to type it one character at a time, as the dialog does; that is what B hid behind. - Added AccountManagerTabOrderTests, measuring real MoveFocus traversal like AddAccountTabOrderTests. This dialog had none, which is why E slipped. I. MailServiceRouter leaked an entry per Test Connection. ConnectAsync TryAdds an unregistered account so the following DisconnectAsync (which carries only a Guid) reaches the same backend, but BuildProbeAccount mints a fresh Guid per probe and UnregisterAccount had no production caller -- the routing table grew for the life of the process. Connect-created bindings are now tracked separately and released by the matching Disconnect, leaving registered accounts untouched. TestConnectionAsync also disconnects the probe in a finally: a probe that failed to connect skipped the disconnect entirely, so the router fix alone would not have covered the failing case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The user guide's "Adding Accounts" section no longer matched the feature. Replaced with a lifecycle-ordered "Accounts" section: choosing a provider, adding an account, what QuickMail works out for you, entering settings yourself, testing a connection, editing, removing. Per-provider material kept and reworked rather than discarded, and the per-provider headings are unchanged so their anchors survive. Only #adding-accounts became #accounts; its one reference was updated and every internal link resolves. Release notes for v0.8.37 gained a section on the setup rework — the largest user-visible change in the release, and previously unmentioned. Two corrections that predate this branch: the guide told users to open Settings -> Accounts, but SettingsDialog has no Accounts tab, and gave Ctrl+Shift+A for it, which is Add attachment. account.manage has no default key at all. Corrected to File -> Manage Accounts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reworks account setup so adding an account asks for three things — provider, address, password — and derives the rest. The full manual form is one collapsed expander away.
Closes the gap that made #369 painful to work around, and supersedes #388.
Why
The Add Account dialog asked for IMAP host, port, SSL, cert, SMTP host, port, SSL, and cert, even when QuickMail already knew every one of those values. The only provider knowledge that existed was hardcoded domain-sniffing in two methods of
AddAccountViewModel, so adding a provider meant editing them.What changed
Provider catalog — Other (first, so Down arrow reaches the rest), Gmail, Outlook.com / Microsoft 365, Yahoo, iCloud.
AccountModel.ProviderIdpersists the choice;Resolvefalls back to matching the IMAP host, so accounts created before the catalog need no migration.Four-tier settings discovery — built-in catalog, Mozilla autoconfig, the domain's Exchange Autodiscover endpoint, then the domain's public DNS. Tiers 2–4 are off when
AutoDiscoverOnline=off.Tier 4 asks where the domain's mail is actually delivered (MX under
mail.protection.outlook.com, MX under Google's, or an autodiscover CNAME to Microsoft). An earlier attempt asked Microsoft's realm endpoint whether the domain had a tenant, which is a different question: a domain keeps its tenant after its mail moves away, and companies run their own mail while using M365 internally. Both answered yes, both got Microsoft's servers, and the user's real password went tosmtp-mail.outlook.comfor a 535. Uses DNS-over-HTTPS through the existingHttpClient— no DNS dependency — and sends only the domain.Gmail defaults to an app password, since Google OAuth sign-in is currently blocked for new accounts (#369, #226). Google sign-in stays available under Advanced settings. Yahoo and iCloud need app passwords too, and say so with a link.
Work-or-school Microsoft accounts connect over Graph. The IMAP path requests
outlook.office.com/IMAP.AccessAsUser.All+SMTP.Send, which most tenants have never consented to, so sign-in ended at "your administrator needs to make a change". Consumer Outlook.com/Hotmail/Live keep IMAP — no admin-consent model, and IMAP+OAuth works. A personal account on a vanity domain is corrected back to IMAP once sign-in reveals the tenant id (#233's signal), because the domain guess can't tell those apart.Test Connection now probes both legs and reports each separately — a working inbox with a misconfigured send server used to pass and then fail on first send — and probes Graph accounts via
GET /me.MailServiceRoutergained a backend selector so an unregistered probe account routes by itsBackendKindinstead of defaulting to IMAP.Dialogs — server settings behind an Advanced expander in both dialogs, matching the rules editor. Field hints go through
AccessibilityHelper.Announcerather thanAutomationProperties.HelpText, soAnnounceHintsactually governs them.Security posture of discovery
Discovery accepts settings on the user's behalf, so it is deliberately stricter than manual entry:
UseSsl=falsemaps to MailKit'sStartTlsWhenAvailable, which connects in plaintext and authenticates anyway when the server offers no STARTTLS. NewAccountModel.RequireStartTlsmaps toStartTls, which fails instead. Set for the catalog and every discovery tier; cleared the moment the user edits a server field, because settings they typed are their call. Existingaccounts.jsondeserializes unchanged.outlook.com,office365.com,microsoftonline.com) — present solely because M365 custom domains 302 toautodiscover-s.outlook.com. 301/302/303 downgrade to a bodyless GET so the Autodiscover envelope, which carries the user's address, never goes out twice.Testing
1859 unit tests (Release), plus live discovery checks against real domains behind
QUICKMAIL_LIVE_DISCOVERY=1, covering both false-positive shapes: a domain that left Exchange Online, and a tenant whose mail is elsewhere.Two reviews ran over this branch. Every fix from the second was verified by reverting the production change and confirming the specific test fails — which caught five tests that would have passed with their fix reverted, including hint tests that were text greps and would have passed if the hints had been deleted rather than moved.
Relationship to other PRs
ShowTestConnection => IsEditing), credited in the code and commit. ItsIsImapBackendgate would now hide Test Connection for Graph accounts, which is where it just started working.