Three code-quality cleanups from the PR #54 review, all in the folder-label resolution / provider-label code, so they ship as one change (bundling avoids the collisions three separate PRs would cause). Each was adapted to the current code, which changed after #108 (shared resolvedFolderLabels + providerLabelFor) and #117 (unified attribute→role table) merged.
Host→brand knowledge now lives in one place, MailProvider:
forImapHost matches an entry's imapHostplus new hostAliases — Gmail gains its legacy imap.googlemail.com host (fixes the under-match that rendered "Drafts - gmail.com").
A new companion brandFor(account) is the single host→brand seam. It recognizes Outlook by OAuth auth type or a precise Microsoft host (*.office365.com / outlook.office.com) — no longer any host merely containing "outlook"/"office365" (fixes the over-match).
MailProvider stays the app-password preset registry; Outlook is deliberately not an entry (it uses interactive OAuth, and entries drives the app-password picker), so brand recognition is added in the companion rather than as a preset.
providerLabel() drops its ad-hoc host substrings and just calls MailProvider.brandFor(account), falling back to the email domain.
#68 — i18n: move folder-label disambiguation patterns into strings.xml
The hardcoded English grammars ("base - provider", "base (parent)", "base [path]") become folder_label_with_provider / _with_parent / _with_path string resources. They are threaded into the pure resolver as a LabelPatterns bundle whose defaults match the old literals, so resolveDrawerLabels stays pure and JVM-testable; the @Composable call site resolves the localized strings and passes them down. Translators can now see and reorder/re-punctuate these.
Memoize:resolvedFolderLabels hoists the role→string and pattern stringResource lookups out of a remember(folders, accounts, roleLabels, patterns) calculation, so the resolved map is rebuilt only when those inputs change — not on every recomposition of a caller that composes while idle (the closed navigation drawer). The stable map instance also lets strong-skipping reuse unchanged row label lambdas.
Early-return:resolveDrawerLabels returns baseLabels unchanged in the dominant no-collision case instead of rebuilding a value-identical map.
Fail-fast: both former ?: fallbacks (in the resolver and the drawer row) now use getValue, so a key miss fails loudly instead of silently reverting to un-deduplicated labels.
Behavior unchanged
This is pure cleanup — same outputs. The existing assertions guard it:
FolderLabelsTest (JVM) — the provider-suffix, nested-parent, top-level tie-break, and safety-net cases from #60/#61/#64/#108 all stay green (they call the resolver with default patterns).
FolderDrawerTest (androidTest) — "Drafts - Gmail" / "Drafts - Outlook" rendering and the account-switch-gap suffix still pass (compiled here; runs in CI E2E).
Tests added
MailProviderTest: forImapHost matches primary hosts + the googlemail alias (case-insensitive); matchesHost accepts aliases but not unrelated hosts; brandFor centralizes host→brand incl. Outlook, with an over-match guard (outlook.example.com → no brand).
FolderLabelsTest: providerLabel via the googlemail alias, a manually-configured Office 365 host without OAuth, and the over-match guard (#69); resolver formats via supplied provider/parent/path patterns (#68); early-return returns the same baseLabels instance, fails fast on a missing base label, and is deterministic for equal inputs (#67).
For the maintainer
MailProvider gained a host→brand concern (brandFor) alongside the app-password presets — kept there (vs. a new brand enum) to avoid duplicating the preset hosts. Outlook host matching is intentionally narrow (*.office365.com, outlook.office.com); personal outlook.com/hotmail.com accounts are OAuth and matched by auth type. Scope was held to the resolver / providerLabel / FolderDrawer / strings.xml (+ the MailProvider registry #69 targets); no DB/entity/schema changes, and #66 (persist IMAP delimiter) was left out as requested.
Three code-quality cleanups from the PR #54 review, all in the folder-label resolution / provider-label code, so they ship as one change (bundling avoids the collisions three separate PRs would cause). Each was adapted to the current code, which changed after #108 (shared `resolvedFolderLabels` + `providerLabelFor`) and #117 (unified attribute→role table) merged.
## Closes #67, #68, #69
### #69 — providerLabel: consolidate provider-brand host matching
Host→brand knowledge now lives in one place, `MailProvider`:
- `forImapHost` matches an entry's `imapHost` **plus new `hostAliases`** — Gmail gains its legacy `imap.googlemail.com` host (fixes the under-match that rendered "Drafts - gmail.com").
- A new companion `brandFor(account)` is the single host→brand seam. It recognizes Outlook by OAuth auth type **or a precise Microsoft host** (`*.office365.com` / `outlook.office.com`) — no longer any host merely *containing* "outlook"/"office365" (fixes the over-match).
- `MailProvider` stays the app-password preset registry; Outlook is deliberately **not** an entry (it uses interactive OAuth, and `entries` drives the app-password picker), so brand recognition is added in the companion rather than as a preset.
- `providerLabel()` drops its ad-hoc host substrings and just calls `MailProvider.brandFor(account)`, falling back to the email domain.
### #68 — i18n: move folder-label disambiguation patterns into strings.xml
The hardcoded English grammars (`"base - provider"`, `"base (parent)"`, `"base [path]"`) become `folder_label_with_provider` / `_with_parent` / `_with_path` string resources. They are threaded into the pure resolver as a `LabelPatterns` bundle whose defaults match the old literals, so `resolveDrawerLabels` stays pure and JVM-testable; the `@Composable` call site resolves the localized strings and passes them down. Translators can now see and reorder/re-punctuate these.
### #67 — FolderDrawer: memoize resolution, resolver early-return, fail-fast lookups
- **Memoize:** `resolvedFolderLabels` hoists the role→string and pattern `stringResource` lookups out of a `remember(folders, accounts, roleLabels, patterns)` calculation, so the resolved map is rebuilt only when those inputs change — not on every recomposition of a caller that composes while idle (the closed navigation drawer). The stable map instance also lets strong-skipping reuse unchanged row label lambdas.
- **Early-return:** `resolveDrawerLabels` returns `baseLabels` unchanged in the dominant no-collision case instead of rebuilding a value-identical map.
- **Fail-fast:** both former `?:` fallbacks (in the resolver and the drawer row) now use `getValue`, so a key miss fails loudly instead of silently reverting to un-deduplicated labels.
## Behavior unchanged
This is pure cleanup — same outputs. The existing assertions guard it:
- `FolderLabelsTest` (JVM) — the provider-suffix, nested-parent, top-level tie-break, and safety-net cases from #60/#61/#64/#108 all stay green (they call the resolver with default patterns).
- `FolderDrawerTest` (androidTest) — "Drafts - Gmail" / "Drafts - Outlook" rendering and the account-switch-gap suffix still pass (compiled here; runs in CI E2E).
## Tests added
- `MailProviderTest`: `forImapHost` matches primary hosts + the googlemail alias (case-insensitive); `matchesHost` accepts aliases but not unrelated hosts; `brandFor` centralizes host→brand incl. Outlook, with an over-match guard (`outlook.example.com` → no brand).
- `FolderLabelsTest`: `providerLabel` via the googlemail alias, a manually-configured Office 365 host without OAuth, and the over-match guard (#69); resolver formats via supplied provider/parent/path patterns (#68); early-return returns the same `baseLabels` instance, fails fast on a missing base label, and is deterministic for equal inputs (#67).
## For the maintainer
`MailProvider` gained a host→brand concern (`brandFor`) alongside the app-password presets — kept there (vs. a new brand enum) to avoid duplicating the preset hosts. Outlook host matching is intentionally narrow (`*.office365.com`, `outlook.office.com`); personal outlook.com/hotmail.com accounts are OAuth and matched by auth type. Scope was held to the resolver / providerLabel / FolderDrawer / strings.xml (+ the `MailProvider` registry #69 targets); no DB/entity/schema changes, and #66 (persist IMAP delimiter) was left out as requested.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
Three code-quality cleanups from the PR #54 review, all in the folder-label resolution / provider-label code, so they ship as one change (bundling avoids the collisions three separate PRs would cause). Each was adapted to the current code, which changed after #108 (shared
resolvedFolderLabels+providerLabelFor) and #117 (unified attribute→role table) merged.Closes #67, #68, #69
#69 — providerLabel: consolidate provider-brand host matching
Host→brand knowledge now lives in one place,
MailProvider:forImapHostmatches an entry'simapHostplus newhostAliases— Gmail gains its legacyimap.googlemail.comhost (fixes the under-match that rendered "Drafts - gmail.com").brandFor(account)is the single host→brand seam. It recognizes Outlook by OAuth auth type or a precise Microsoft host (*.office365.com/outlook.office.com) — no longer any host merely containing "outlook"/"office365" (fixes the over-match).MailProviderstays the app-password preset registry; Outlook is deliberately not an entry (it uses interactive OAuth, andentriesdrives the app-password picker), so brand recognition is added in the companion rather than as a preset.providerLabel()drops its ad-hoc host substrings and just callsMailProvider.brandFor(account), falling back to the email domain.#68 — i18n: move folder-label disambiguation patterns into strings.xml
The hardcoded English grammars (
"base - provider","base (parent)","base [path]") becomefolder_label_with_provider/_with_parent/_with_pathstring resources. They are threaded into the pure resolver as aLabelPatternsbundle whose defaults match the old literals, soresolveDrawerLabelsstays pure and JVM-testable; the@Composablecall site resolves the localized strings and passes them down. Translators can now see and reorder/re-punctuate these.#67 — FolderDrawer: memoize resolution, resolver early-return, fail-fast lookups
resolvedFolderLabelshoists the role→string and patternstringResourcelookups out of aremember(folders, accounts, roleLabels, patterns)calculation, so the resolved map is rebuilt only when those inputs change — not on every recomposition of a caller that composes while idle (the closed navigation drawer). The stable map instance also lets strong-skipping reuse unchanged row label lambdas.resolveDrawerLabelsreturnsbaseLabelsunchanged in the dominant no-collision case instead of rebuilding a value-identical map.?:fallbacks (in the resolver and the drawer row) now usegetValue, so a key miss fails loudly instead of silently reverting to un-deduplicated labels.Behavior unchanged
This is pure cleanup — same outputs. The existing assertions guard it:
FolderLabelsTest(JVM) — the provider-suffix, nested-parent, top-level tie-break, and safety-net cases from #60/#61/#64/#108 all stay green (they call the resolver with default patterns).FolderDrawerTest(androidTest) — "Drafts - Gmail" / "Drafts - Outlook" rendering and the account-switch-gap suffix still pass (compiled here; runs in CI E2E).Tests added
MailProviderTest:forImapHostmatches primary hosts + the googlemail alias (case-insensitive);matchesHostaccepts aliases but not unrelated hosts;brandForcentralizes host→brand incl. Outlook, with an over-match guard (outlook.example.com→ no brand).FolderLabelsTest:providerLabelvia the googlemail alias, a manually-configured Office 365 host without OAuth, and the over-match guard (#69); resolver formats via supplied provider/parent/path patterns (#68); early-return returns the samebaseLabelsinstance, fails fast on a missing base label, and is deterministic for equal inputs (#67).For the maintainer
MailProvidergained a host→brand concern (brandFor) alongside the app-password presets — kept there (vs. a new brand enum) to avoid duplicating the preset hosts. Outlook host matching is intentionally narrow (*.office365.com,outlook.office.com); personal outlook.com/hotmail.com accounts are OAuth and matched by auth type. Scope was held to the resolver / providerLabel / FolderDrawer / strings.xml (+ theMailProviderregistry #69 targets); no DB/entity/schema changes, and #66 (persist IMAP delimiter) was left out as requested.🤖 Generated with Claude Code