Closes#390. The reactive complement to the pre-auth Outlook IMAP notice (#411/#426).
Problem
When account setup obtains a valid credential but the IMAP AUTHENTICATE step is then rejected because IMAP access is switched off for the mailbox (the default on new personal outlook.com accounts; also possible on Gmail etc.), the user saw an opaque generic "authentication failed" with no hint that the real fix is an account-side IMAP toggle.
Approach
ImapAuthError.isImapDisabled(error, usedOAuth) (new, org.libremail.mail) classifies the failure on two deliberately-conservative signals:
Explicit provider text in the exception (or its cause chain): IMAP ... disabled / not enabled / turned off, incl. Gmail's Your account is not enabled for IMAP use.
OAuth inference: on the Outlook XOAUTH2 path, a valid-token AuthenticationFailedException -- outlook.office365.com returns only a generic AUTHENTICATE failed with no distinctive text, and the token was already accepted at exchange, so an AUTHENTICATE rejection here means IMAP is off, not a bad token.
Ordinary wrong-password / expired-token / network failures are not matched, so they keep the existing generic error (verified, incl. a real GreenMail wrong-password rejection).
imapDisabledPromptFor(error, account, usedOAuth) turns a classified failure into a provider-aware prompt: brand via MailProvider.brandFor(account) (so even a manually-configured Gmail host is recognised), with the provider's enable-IMAP help URL for Outlook + Gmail and a generic message with no link otherwise.
UI surface chosen
A shared ImapDisabledDialog (Material 3 AlertDialog) rather than a dedicated screen: the reactive failure can surface from three entry points -- the Outlook picker (AccountSetupViewModel/AccountPickerScreen), the app-password form, and manual setup -- so a dialog overlaid on whichever screen the user is on is the consistent, low-plumbing choice (a full screen would need navigation wiring across all three). It shows sign-in-succeeded-but-IMAP-rejected copy, a "How to turn on IMAP" link (leading), and "Got it" (trailing, the stable E2E click target). All three view-models set an imapDisabledPrompt in state instead of the generic error when classified as disabled.
Gmail: not enabled for IMAP use / enable ... IMAP text; links Google's IMAP settings article.
Yahoo/iCloud/AOL: recognised brand, generic message, no deep link (they gate via app passwords, not a user-facing IMAP switch).
Logging
PII-free AppLog breadcrumbs at each classification/prompt point using accountLogRef(id) only -- never the email/host/token. The dialog stays pure UI.
Tests
ImapAuthErrorTest -- provider "IMAP disabled" text mappings + Outlook OAuth inference + negatives (wrong password with/without OAuth, token-exchange failure, network error, bare IMAP host in message), plus a real GreenMail wrong-password rejection asserted not misclassified.
Instrumented AppPasswordSetupScreenTest case driving the failure through a FakeAccountRepository end to end (dialog + Gmail help-link ACTION_VIEW via Espresso-Intents).
Closes #390. The reactive complement to the pre-auth Outlook IMAP notice (#411/#426).
## Problem
When account setup obtains a valid credential but the IMAP `AUTHENTICATE` step is then rejected because IMAP access is switched **off** for the mailbox (the default on new personal `outlook.com` accounts; also possible on Gmail etc.), the user saw an opaque generic "authentication failed" with no hint that the real fix is an account-side IMAP toggle.
## Approach
- **`ImapAuthError.isImapDisabled(error, usedOAuth)`** (new, `org.libremail.mail`) classifies the failure on two deliberately-conservative signals:
1. **Explicit provider text** in the exception (or its cause chain): `IMAP ... disabled / not enabled / turned off`, incl. Gmail's `Your account is not enabled for IMAP use`.
2. **OAuth inference**: on the Outlook **XOAUTH2** path, a valid-token `AuthenticationFailedException` -- `outlook.office365.com` returns only a generic `AUTHENTICATE failed` with no distinctive text, and the token was already accepted at exchange, so an AUTHENTICATE rejection here means IMAP is off, not a bad token.
Ordinary wrong-password / expired-token / network failures are **not** matched, so they keep the existing generic error (verified, incl. a real GreenMail wrong-password rejection).
- **`imapDisabledPromptFor(error, account, usedOAuth)`** turns a classified failure into a provider-aware prompt: brand via `MailProvider.brandFor(account)` (so even a manually-configured Gmail host is recognised), with the provider's enable-IMAP help URL for Outlook + Gmail and a generic message with no link otherwise.
## UI surface chosen
A shared **`ImapDisabledDialog`** (Material 3 `AlertDialog`) rather than a dedicated screen: the reactive failure can surface from three entry points -- the Outlook picker (`AccountSetupViewModel`/`AccountPickerScreen`), the app-password form, and manual setup -- so a dialog overlaid on whichever screen the user is on is the consistent, low-plumbing choice (a full screen would need navigation wiring across all three). It shows sign-in-succeeded-but-IMAP-rejected copy, a "How to turn on IMAP" link (leading), and "Got it" (trailing, the stable E2E click target). All three view-models set an `imapDisabledPrompt` in state instead of the generic `error` when classified as disabled.
## Provider signals keyed on
- **Outlook/Microsoft** (primary, the on-device evidence in #390): valid XOAUTH2 token + generic `AUTHENTICATE failed` -> IMAP off; links Microsoft's POP/IMAP settings article.
- **Gmail**: `not enabled for IMAP use` / `enable ... IMAP` text; links Google's IMAP settings article.
- **Yahoo/iCloud/AOL**: recognised brand, generic message, no deep link (they gate via app passwords, not a user-facing IMAP switch).
## Logging
PII-free `AppLog` breadcrumbs at each classification/prompt point using `accountLogRef(id)` only -- never the email/host/token. The dialog stays pure UI.
## Tests
- `ImapAuthErrorTest` -- provider "IMAP disabled" text mappings + Outlook OAuth inference + negatives (wrong password with/without OAuth, token-exchange failure, network error, bare IMAP host in message), plus a real GreenMail wrong-password rejection asserted *not* misclassified.
- `ImapDisabledPromptTest` -- brand/URL resolution (Outlook, Gmail, manual-Gmail-host, Yahoo, unknown host, non-disabled).
- `ImapDisabledDialogJvmTest` (Robolectric) -- branded vs generic message, help-link launch without dismiss, "Got it" dismiss.
- Per-view-model IMAP-disabled tests (`AccountSetupViewModelTest`, `AppPasswordViewModelTest`, `ManualSetupViewModelTest`) + per-screen dialog-wiring tests (all three screen JVM tests).
- Instrumented `AppPasswordSetupScreenTest` case driving the failure through a `FakeAccountRepository` end to end (dialog + Gmail help-link `ACTION_VIEW` via Espresso-Intents).
## Local gate (all green)
`assembleDebug` + `testDebugUnitTest` + `jacocoTestCoverageVerification` (0.84 floor held) + `compileDebugAndroidTestKotlin` + `lintDebug` + `ktlintCheck` + `detekt`. Emulator E2E left to CI's matrix.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #390. The reactive complement to the pre-auth Outlook IMAP notice (#411/#426).
Problem
When account setup obtains a valid credential but the IMAP
AUTHENTICATEstep is then rejected because IMAP access is switched off for the mailbox (the default on new personaloutlook.comaccounts; also possible on Gmail etc.), the user saw an opaque generic "authentication failed" with no hint that the real fix is an account-side IMAP toggle.Approach
ImapAuthError.isImapDisabled(error, usedOAuth)(new,org.libremail.mail) classifies the failure on two deliberately-conservative signals:IMAP ... disabled / not enabled / turned off, incl. Gmail'sYour account is not enabled for IMAP use.AuthenticationFailedException--outlook.office365.comreturns only a genericAUTHENTICATE failedwith no distinctive text, and the token was already accepted at exchange, so an AUTHENTICATE rejection here means IMAP is off, not a bad token.Ordinary wrong-password / expired-token / network failures are not matched, so they keep the existing generic error (verified, incl. a real GreenMail wrong-password rejection).
imapDisabledPromptFor(error, account, usedOAuth)turns a classified failure into a provider-aware prompt: brand viaMailProvider.brandFor(account)(so even a manually-configured Gmail host is recognised), with the provider's enable-IMAP help URL for Outlook + Gmail and a generic message with no link otherwise.UI surface chosen
A shared
ImapDisabledDialog(Material 3AlertDialog) rather than a dedicated screen: the reactive failure can surface from three entry points -- the Outlook picker (AccountSetupViewModel/AccountPickerScreen), the app-password form, and manual setup -- so a dialog overlaid on whichever screen the user is on is the consistent, low-plumbing choice (a full screen would need navigation wiring across all three). It shows sign-in-succeeded-but-IMAP-rejected copy, a "How to turn on IMAP" link (leading), and "Got it" (trailing, the stable E2E click target). All three view-models set animapDisabledPromptin state instead of the genericerrorwhen classified as disabled.Provider signals keyed on
AUTHENTICATE failed-> IMAP off; links Microsoft's POP/IMAP settings article.not enabled for IMAP use/enable ... IMAPtext; links Google's IMAP settings article.Logging
PII-free
AppLogbreadcrumbs at each classification/prompt point usingaccountLogRef(id)only -- never the email/host/token. The dialog stays pure UI.Tests
ImapAuthErrorTest-- provider "IMAP disabled" text mappings + Outlook OAuth inference + negatives (wrong password with/without OAuth, token-exchange failure, network error, bare IMAP host in message), plus a real GreenMail wrong-password rejection asserted not misclassified.ImapDisabledPromptTest-- brand/URL resolution (Outlook, Gmail, manual-Gmail-host, Yahoo, unknown host, non-disabled).ImapDisabledDialogJvmTest(Robolectric) -- branded vs generic message, help-link launch without dismiss, "Got it" dismiss.AccountSetupViewModelTest,AppPasswordViewModelTest,ManualSetupViewModelTest) + per-screen dialog-wiring tests (all three screen JVM tests).AppPasswordSetupScreenTestcase driving the failure through aFakeAccountRepositoryend to end (dialog + Gmail help-linkACTION_VIEWvia Espresso-Intents).Local gate (all green)
assembleDebug+testDebugUnitTest+jacocoTestCoverageVerification(0.84 floor held) +compileDebugAndroidTestKotlin+lintDebug+ktlintCheck+detekt. Emulator E2E left to CI's matrix.Merge Queue Status
2026-07-08 05:23 UTC· Rule:default· triggered by merge protections2026-07-08 05:43 UTC· atd6665fe8c379ebcdf415432f711aacac00c0f946· mergeThis pull request spent 19 minutes 55 seconds in the queue, including 19 minutes 44 seconds running CI.
Required conditions to merge
-conflict-draftbase = maincheck-success = CI passedgithub-review-approved[🛡 GitHub repository ruleset rulemain]label != brokencheck-success = Debug buildcheck-neutral = Debug buildcheck-skipped = Debug buildcheck-success = Unit testscheck-neutral = Unit testscheck-skipped = Unit testscheck-success = CI passedcheck-neutral = CI passedcheck-skipped = CI passedmain]:check-success = @github-actions/CI passedcheck-neutral = @github-actions/CI passedcheck-skipped = @github-actions/CI passed