From 4cac9088dd44a3ad0f9fe1986ea63a0600950987 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Mon, 21 Sep 2026 13:44:50 -0700 Subject: [PATCH] Security says why a mail app won't connect, when legacy protocols are off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the mail server has turned off legacy mail protocols -- IMAP, POP3, ManageSieve and sending from mail apps -- for this account, whether for the whole server or for the account's organization, Settings › Security says so at the top of App passwords, the section people come to when a phone won't connect: Your organization allows only {app} and JMAP apps, so phone and desktop mail apps can't connect to this account. This is INBUXA's legacy-protocols LP-19. The server reports it per account as legacyProtocols on the urn:inbuxa:jmap account capability (contract C-1); anything short of a plain "disabled" -- an older server, another server, no session yet -- reads as on, so the line never appears where it isn't true. The app's name comes from {app}, as everywhere else. Translated into all nine languages, in the register each catalog already uses. --- .../jmap/__tests__/legacy-protocols.test.ts | 29 +++++++++++++++++++ web/src/jmap/client.ts | 15 ++++++++++ web/src/locales/de.ts | 1 + web/src/locales/es.ts | 1 + web/src/locales/fr.ts | 1 + web/src/locales/ja.ts | 1 + web/src/locales/nl.ts | 1 + web/src/locales/pt-BR.ts | 1 + web/src/locales/ru.ts | 1 + web/src/locales/uk.ts | 1 + web/src/locales/zh-Hans.ts | 1 + web/src/views/settings/SecuritySettings.tsx | 9 +++++- 12 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 web/src/jmap/__tests__/legacy-protocols.test.ts diff --git a/web/src/jmap/__tests__/legacy-protocols.test.ts b/web/src/jmap/__tests__/legacy-protocols.test.ts new file mode 100644 index 0000000..3165680 --- /dev/null +++ b/web/src/jmap/__tests__/legacy-protocols.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { INBUXA_CAP, legacyProtocolsOff } from "../client"; +import type { JmapSession } from "../types"; + +/** + * The server says, per account, whether legacy mail protocols are off for it + * (legacy-protocols LP-19, contract C-1). Anything short of a plain + * "disabled" -- an older server, another server, no session yet -- reads as + * on, so the notice never appears where it isn't true. + */ +describe("legacyProtocolsOff", () => { + const session = (cap: Record | undefined) => + ({ + accounts: { a: { name: "u@example.org", accountCapabilities: cap ? { [INBUXA_CAP]: cap } : {} } }, + }) as unknown as JmapSession; + + it("is on only when the account's capability says disabled", () => { + expect(legacyProtocolsOff(session({ legacyProtocols: "disabled" }), "a")).toBe(true); + expect(legacyProtocolsOff(session({ legacyProtocols: "enabled" }), "a")).toBe(false); + }); + + it("reads an older or other server, or no session, as on", () => { + expect(legacyProtocolsOff(session({ logo: null }), "a")).toBe(false); + expect(legacyProtocolsOff(session(undefined), "a")).toBe(false); + expect(legacyProtocolsOff(session({ legacyProtocols: "disabled" }), "b")).toBe(false); + expect(legacyProtocolsOff(null, "a")).toBe(false); + expect(legacyProtocolsOff(session({ legacyProtocols: "disabled" }), null)).toBe(false); + }); +}); diff --git a/web/src/jmap/client.ts b/web/src/jmap/client.ts index 2649a8f..3d9abf9 100644 --- a/web/src/jmap/client.ts +++ b/web/src/jmap/client.ts @@ -22,6 +22,21 @@ export const CAP = { /** Stalwart's own capability, which carries its `x:` registry methods. */ export const STALWART_CAP = "urn:stalwart:jmap"; +/** INBUXA's own capability (contract C-1), on the signed-in account. */ +export const INBUXA_CAP = "urn:inbuxa:jmap"; + +/** + * Whether legacy mail protocols -- IMAP, POP3, ManageSieve, sending from mail + * apps -- are off for this account: the stricter of the server's switch and + * its organization's, as the server reports it (legacy-protocols LP-19). Any + * server that doesn't say reads as on. + */ +export function legacyProtocolsOff(session: JmapSession | null, accountId: Id | null): boolean { + if (!session || !accountId) return false; + const cap = session.accounts[accountId]?.accountCapabilities?.[INBUXA_CAP] as { legacyProtocols?: string } | undefined; + return cap?.legacyProtocols === "disabled"; +} + export class JmapMethodError extends Error { constructor( public readonly method: string, diff --git a/web/src/locales/de.ts b/web/src/locales/de.ts index 72b772a..2112cc9 100644 --- a/web/src/locales/de.ts +++ b/web/src/locales/de.ts @@ -924,6 +924,7 @@ export const catalog: Catalog = { "Turn off two-factor authentication": "Zwei-Faktor-Authentifizierung deaktivieren", "Your password alone will be enough to sign in again.": "Ihr Passwort allein genügt dann wieder zum Anmelden.", "App passwords": "App-Passwörter", + "Your organization allows only {app} and JMAP apps, so phone and desktop mail apps can't connect to this account.": "Ihre Organisation erlaubt nur {app} und JMAP-Apps. Mail-Apps auf Smartphones und Computern können sich daher nicht mit diesem Konto verbinden.", "New app password for": "Neues App-Passwort für", "Your new app password": "Ihr neues App-Passwort", "Secret": "Geheimnis", diff --git a/web/src/locales/es.ts b/web/src/locales/es.ts index 8674124..a634599 100644 --- a/web/src/locales/es.ts +++ b/web/src/locales/es.ts @@ -930,6 +930,7 @@ export const catalog: Catalog = { "Turn off two-factor authentication": "Desactivar la autenticación en dos pasos", "Your password alone will be enough to sign in again.": "Su contraseña por sí sola bastará para volver a iniciar sesión.", "App passwords": "Contraseñas de aplicación", + "Your organization allows only {app} and JMAP apps, so phone and desktop mail apps can't connect to this account.": "Su organización solo permite {app} y aplicaciones JMAP, así que las aplicaciones de correo del teléfono y del ordenador no pueden conectarse a esta cuenta.", "New app password for": "Contraseña de aplicación nueva para", "Your new app password": "Su contraseña de aplicación nueva", "Secret": "Secreto", diff --git a/web/src/locales/fr.ts b/web/src/locales/fr.ts index 106042b..d90bf78 100644 --- a/web/src/locales/fr.ts +++ b/web/src/locales/fr.ts @@ -936,6 +936,7 @@ export const catalog: Catalog = { "Turn off two-factor authentication": "Désactiver l'authentification à deux facteurs", "Your password alone will be enough to sign in again.": "Votre mot de passe seul suffira pour vous reconnecter.", "App passwords": "Mots de passe d'application", + "Your organization allows only {app} and JMAP apps, so phone and desktop mail apps can't connect to this account.": "Votre organisation n'autorise que {app} et les applications JMAP : les applications de messagerie sur téléphone et ordinateur ne peuvent donc pas se connecter à ce compte.", "New app password for": "Nouveau mot de passe d'application pour", "Your new app password": "Votre nouveau mot de passe d'application", "Secret": "Secret", diff --git a/web/src/locales/ja.ts b/web/src/locales/ja.ts index b070bee..774f97c 100644 --- a/web/src/locales/ja.ts +++ b/web/src/locales/ja.ts @@ -930,6 +930,7 @@ export const catalog: Catalog = { "Turn off two-factor authentication": "2 段階認証をオフにする", "Your password alone will be enough to sign in again.": "以後はパスワードだけでサインインできるようになります。", "App passwords": "アプリパスワード", + "Your organization allows only {app} and JMAP apps, so phone and desktop mail apps can't connect to this account.": "組織では {app} と JMAP アプリのみが許可されているため、スマートフォンやパソコンのメールアプリはこのアカウントに接続できません。", "New app password for": "アプリパスワードの用途", "Your new app password": "新しいアプリパスワード", "Secret": "シークレット", diff --git a/web/src/locales/nl.ts b/web/src/locales/nl.ts index d5d2ddf..733eada 100644 --- a/web/src/locales/nl.ts +++ b/web/src/locales/nl.ts @@ -928,6 +928,7 @@ export const catalog: Catalog = { "Turn off two-factor authentication": "Tweefactorauthenticatie uitschakelen", "Your password alone will be enough to sign in again.": "Uw wachtwoord alleen volstaat dan weer om in te loggen.", "App passwords": "App-wachtwoorden", + "Your organization allows only {app} and JMAP apps, so phone and desktop mail apps can't connect to this account.": "Uw organisatie staat alleen {app} en JMAP-apps toe, dus mail-apps op telefoon en computer kunnen geen verbinding maken met dit account.", "New app password for": "Nieuw app-wachtwoord voor", "Your new app password": "Uw nieuwe app-wachtwoord", "Secret": "Geheim", diff --git a/web/src/locales/pt-BR.ts b/web/src/locales/pt-BR.ts index 7304bf7..6784a28 100644 --- a/web/src/locales/pt-BR.ts +++ b/web/src/locales/pt-BR.ts @@ -933,6 +933,7 @@ export const catalog: Catalog = { "Turn off two-factor authentication": "Desativar a autenticação em duas etapas", "Your password alone will be enough to sign in again.": "Sua senha sozinha bastará para entrar de novo.", "App passwords": "Senhas de aplicativo", + "Your organization allows only {app} and JMAP apps, so phone and desktop mail apps can't connect to this account.": "Sua organização permite apenas {app} e aplicativos JMAP, então aplicativos de e-mail no celular e no computador não conseguem se conectar a esta conta.", "New app password for": "Nova senha de aplicativo para", "Your new app password": "Sua nova senha de aplicativo", "Secret": "Segredo", diff --git a/web/src/locales/ru.ts b/web/src/locales/ru.ts index d8598e6..29dd63c 100644 --- a/web/src/locales/ru.ts +++ b/web/src/locales/ru.ts @@ -933,6 +933,7 @@ export const catalog: Catalog = { "Turn off two-factor authentication": "Отключить двухфакторную аутентификацию", "Your password alone will be enough to sign in again.": "Для входа снова будет достаточно одного пароля.", "App passwords": "Пароли приложений", + "Your organization allows only {app} and JMAP apps, so phone and desktop mail apps can't connect to this account.": "Ваша организация разрешает только {app} и приложения JMAP, поэтому почтовые приложения на телефоне и компьютере не могут подключиться к этой учётной записи.", "New app password for": "Новый пароль приложения для", "Your new app password": "Ваш новый пароль приложения", "Secret": "Секрет", diff --git a/web/src/locales/uk.ts b/web/src/locales/uk.ts index 13f2f6c..32ade90 100644 --- a/web/src/locales/uk.ts +++ b/web/src/locales/uk.ts @@ -927,6 +927,7 @@ export const catalog: Catalog = { "Turn off two-factor authentication": "Вимкнути двофакторну автентифікацію", "Your password alone will be enough to sign in again.": "Для входу знову буде достатньо самого пароля.", "App passwords": "Паролі програм", + "Your organization allows only {app} and JMAP apps, so phone and desktop mail apps can't connect to this account.": "Ваша організація дозволяє лише {app} і програми JMAP, тому поштові програми на телефоні й комп'ютері не можуть підключитися до цього облікового запису.", "New app password for": "Новий пароль програми для", "Your new app password": "Ваш новий пароль програми", "Secret": "Секрет", diff --git a/web/src/locales/zh-Hans.ts b/web/src/locales/zh-Hans.ts index 52db025..e6a91d0 100644 --- a/web/src/locales/zh-Hans.ts +++ b/web/src/locales/zh-Hans.ts @@ -929,6 +929,7 @@ export const catalog: Catalog = { "Turn off two-factor authentication": "关闭两步验证", "Your password alone will be enough to sign in again.": "之后仅凭密码即可重新登录。", "App passwords": "应用专用密码", + "Your organization allows only {app} and JMAP apps, so phone and desktop mail apps can't connect to this account.": "您的组织只允许使用 {app} 和 JMAP 应用,因此手机和电脑上的邮件应用无法连接到此账户。", "New app password for": "新建应用专用密码,用于", "Your new app password": "您的新应用专用密码", "Secret": "密钥", diff --git a/web/src/views/settings/SecuritySettings.tsx b/web/src/views/settings/SecuritySettings.tsx index 8437231..17d0acc 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 } from "@/jmap/client"; +import { apiFetch, ApiError, legacyProtocolsOff } from "@/jmap/client"; import { useSession } from "@/store/session"; import { formatFullDate } from "@/lib/format"; import { toast } from "@/ui/toast"; @@ -39,7 +39,9 @@ export function SecuritySettings() { /** Set when the server has no self-service API at all (a proxy, say). */ const [unsupported, setUnsupported] = useState(null); const session = useSession((s) => s.session); + const accountId = useSession((s) => s.accountId); const logout = useSession((s) => s.logout); + const appName = useAppName(); const load = () => apiFetch<{ current: string; sessions: SessionRow[] }>("/api/auth/sessions").then((r) => { setRows(r.sessions); setCurrent(r.current); }).catch(() => setRows([])); @@ -82,6 +84,11 @@ export function SecuritySettings() { )}

{t("App passwords")}

+ {legacyProtocolsOff(session, accountId) && ( +

+ {t("Your organization allows only {app} and JMAP apps, so phone and desktop mail apps can't connect to this account.", { app: appName })} +

+ )} {unsupported ? (

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

) : (