Merge branch 'feat/legacy-protocols-notice' into 'main'

Security says why a mail app won't connect, when legacy protocols are off

See merge request inbuxa/ihasmail-inbuxa!4
This commit was merged in pull request #4.
This commit is contained in:
2026-09-21 13:50:32 -07:00
12 changed files with 61 additions and 1 deletions
@@ -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<string, unknown> | undefined) =>
({
accounts: { a: { name: "[email protected]", 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);
});
});
+15
View File
@@ -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,
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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": "シークレット",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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": "Секрет",
+1
View File
@@ -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": "Секрет",
+1
View File
@@ -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": "密钥",
+8 -1
View File
@@ -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<string | null>(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() {
)}
<h2>{t("App passwords")}</h2>
{legacyProtocolsOff(session, accountId) && (
<p className="info-box mb-16">
{t("Your organization allows only {app} and JMAP apps, so phone and desktop mail apps can't connect to this account.", { app: appName })}
</p>
)}
{unsupported ? (
<p className="hint">{t("App passwords are managed by your mail administrator.")}</p>
) : (