diff --git a/web/src/jmap/__tests__/legacy-partly-off.test.ts b/web/src/jmap/__tests__/legacy-partly-off.test.ts new file mode 100644 index 0000000..0fca462 --- /dev/null +++ b/web/src/jmap/__tests__/legacy-partly-off.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { INBUXA_CAP, legacyProtocolsPartlyOff } from "../client"; +import { parseTenantLegacy } from "@/lib/admin/adminLegacyProtocols"; +import type { JmapSession } from "../types"; + +/** + * INBUXA can switch IMAP, POP3 and ManageSieve off one at a time. The session + * lists what is still allowed (`legacyAllowed`); the webmail names what isn't, + * only while some but not all are off, and never from a server that doesn't + * say. + */ +describe("legacyProtocolsPartlyOff", () => { + const session = (cap: Record | undefined) => + ({ + accounts: { a: { name: "u@example.org", accountCapabilities: cap ? { [INBUXA_CAP]: cap } : {} } }, + }) as unknown as JmapSession; + + it("names what is off when only some are", () => { + const s = session({ legacyProtocols: "enabled", legacyAllowed: ["imap", "manageSieve", "submission"] }); + expect(legacyProtocolsPartlyOff(s, "a")).toEqual(["POP3"]); + const two = session({ legacyProtocols: "enabled", legacyAllowed: ["pop3", "submission"] }); + expect(legacyProtocolsPartlyOff(two, "a")).toEqual(["IMAP", "ManageSieve"]); + }); + + it("is empty with none off, all off, or an older server", () => { + const all = ["imap", "pop3", "manageSieve", "submission"]; + expect(legacyProtocolsPartlyOff(session({ legacyProtocols: "enabled", legacyAllowed: all }), "a")).toEqual([]); + expect(legacyProtocolsPartlyOff(session({ legacyProtocols: "disabled", legacyAllowed: [] }), "a")).toEqual([]); + expect(legacyProtocolsPartlyOff(session({ legacyProtocols: "enabled" }), "a")).toEqual([]); + expect(legacyProtocolsPartlyOff(null, "a")).toEqual([]); + }); +}); + +describe("parseTenantLegacy", () => { + it("reads a tenant with only some off, and an older server's one switch", () => { + expect(parseTenantLegacy({ legacyProtocols: "enabled", pop3: "disabled" }).partlyOff).toEqual(["POP3"]); + const all = parseTenantLegacy({ legacyProtocols: "disabled", imap: "disabled", pop3: "disabled" }); + expect(all.off).toBe(true); + expect(all.partlyOff).toEqual([]); + expect(parseTenantLegacy({ legacyProtocols: "enabled" }).partlyOff).toEqual([]); + }); +}); diff --git a/web/src/jmap/client.ts b/web/src/jmap/client.ts index 7427f9c..0816147 100644 --- a/web/src/jmap/client.ts +++ b/web/src/jmap/client.ts @@ -37,6 +37,24 @@ export function legacyProtocolsOff(session: JmapSession | null, accountId: Id | return cap?.legacyProtocols === "disabled"; } +/** The protocols the server can switch off one at a time, as it names them. */ +const SWITCHED = ["imap", "pop3", "manageSieve"] as const; +const PROTOCOL_NAMES: Record = { imap: "IMAP", pop3: "POP3", manageSieve: "ManageSieve" }; + +/** + * Which of IMAP, POP3 and ManageSieve are off for this account, by name, when + * only some are (INBUXA legacy-protocols, one switch per protocol). Empty when + * none are, when all are (see `legacyProtocolsOff`), and from a server that + * doesn't say which (`legacyAllowed`). + */ +export function legacyProtocolsPartlyOff(session: JmapSession | null, accountId: Id | null): string[] { + if (!session || !accountId || legacyProtocolsOff(session, accountId)) return []; + const cap = session.accounts[accountId]?.accountCapabilities?.[INBUXA_CAP] as { legacyAllowed?: unknown } | undefined; + if (!Array.isArray(cap?.legacyAllowed)) return []; + const allowed = cap.legacyAllowed; + return SWITCHED.filter((p) => !allowed.includes(p)).map((p) => PROTOCOL_NAMES[p] ?? p); +} + export class JmapMethodError extends Error { constructor( public readonly method: string, diff --git a/web/src/lib/admin/__tests__/adminLegacyProtocols.test.ts b/web/src/lib/admin/__tests__/adminLegacyProtocols.test.ts index 8dfe02e..f63ba47 100644 --- a/web/src/lib/admin/__tests__/adminLegacyProtocols.test.ts +++ b/web/src/lib/admin/__tests__/adminLegacyProtocols.test.ts @@ -3,8 +3,8 @@ import { CONFIRM_PHRASE, impactEntries, parseTenantLegacy, phraseMatches } from describe("a tenant's legacy mail protocols switch, as the server sends it", () => { it("reads the switch, and tells an older server from nobody", () => { - expect(parseTenantLegacy({ legacyProtocols: "disabled", recentLegacyUse: [] })).toEqual({ off: true, recent: [] }); - expect(parseTenantLegacy({ legacyProtocols: "enabled" })).toEqual({ off: false, recent: null }); + expect(parseTenantLegacy({ legacyProtocols: "disabled", recentLegacyUse: [] })).toEqual({ off: true, partlyOff: [], recent: [] }); + expect(parseTenantLegacy({ legacyProtocols: "enabled" })).toEqual({ off: false, partlyOff: [], recent: null }); }); it("puts each account on the panel once, with every protocol and its latest use", () => { diff --git a/web/src/lib/admin/adminLegacyProtocols.ts b/web/src/lib/admin/adminLegacyProtocols.ts index dfa03eb..e534dfa 100644 --- a/web/src/lib/admin/adminLegacyProtocols.ts +++ b/web/src/lib/admin/adminLegacyProtocols.ts @@ -34,7 +34,10 @@ export interface RecentUse { } export interface TenantLegacy { + /** All of IMAP, POP3 and ManageSieve off: the kill-all's state. */ off: boolean; + /** When only some are off, which, by name; set one at a time in the console. */ + partlyOff: string[]; /** Null from a server too old to say who uses legacy apps -- not the same as nobody. */ recent: RecentUse[] | null; } @@ -49,8 +52,13 @@ function parseRecent(raw: unknown): RecentUse[] | null { }); } +const PROTOCOL_NAMES: [string, string][] = [["imap", "IMAP"], ["pop3", "POP3"], ["manageSieve", "ManageSieve"]]; + export function parseTenantLegacy(raw: Record): TenantLegacy { - return { off: raw.legacyProtocols === "disabled", recent: parseRecent(raw.recentLegacyUse) }; + const off = raw.legacyProtocols === "disabled"; + // An older server sends only legacyProtocols, which stands for all three. + const partlyOff = off ? [] : PROTOCOL_NAMES.filter(([key]) => raw[key] === "disabled").map(([, name]) => name); + return { off, partlyOff, recent: parseRecent(raw.recentLegacyUse) }; } /** The tenant's switch, or null where the server has none (not INBUXA, or too old). */ diff --git a/web/src/locales/de.ts b/web/src/locales/de.ts index 5ec180b..a42fc20 100644 --- a/web/src/locales/de.ts +++ b/web/src/locales/de.ts @@ -1754,6 +1754,10 @@ export const catalog: Catalog = { // ── Spam filter: the language model's opinion (inbuxa) ────────── "Language model's opinion": "Einschätzung des Sprachmodells", "One of several signals the spam filter weighed": "Eines von mehreren Signalen, die der Spamfilter berücksichtigt hat", + // inbuxa: legacy mail protocols, one switch per protocol + "Your organization has turned off {protocols} for mail apps. Mail apps that use it can't connect to this account; others still can.": "Ihre Organisation hat {protocols} für Mail-Apps ausgeschaltet. Mail-Apps, die das nutzen, können sich nicht mit diesem Konto verbinden; andere schon.", + "Some legacy mail protocols are off for your organization: {protocols}.": "Einige ältere Mailprotokolle sind für Ihre Organisation ausgeschaltet: {protocols}.", + "Some are off for {tenant}: {protocols}. Switch them one at a time in the administration console.": "Einige sind für {tenant} aus: {protocols}. In der Verwaltungskonsole lassen sie sich einzeln ein- und ausschalten.", // inbuxa: deleting a person is the console's (audit-hold-lock spec) "Deleting, locking and legal holds are done in the administration console, which records why and keeps what a hold covers.": "Löschen, Sperren und rechtliche Aufbewahrungspflichten werden in der Verwaltungskonsole erledigt, die den Grund festhält und bewahrt, was eine Aufbewahrungspflicht umfasst.", "Open in the console": "In der Konsole öffnen", diff --git a/web/src/locales/es.ts b/web/src/locales/es.ts index 96da847..8b97110 100644 --- a/web/src/locales/es.ts +++ b/web/src/locales/es.ts @@ -1727,6 +1727,10 @@ export const catalog: Catalog = { // ── Spam filter: the language model's opinion (inbuxa) ────────── "Language model's opinion": "Opinión del modelo de lenguaje", "One of several signals the spam filter weighed": "Una de varias señales que el filtro de spam ha tenido en cuenta", + // inbuxa: legacy mail protocols, one switch per protocol + "Your organization has turned off {protocols} for mail apps. Mail apps that use it can't connect to this account; others still can.": "Su organización ha desactivado {protocols} para las aplicaciones de correo. Las que lo usan no pueden conectarse a esta cuenta; las demás sí.", + "Some legacy mail protocols are off for your organization: {protocols}.": "Algunos protocolos de correo heredados están desactivados para su organización: {protocols}.", + "Some are off for {tenant}: {protocols}. Switch them one at a time in the administration console.": "Algunos están desactivados para {tenant}: {protocols}. Actívelos o desactívelos uno a uno en la consola de administración.", // inbuxa: deleting a person is the console's (audit-hold-lock spec) "Deleting, locking and legal holds are done in the administration console, which records why and keeps what a hold covers.": "Eliminar, bloquear y las retenciones legales se gestionan en la consola de administración, que registra el motivo y conserva lo que cubre una retención.", "Open in the console": "Abrir en la consola", diff --git a/web/src/locales/fr.ts b/web/src/locales/fr.ts index bebce11..4603980 100644 --- a/web/src/locales/fr.ts +++ b/web/src/locales/fr.ts @@ -1732,6 +1732,10 @@ export const catalog: Catalog = { // ── Spam filter: the language model's opinion (inbuxa) ────────── "Language model's opinion": "Avis du modèle de langage", "One of several signals the spam filter weighed": "Un signal parmi d'autres pris en compte par le filtre antispam", + // inbuxa: legacy mail protocols, one switch per protocol + "Your organization has turned off {protocols} for mail apps. Mail apps that use it can't connect to this account; others still can.": "Votre organisation a désactivé {protocols} pour les applications de messagerie. Celles qui l’utilisent ne peuvent pas se connecter à ce compte ; les autres le peuvent toujours.", + "Some legacy mail protocols are off for your organization: {protocols}.": "Certains protocoles de messagerie historiques sont désactivés pour votre organisation : {protocols}.", + "Some are off for {tenant}: {protocols}. Switch them one at a time in the administration console.": "Certains sont désactivés pour {tenant} : {protocols}. Activez-les ou désactivez-les un par un dans la console d’administration.", // inbuxa: deleting a person is the console's (audit-hold-lock spec) "Deleting, locking and legal holds are done in the administration console, which records why and keeps what a hold covers.": "La suppression, le verrouillage et les conservations légales se font dans la console d’administration, qui enregistre le motif et conserve ce qu’une conservation couvre.", "Open in the console": "Ouvrir dans la console", diff --git a/web/src/locales/ja.ts b/web/src/locales/ja.ts index 837347b..fa5965b 100644 --- a/web/src/locales/ja.ts +++ b/web/src/locales/ja.ts @@ -1735,6 +1735,10 @@ export const catalog: Catalog = { // ── Spam filter: the language model's opinion (inbuxa) ────────── "Language model's opinion": "言語モデルの見解", "One of several signals the spam filter weighed": "迷惑メールフィルターが考慮した複数の判断材料のひとつ", + // inbuxa: legacy mail protocols, one switch per protocol + "Your organization has turned off {protocols} for mail apps. Mail apps that use it can't connect to this account; others still can.": "組織ではメールアプリ向けの {protocols} がオフになっています。これを使うメールアプリはこのアカウントに接続できませんが、ほかのアプリは接続できます。", + "Some legacy mail protocols are off for your organization: {protocols}.": "組織では一部の従来のメールプロトコルがオフになっています: {protocols}。", + "Some are off for {tenant}: {protocols}. Switch them one at a time in the administration console.": "{tenant} では一部がオフです: {protocols}。管理コンソールで 1 つずつ切り替えてください。", // inbuxa: deleting a person is the console's (audit-hold-lock spec) "Deleting, locking and legal holds are done in the administration console, which records why and keeps what a hold covers.": "削除、ロック、訴訟ホールドは管理コンソールで行います。コンソールでは理由が記録され、ホールドの対象は保持されます。", "Open in the console": "コンソールで開く", diff --git a/web/src/locales/nl.ts b/web/src/locales/nl.ts index 79645a8..ad54aba 100644 --- a/web/src/locales/nl.ts +++ b/web/src/locales/nl.ts @@ -1727,6 +1727,10 @@ export const catalog: Catalog = { // ── Spam filter: the language model's opinion (inbuxa) ────────── "Language model's opinion": "Oordeel van het taalmodel", "One of several signals the spam filter weighed": "Een van meerdere signalen die het spamfilter heeft meegewogen", + // inbuxa: legacy mail protocols, one switch per protocol + "Your organization has turned off {protocols} for mail apps. Mail apps that use it can't connect to this account; others still can.": "Uw organisatie heeft {protocols} uitgeschakeld voor mail-apps. Mail-apps die dat gebruiken, kunnen geen verbinding maken met dit account; andere nog wel.", + "Some legacy mail protocols are off for your organization: {protocols}.": "Sommige verouderde mailprotocollen zijn uitgeschakeld voor uw organisatie: {protocols}.", + "Some are off for {tenant}: {protocols}. Switch them one at a time in the administration console.": "Sommige zijn uit voor {tenant}: {protocols}. In de beheerconsole zijn ze één voor één aan en uit te zetten.", // inbuxa: deleting a person is the console's (audit-hold-lock spec) "Deleting, locking and legal holds are done in the administration console, which records why and keeps what a hold covers.": "Verwijderen, vergrendelen en juridische bewaarplichten gebeuren in de beheerconsole, die de reden vastlegt en bewaart wat onder een bewaarplicht valt.", "Open in the console": "Openen in de console", diff --git a/web/src/locales/pt-BR.ts b/web/src/locales/pt-BR.ts index 5308219..c781ce3 100644 --- a/web/src/locales/pt-BR.ts +++ b/web/src/locales/pt-BR.ts @@ -1730,6 +1730,10 @@ export const catalog: Catalog = { // ── Spam filter: the language model's opinion (inbuxa) ────────── "Language model's opinion": "Opinião do modelo de linguagem", "One of several signals the spam filter weighed": "Um dos vários sinais considerados pelo filtro de spam", + // inbuxa: legacy mail protocols, one switch per protocol + "Your organization has turned off {protocols} for mail apps. Mail apps that use it can't connect to this account; others still can.": "Sua organização desativou {protocols} para aplicativos de e-mail. Os que usam isso não conseguem se conectar a esta conta; os outros ainda conseguem.", + "Some legacy mail protocols are off for your organization: {protocols}.": "Alguns protocolos de e-mail legados estão desativados para sua organização: {protocols}.", + "Some are off for {tenant}: {protocols}. Switch them one at a time in the administration console.": "Alguns estão desativados para {tenant}: {protocols}. Ative ou desative um de cada vez no console de administração.", // inbuxa: deleting a person is the console's (audit-hold-lock spec) "Deleting, locking and legal holds are done in the administration console, which records why and keeps what a hold covers.": "Excluir, bloquear e retenções legais são feitos no console de administração, que registra o motivo e preserva o que uma retenção abrange.", "Open in the console": "Abrir no console", diff --git a/web/src/locales/ru.ts b/web/src/locales/ru.ts index fda1c8a..429d556 100644 --- a/web/src/locales/ru.ts +++ b/web/src/locales/ru.ts @@ -1729,6 +1729,10 @@ export const catalog: Catalog = { // ── Spam filter: the language model's opinion (inbuxa) ────────── "Language model's opinion": "Мнение языковой модели", "One of several signals the spam filter weighed": "Один из нескольких признаков, которые учёл спам-фильтр", + // inbuxa: legacy mail protocols, one switch per protocol + "Your organization has turned off {protocols} for mail apps. Mail apps that use it can't connect to this account; others still can.": "Ваша организация отключила {protocols} для почтовых приложений. Приложения, которые его используют, не могут подключиться к этому аккаунту; остальные могут.", + "Some legacy mail protocols are off for your organization: {protocols}.": "Некоторые устаревшие почтовые протоколы отключены для вашей организации: {protocols}.", + "Some are off for {tenant}: {protocols}. Switch them one at a time in the administration console.": "Для {tenant} отключены некоторые: {protocols}. Включайте и отключайте их по одному в консоли администрирования.", // inbuxa: deleting a person is the console's (audit-hold-lock spec) "Deleting, locking and legal holds are done in the administration console, which records why and keeps what a hold covers.": "Удаление, блокировка и юридическое удержание выполняются в консоли администрирования: она записывает причину и сохраняет всё, что охватывает удержание.", "Open in the console": "Открыть в консоли", diff --git a/web/src/locales/uk.ts b/web/src/locales/uk.ts index 0e30882..5bc34fb 100644 --- a/web/src/locales/uk.ts +++ b/web/src/locales/uk.ts @@ -1723,6 +1723,10 @@ export const catalog: Catalog = { // ── Spam filter: the language model's opinion (inbuxa) ────────── "Language model's opinion": "Думка мовної моделі", "One of several signals the spam filter weighed": "Одна з кількох ознак, які врахував спам-фільтр", + // inbuxa: legacy mail protocols, one switch per protocol + "Your organization has turned off {protocols} for mail apps. Mail apps that use it can't connect to this account; others still can.": "Ваша організація вимкнула {protocols} для поштових програм. Програми, які його використовують, не можуть підключитися до цього облікового запису; інші можуть.", + "Some legacy mail protocols are off for your organization: {protocols}.": "Деякі застарілі поштові протоколи вимкнено для вашої організації: {protocols}.", + "Some are off for {tenant}: {protocols}. Switch them one at a time in the administration console.": "Для {tenant} вимкнено деякі: {protocols}. Вмикайте й вимикайте їх по одному в консолі адміністрування.", // inbuxa: deleting a person is the console's (audit-hold-lock spec) "Deleting, locking and legal holds are done in the administration console, which records why and keeps what a hold covers.": "Видалення, блокування та юридичне утримання виконуються в консолі адміністрування: вона записує причину й зберігає все, що охоплює утримання.", "Open in the console": "Відкрити в консолі", diff --git a/web/src/locales/zh-Hans.ts b/web/src/locales/zh-Hans.ts index 8ca8a54..d4b9917 100644 --- a/web/src/locales/zh-Hans.ts +++ b/web/src/locales/zh-Hans.ts @@ -1734,6 +1734,10 @@ export const catalog: Catalog = { // ── Spam filter: the language model's opinion (inbuxa) ────────── "Language model's opinion": "语言模型的判断", "One of several signals the spam filter weighed": "垃圾邮件过滤考虑的多个信号之一", + // inbuxa: legacy mail protocols, one switch per protocol + "Your organization has turned off {protocols} for mail apps. Mail apps that use it can't connect to this account; others still can.": "您的组织已为邮件应用关闭 {protocols}。使用它的邮件应用无法连接到此账户;其他应用仍可连接。", + "Some legacy mail protocols are off for your organization: {protocols}.": "您的组织已关闭部分传统邮件协议:{protocols}。", + "Some are off for {tenant}: {protocols}. Switch them one at a time in the administration console.": "{tenant} 已关闭部分协议:{protocols}。请在管理控制台中逐个开启或关闭。", // inbuxa: deleting a person is the console's (audit-hold-lock spec) "Deleting, locking and legal holds are done in the administration console, which records why and keeps what a hold covers.": "删除、锁定和法律保留均在管理控制台中进行,控制台会记录原因,并保留保留范围内的内容。", "Open in the console": "在控制台中打开", diff --git a/web/src/views/admin/AdminDashboard.tsx b/web/src/views/admin/AdminDashboard.tsx index 3d48dc9..b572972 100644 --- a/web/src/views/admin/AdminDashboard.tsx +++ b/web/src/views/admin/AdminDashboard.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, type ReactNode } from "react"; import { Link } from "wouter"; import { ArrowDownToLine, ArrowUpFromLine, ExternalLink, Globe, Hourglass, LayoutDashboard, MemoryStick, RefreshCw, ShieldCheck, Users } from "lucide-react"; -import { legacyProtocolsOff } from "@/jmap/client"; +import { legacyProtocolsOff, legacyProtocolsPartlyOff } from "@/jmap/client"; import { useAppName } from "@/lib/brand"; import { adminSections, dashboardCards, type DashboardCard } from "@/lib/admin/adminAccess"; import { balancedColumns, countObjects, DASHBOARD_WINDOW_MS, isRefused, loadMetrics, summarizeMetrics, type MessageStats } from "@/lib/admin/adminDashboard"; @@ -38,6 +38,7 @@ export function AdminDashboard() { const perms = usePermissions(); const adminUrl = useSession((s) => s.session?.ihasmail?.server?.adminUrl ?? null); const legacyOff = useSession((s) => legacyProtocolsOff(s.session, s.accountId)); + const legacyPartlyOff = useSession((s) => legacyProtocolsPartlyOff(s.session, s.accountId).join(", ")); const app = useAppName(); const cards = dashboardCards(perms); const sections = adminSections(perms); @@ -113,6 +114,13 @@ export function AdminDashboard() { {t("Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.", { app })}

)} + {legacyPartlyOff && ( + // INBUXA: one switch per protocol, some of them off +

+ + {t("Some legacy mail protocols are off for your organization: {protocols}.", { protocols: legacyPartlyOff })} +

+ )} {shown.length ? (
diff --git a/web/src/views/admin/TenantLegacyProtocols.tsx b/web/src/views/admin/TenantLegacyProtocols.tsx index e87e4f6..9fe4547 100644 --- a/web/src/views/admin/TenantLegacyProtocols.tsx +++ b/web/src/views/admin/TenantLegacyProtocols.tsx @@ -68,10 +68,15 @@ export function TenantLegacyProtocols({ tenantId, tenantName }: { tenantId: stri

{state.off ? t("Off for {tenant}. Only {app} and JMAP apps can sign in to its domains.", { tenant: tenantName, app }) - : t("On for {tenant}. Mail apps can use IMAP, POP3 and ManageSieve on its domains.", { tenant: tenantName })} + : state.partlyOff.length > 0 + ? t("Some are off for {tenant}: {protocols}. Switch them one at a time in the administration console.", { + tenant: tenantName, + protocols: state.partlyOff.join(", "), + }) + : t("On for {tenant}. Mail apps can use IMAP, POP3 and ManageSieve on its domains.", { tenant: tenantName })}

- {canChange && state.off && ( + {canChange && (state.off || state.partlyOff.length > 0) && ( diff --git a/web/src/views/admin/__tests__/tenant-legacy-protocols.test.tsx b/web/src/views/admin/__tests__/tenant-legacy-protocols.test.tsx index 29cf605..2a2c701 100644 --- a/web/src/views/admin/__tests__/tenant-legacy-protocols.test.tsx +++ b/web/src/views/admin/__tests__/tenant-legacy-protocols.test.tsx @@ -62,7 +62,7 @@ describe("a tenant's legacy mail protocols switch", () => { }); it("shows who would notice and what it means, then asks for the exact phrase", async () => { - api.state = { off: false, recent: [{ accountId: "a", name: "maria@acme.example", protocol: "imap", lastUsedAt: Date.now() - 2 * 86400_000 }] }; + api.state = { off: false, partlyOff: [], recent: [{ accountId: "a", name: "maria@acme.example", protocol: "imap", lastUsedAt: Date.now() - 2 * 86400_000 }] }; signIn(["sysDomainGet", "sysDomainUpdate"]); await render(); await act(async () => button(host, "Turn off legacy protocols…")!.click()); @@ -84,7 +84,7 @@ describe("a tenant's legacy mail protocols switch", () => { }); it("turns it back on with one click", async () => { - api.state = { off: true, recent: [] }; + api.state = { off: true, partlyOff: [], recent: [] }; signIn(["sysDomainGet", "sysDomainUpdate"]); await render(); await act(async () => button(host, "Turn legacy protocols back on")!.click()); @@ -92,7 +92,7 @@ describe("a tenant's legacy mail protocols switch", () => { }); it("shows the state but no switch to someone who can't change domains", async () => { - api.state = { off: true, recent: null }; + api.state = { off: true, partlyOff: [], recent: null }; signIn(["sysDomainGet"]); await render(); expect(host.textContent).toContain("Off for Acme Corp"); diff --git a/web/src/views/settings/SecuritySettings.tsx b/web/src/views/settings/SecuritySettings.tsx index 17d0acc..940a71e 100644 --- a/web/src/views/settings/SecuritySettings.tsx +++ b/web/src/views/settings/SecuritySettings.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { useAppName } from "@/lib/brand"; import { Copy, KeyRound, ShieldCheck, Smartphone } from "lucide-react"; -import { apiFetch, ApiError, legacyProtocolsOff } from "@/jmap/client"; +import { apiFetch, ApiError, legacyProtocolsOff, legacyProtocolsPartlyOff } from "@/jmap/client"; import { useSession } from "@/store/session"; import { formatFullDate } from "@/lib/format"; import { toast } from "@/ui/toast"; @@ -89,6 +89,14 @@ export function SecuritySettings() { {t("Your organization allows only {app} and JMAP apps, so phone and desktop mail apps can't connect to this account.", { app: appName })}

)} + {legacyProtocolsPartlyOff(session, accountId).length > 0 && ( + // INBUXA: one switch per protocol; mail apps using the rest still work +

+ {t("Your organization has turned off {protocols} for mail apps. Mail apps that use it can't connect to this account; others still can.", { + protocols: legacyProtocolsPartlyOff(session, accountId).join(", "), + })} +

+ )} {unsupported ? (

{t("App passwords are managed by your mail administrator.")}

) : (