diff --git a/web/src/lib/admin/__tests__/adminLegacyProtocols.test.ts b/web/src/lib/admin/__tests__/adminLegacyProtocols.test.ts new file mode 100644 index 0000000..8dfe02e --- /dev/null +++ b/web/src/lib/admin/__tests__/adminLegacyProtocols.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { CONFIRM_PHRASE, impactEntries, parseTenantLegacy, phraseMatches } from "../adminLegacyProtocols"; + +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 }); + }); + + it("puts each account on the panel once, with every protocol and its latest use", () => { + const { recent } = parseTenantLegacy({ + recentLegacyUse: [ + { accountId: "a", name: "maria@acme.example", protocol: "submission", lastUsedAt: 100 }, + { accountId: "a", name: "maria@acme.example", protocol: "imap", lastUsedAt: 300 }, + { accountId: "b", name: "ada@acme.example", protocol: "pop3", lastUsedAt: 200 }, + { accountId: "c", name: "broken" }, + ], + }); + expect(impactEntries(recent!)).toEqual([ + { name: "maria@acme.example", protocols: ["IMAP", "SMTP"], lastUsedAt: 300 }, + { name: "ada@acme.example", protocols: ["POP3"], lastUsedAt: 200 }, + ]); + }); + + it("takes only the exact phrase", () => { + expect(phraseMatches(CONFIRM_PHRASE)).toBe(true); + expect(phraseMatches(` ${CONFIRM_PHRASE}`)).toBe(false); + expect(phraseMatches(CONFIRM_PHRASE.toUpperCase())).toBe(false); + }); +}); diff --git a/web/src/lib/admin/adminLegacyProtocols.ts b/web/src/lib/admin/adminLegacyProtocols.ts new file mode 100644 index 0000000..dfa03eb --- /dev/null +++ b/web/src/lib/admin/adminLegacyProtocols.ts @@ -0,0 +1,104 @@ +import { client, INBUXA_CAP } from "@/jmap/client"; + +/** + * One tenant's legacy mail protocols switch: INBUXA's + * `inbuxa:TenantProtocolPolicy` (legacy-protocols LP-9 to LP-18). + * + * Turning it off refuses sign-in over IMAP, POP3, ManageSieve and SMTP + * submission on the tenant's domains, so only this webmail and other JMAP + * apps work there. It closes no port -- other tenants share them. Turning it + * back on is refused by the server while the server has legacy protocols off + * for everyone. + * + * Only INBUXA serves it. On any other server the method is unknown, which + * `fetchTenantLegacy` reports as `null` so the sheet shows nothing. + */ + +const OBJECT = "inbuxa:TenantProtocolPolicy"; + +/** The phrase that turns legacy protocols off (LP-17). Turning them back on needs none. */ +export const CONFIRM_PHRASE = "turn off legacy mail"; + +/** Whether the typed confirmation matches: exactly, no trimming, no case folding. */ +export function phraseMatches(typed: string): boolean { + return typed === CONFIRM_PHRASE; +} + +/** One account's last sign-in over one legacy protocol, as the server reports it (LP-15). */ +export interface RecentUse { + accountId: string; + name: string; + protocol: string; + /** Milliseconds since the epoch. */ + lastUsedAt: number; +} + +export interface TenantLegacy { + off: boolean; + /** Null from a server too old to say who uses legacy apps -- not the same as nobody. */ + recent: RecentUse[] | null; +} + +function parseRecent(raw: unknown): RecentUse[] | null { + if (!Array.isArray(raw)) return null; + return raw.flatMap((entry) => { + if (!entry || typeof entry !== "object") return []; + const r = entry as Record; + if (typeof r.name !== "string" || typeof r.protocol !== "string" || typeof r.lastUsedAt !== "number") return []; + return [{ accountId: typeof r.accountId === "string" ? r.accountId : "", name: r.name, protocol: r.protocol, lastUsedAt: r.lastUsedAt }]; + }); +} + +export function parseTenantLegacy(raw: Record): TenantLegacy { + return { off: raw.legacyProtocols === "disabled", recent: parseRecent(raw.recentLegacyUse) }; +} + +/** The tenant's switch, or null where the server has none (not INBUXA, or too old). */ +export async function fetchTenantLegacy(tenantId: string): Promise { + try { + const res = await client.call<{ list?: Record[] }>(`${OBJECT}/get`, { ids: [tenantId] }, [INBUXA_CAP]); + return res.list?.[0] ? parseTenantLegacy(res.list[0]) : null; + } catch { + return null; + } +} + +/** Turns the tenant's switch. A refusal (LP-9) comes back as the server's words. */ +export async function setTenantLegacy(tenantId: string, off: boolean): Promise { + const res = await client.call<{ notUpdated?: Record | null }>( + `${OBJECT}/set`, + { update: { [tenantId]: { legacyProtocols: off ? "disabled" : "enabled" } } }, + [INBUXA_CAP], + ); + const failed = res.notUpdated?.[tenantId]; + if (failed) throw new Error(failed.description ?? failed.type); +} + +const PROTOCOL_ORDER = ["imap", "pop3", "manageSieve", "submission"]; +const PROTOCOL_LABELS: Record = { imap: "IMAP", pop3: "POP3", manageSieve: "ManageSieve", submission: "SMTP" }; + +/** One account on the impact panel: every protocol it used, and when it last used any. */ +export interface ImpactEntry { + name: string; + protocols: string[]; + lastUsedAt: number; +} + +/** The impact panel's lines (LP-15): one per account, most recent first. */ +export function impactEntries(recent: RecentUse[]): ImpactEntry[] { + const byAccount = new Map; lastUsedAt: number }>(); + for (const use of recent) { + const key = use.accountId || use.name; + const entry = byAccount.get(key) ?? { name: use.name, protocols: new Set(), lastUsedAt: 0 }; + entry.protocols.add(use.protocol); + entry.lastUsedAt = Math.max(entry.lastUsedAt, use.lastUsedAt); + byAccount.set(key, entry); + } + return [...byAccount.values()] + .map((e) => ({ + name: e.name, + protocols: [...e.protocols].sort((a, b) => PROTOCOL_ORDER.indexOf(a) - PROTOCOL_ORDER.indexOf(b)).map((p) => PROTOCOL_LABELS[p] ?? p), + lastUsedAt: e.lastUsedAt, + })) + .sort((a, b) => b.lastUsedAt - a.lastUsedAt || a.name.localeCompare(b.name)); +} diff --git a/web/src/locales/de.ts b/web/src/locales/de.ts index 2112cc9..61fb8a0 100644 --- a/web/src/locales/de.ts +++ b/web/src/locales/de.ts @@ -1721,8 +1721,29 @@ export const catalog: Catalog = { "{app} will tell you if a later message from this address is signed by anybody else.": "{app} weist Sie darauf hin, wenn eine spätere Nachricht von dieser Adresse von jemand anderem signiert ist.", "itself, or an issuer it does not name": "sich selbst, oder einem nicht genannten Aussteller", "no address": "keine Adresse", + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "Legacy mail apps": "Mail-Apps über ältere Protokolle", + "Off for {tenant}. Only {app} and JMAP apps can sign in to its domains.": "Aus für {tenant}. Nur {app} und JMAP-Apps können sich bei den Domains dieser Organisation anmelden.", + "On for {tenant}. Mail apps can use IMAP, POP3 and ManageSieve on its domains.": "An für {tenant}. Mail-Apps können auf den Domains dieser Organisation IMAP, POP3 und ManageSieve nutzen.", + "Turn legacy protocols back on": "Ältere Mailprotokolle wieder einschalten", + "Turn off legacy protocols…": "Ältere Mailprotokolle ausschalten…", + "No account used a legacy mail app in the last 30 days.": "In den letzten 30 Tagen hat kein Konto eine Mail-App mit älteren Protokollen verwendet.", + "Their mail apps will stop working the moment you turn this on:": "Deren Mail-Apps funktionieren nicht mehr, sobald Sie dies einschalten:", + "Only {app} and JMAP apps will work.": "Nur {app} und JMAP-Apps funktionieren dann noch.", + "Legacy mail protocols (IMAP, POP3, ManageSieve and sending from mail apps) will be turned off for everyone in {tenant}.": "Ältere Mailprotokolle (IMAP, POP3, ManageSieve und das Senden aus Mail-Apps) werden für alle in {tenant} ausgeschaltet.", + "Phone and desktop mail apps will stop receiving and sending mail. That's iPhone and iPad Mail, the Gmail and Outlook apps, Outlook, Thunderbird and Apple Mail. People will see sign-in errors in them.": "Mail-Apps auf Smartphones und Computern können keine Mails mehr empfangen und senden. Das betrifft Mail auf iPhone und iPad, die Gmail- und Outlook-Apps, Outlook, Thunderbird und Apple Mail. Dort erscheinen Anmeldefehler.", + "Filters managed from a mail app (ManageSieve) will stop working. Filters set in {app} keep working.": "Filter, die über eine Mail-App verwaltet werden (ManageSieve), funktionieren nicht mehr. In {app} eingerichtete Filter funktionieren weiterhin.", + "Incoming mail is not affected. Calendars and contacts are not affected.": "Eingehende Mails sind nicht betroffen. Kalender und Kontakte sind nicht betroffen.", + "People keep full access through {app}, which can be installed as an app on phones and computers.": "Alle behalten vollen Zugriff über {app}, das sich auf Smartphones und Computern als App installieren lässt.", + "Sending from mail apps (SMTP submission) will stop working, but its ports stay open: mail apps will be told they cannot sign in. Incoming mail (SMTP) and {app} (JMAP) are not affected and cannot be turned off here.": "Das Senden aus Mail-Apps (SMTP-Submission) funktioniert nicht mehr, die Ports bleiben aber offen: Mail-Apps erfahren, dass sie sich nicht anmelden können. Eingehende Mails (SMTP) und {app} (JMAP) sind nicht betroffen und lassen sich hier nicht ausschalten.", + "You can turn legacy protocols back on at any time.": "Sie können ältere Mailprotokolle jederzeit wieder einschalten.", + "To confirm, type {phrase}": "Zur Bestätigung {phrase} eingeben", + "Turn off legacy protocols": "Ältere Mailprotokolle ausschalten", + "Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Ältere Mailprotokolle sind für Ihre Organisation ausgeschaltet. Nur {app} und JMAP-Apps können sich anmelden.", }, plurals: { + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "{n} accounts used a legacy mail app in the last 30 days.": {one: "{n} Konto hat in den letzten 30 Tagen eine Mail-App mit älteren Protokollen verwendet.", other: "{n} Konten haben in den letzten 30 Tagen eine Mail-App mit älteren Protokollen verwendet."}, // ── Administration: domains ──────────────────────────────────── "{n} accounts use this domain. Move or delete them first.": { one: "{n} Konto verwendet diese Domain. Verschieben oder löschen Sie es zuerst.", other: "{n} Konten verwenden diese Domain. Verschieben oder löschen Sie sie zuerst." }, "The server stops accepting mail for this domain, and its {n} DKIM keys are deleted. This can't be undone.": { one: "Der Server nimmt keine E-Mails mehr für diese Domain an, und ihr {n} DKIM-Schlüssel wird gelöscht. Dies kann nicht rückgängig gemacht werden.", other: "Der Server nimmt keine E-Mails mehr für diese Domain an, und ihre {n} DKIM-Schlüssel werden gelöscht. Dies kann nicht rückgängig gemacht werden." }, diff --git a/web/src/locales/es.ts b/web/src/locales/es.ts index a634599..37d35ce 100644 --- a/web/src/locales/es.ts +++ b/web/src/locales/es.ts @@ -1694,8 +1694,29 @@ export const catalog: Catalog = { "{app} will tell you if a later message from this address is signed by anybody else.": "{app} le avisará si un mensaje posterior de esta dirección lo firma otra persona.", "itself, or an issuer it does not name": "sí mismo, o un emisor que no nombra", "no address": "ninguna dirección", + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "Legacy mail apps": "Aplicaciones de correo con protocolos heredados", + "Off for {tenant}. Only {app} and JMAP apps can sign in to its domains.": "Desactivados para {tenant}. Solo {app} y las aplicaciones JMAP pueden iniciar sesión en sus dominios.", + "On for {tenant}. Mail apps can use IMAP, POP3 and ManageSieve on its domains.": "Activados para {tenant}. Las aplicaciones de correo pueden usar IMAP, POP3 y ManageSieve en sus dominios.", + "Turn legacy protocols back on": "Volver a activar los protocolos de correo heredados", + "Turn off legacy protocols…": "Desactivar los protocolos de correo heredados…", + "No account used a legacy mail app in the last 30 days.": "Ninguna cuenta ha usado una aplicación de correo con protocolos heredados en los últimos 30 días.", + "Their mail apps will stop working the moment you turn this on:": "Sus aplicaciones de correo dejarán de funcionar en cuanto active esto:", + "Only {app} and JMAP apps will work.": "Solo funcionarán {app} y las aplicaciones JMAP.", + "Legacy mail protocols (IMAP, POP3, ManageSieve and sending from mail apps) will be turned off for everyone in {tenant}.": "Los protocolos de correo heredados (IMAP, POP3, ManageSieve y el envío desde aplicaciones de correo) se desactivarán para todos en {tenant}.", + "Phone and desktop mail apps will stop receiving and sending mail. That's iPhone and iPad Mail, the Gmail and Outlook apps, Outlook, Thunderbird and Apple Mail. People will see sign-in errors in them.": "Las aplicaciones de correo del teléfono y del ordenador dejarán de recibir y enviar correo. Eso incluye Mail de iPhone y iPad, las aplicaciones de Gmail y Outlook, Outlook, Thunderbird y Apple Mail. Se verán errores de inicio de sesión en ellas.", + "Filters managed from a mail app (ManageSieve) will stop working. Filters set in {app} keep working.": "Los filtros gestionados desde una aplicación de correo (ManageSieve) dejarán de funcionar. Los filtros configurados en {app} siguen funcionando.", + "Incoming mail is not affected. Calendars and contacts are not affected.": "El correo entrante no se ve afectado. Los calendarios y los contactos no se ven afectados.", + "People keep full access through {app}, which can be installed as an app on phones and computers.": "Todos conservan el acceso completo a través de {app}, que se puede instalar como aplicación en teléfonos y ordenadores.", + "Sending from mail apps (SMTP submission) will stop working, but its ports stay open: mail apps will be told they cannot sign in. Incoming mail (SMTP) and {app} (JMAP) are not affected and cannot be turned off here.": "El envío desde aplicaciones de correo (envío SMTP) dejará de funcionar, pero sus puertos siguen abiertos: se indicará a las aplicaciones de correo que no pueden iniciar sesión. El correo entrante (SMTP) y {app} (JMAP) no se ven afectados y no se pueden desactivar aquí.", + "You can turn legacy protocols back on at any time.": "Puede volver a activar los protocolos de correo heredados en cualquier momento.", + "To confirm, type {phrase}": "Para confirmar, escriba {phrase}", + "Turn off legacy protocols": "Desactivar los protocolos de correo heredados", + "Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Los protocolos de correo heredados están desactivados para su organización. Solo {app} y las aplicaciones JMAP pueden iniciar sesión.", }, plurals: { + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "{n} accounts used a legacy mail app in the last 30 days.": {one: "{n} cuenta ha usado una aplicación de correo con protocolos heredados en los últimos 30 días.", other: "{n} cuentas han usado una aplicación de correo con protocolos heredados en los últimos 30 días."}, // ── Administration: domains ──────────────────────────────────── "{n} accounts use this domain. Move or delete them first.": { one: "{n} cuenta usa este dominio. Muévala o elimínela primero.", other: "{n} cuentas usan este dominio. Muévalas o elimínelas primero." }, "The server stops accepting mail for this domain, and its {n} DKIM keys are deleted. This can't be undone.": { one: "El servidor deja de aceptar correo para este dominio y se elimina su {n} clave DKIM. No se puede deshacer.", other: "El servidor deja de aceptar correo para este dominio y se eliminan sus {n} claves DKIM. No se puede deshacer." }, diff --git a/web/src/locales/fr.ts b/web/src/locales/fr.ts index d90bf78..8b1f90b 100644 --- a/web/src/locales/fr.ts +++ b/web/src/locales/fr.ts @@ -1699,8 +1699,29 @@ export const catalog: Catalog = { "{app} will tell you if a later message from this address is signed by anybody else.": "{app} vous préviendra si un message ultérieur de cette adresse est signé par quelqu'un d'autre.", "itself, or an issuer it does not name": "lui-même, ou un émetteur qu'il ne nomme pas", "no address": "aucune adresse", + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "Legacy mail apps": "Applications de messagerie à protocoles historiques", + "Off for {tenant}. Only {app} and JMAP apps can sign in to its domains.": "Désactivés pour {tenant}. Seuls {app} et les applications JMAP peuvent se connecter à ses domaines.", + "On for {tenant}. Mail apps can use IMAP, POP3 and ManageSieve on its domains.": "Activés pour {tenant}. Les applications de messagerie peuvent utiliser IMAP, POP3 et ManageSieve sur ses domaines.", + "Turn legacy protocols back on": "Réactiver les protocoles de messagerie historiques", + "Turn off legacy protocols…": "Désactiver les protocoles de messagerie historiques…", + "No account used a legacy mail app in the last 30 days.": "Aucun compte n'a utilisé d'application de messagerie à protocoles historiques ces 30 derniers jours.", + "Their mail apps will stop working the moment you turn this on:": "Leurs applications de messagerie cesseront de fonctionner dès que vous activerez ceci :", + "Only {app} and JMAP apps will work.": "Seuls {app} et les applications JMAP fonctionneront.", + "Legacy mail protocols (IMAP, POP3, ManageSieve and sending from mail apps) will be turned off for everyone in {tenant}.": "Les protocoles de messagerie historiques (IMAP, POP3, ManageSieve et l'envoi depuis les applications de messagerie) seront désactivés pour tout le monde dans {tenant}.", + "Phone and desktop mail apps will stop receiving and sending mail. That's iPhone and iPad Mail, the Gmail and Outlook apps, Outlook, Thunderbird and Apple Mail. People will see sign-in errors in them.": "Les applications de messagerie sur téléphone et ordinateur ne pourront plus recevoir ni envoyer de courrier : Mail sur iPhone et iPad, les applications Gmail et Outlook, Outlook, Thunderbird et Apple Mail. Des erreurs de connexion s'y afficheront.", + "Filters managed from a mail app (ManageSieve) will stop working. Filters set in {app} keep working.": "Les filtres gérés depuis une application de messagerie (ManageSieve) cesseront de fonctionner. Les filtres configurés dans {app} continuent de fonctionner.", + "Incoming mail is not affected. Calendars and contacts are not affected.": "Le courrier entrant n'est pas concerné. Les agendas et les contacts ne sont pas concernés.", + "People keep full access through {app}, which can be installed as an app on phones and computers.": "Chacun garde un accès complet via {app}, qui peut s'installer comme application sur téléphone et ordinateur.", + "Sending from mail apps (SMTP submission) will stop working, but its ports stay open: mail apps will be told they cannot sign in. Incoming mail (SMTP) and {app} (JMAP) are not affected and cannot be turned off here.": "L'envoi depuis les applications de messagerie (soumission SMTP) cessera de fonctionner, mais ses ports restent ouverts : les applications de messagerie seront informées qu'elles ne peuvent pas se connecter. Le courrier entrant (SMTP) et {app} (JMAP) ne sont pas concernés et ne peuvent pas être désactivés ici.", + "You can turn legacy protocols back on at any time.": "Vous pouvez réactiver les protocoles de messagerie historiques à tout moment.", + "To confirm, type {phrase}": "Pour confirmer, saisissez {phrase}", + "Turn off legacy protocols": "Désactiver les protocoles de messagerie historiques", + "Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Les protocoles de messagerie historiques sont désactivés pour votre organisation. Seuls {app} et les applications JMAP peuvent se connecter.", }, plurals: { + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "{n} accounts used a legacy mail app in the last 30 days.": {one: "{n} compte a utilisé une application de messagerie à protocoles historiques ces 30 derniers jours.", other: "{n} comptes ont utilisé une application de messagerie à protocoles historiques ces 30 derniers jours."}, // ── Administration: domains ──────────────────────────────────── "{n} accounts use this domain. Move or delete them first.": { one: "{n} compte utilise ce domaine. Déplacez-le ou supprimez-le d’abord.", other: "{n} comptes utilisent ce domaine. Déplacez-les ou supprimez-les d’abord." }, "The server stops accepting mail for this domain, and its {n} DKIM keys are deleted. This can't be undone.": { one: "Le serveur n’accepte plus de messages pour ce domaine, et sa {n} clé DKIM est supprimée. C’est irréversible.", other: "Le serveur n’accepte plus de messages pour ce domaine, et ses {n} clés DKIM sont supprimées. C’est irréversible." }, diff --git a/web/src/locales/ja.ts b/web/src/locales/ja.ts index 774f97c..2394d60 100644 --- a/web/src/locales/ja.ts +++ b/web/src/locales/ja.ts @@ -1702,8 +1702,29 @@ export const catalog: Catalog = { "{app} will tell you if a later message from this address is signed by anybody else.": "このアドレスからの以降のメールが別の人の署名だった場合、{app} がお知らせします。", "itself, or an issuer it does not name": "自分自身、または名前のない発行者", "no address": "アドレスなし", + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "Legacy mail apps": "従来のプロトコルを使うメールアプリ", + "Off for {tenant}. Only {app} and JMAP apps can sign in to its domains.": "{tenant} ではオフです。そのドメインにサインインできるのは {app} と JMAP アプリのみです。", + "On for {tenant}. Mail apps can use IMAP, POP3 and ManageSieve on its domains.": "{tenant} ではオンです。そのドメインではメールアプリが IMAP、POP3、ManageSieve を利用できます。", + "Turn legacy protocols back on": "従来のメールプロトコルを再びオンにする", + "Turn off legacy protocols…": "従来のメールプロトコルをオフにする…", + "No account used a legacy mail app in the last 30 days.": "過去 30 日間に従来のプロトコルでメールアプリを使用したアカウントはありません。", + "Their mail apps will stop working the moment you turn this on:": "これをオンにした時点で、これらのメールアプリは動作しなくなります:", + "Only {app} and JMAP apps will work.": "{app} と JMAP アプリのみが動作します。", + "Legacy mail protocols (IMAP, POP3, ManageSieve and sending from mail apps) will be turned off for everyone in {tenant}.": "{tenant} のすべてのユーザーに対して、従来のメールプロトコル(IMAP、POP3、ManageSieve、メールアプリからの送信)がオフになります。", + "Phone and desktop mail apps will stop receiving and sending mail. That's iPhone and iPad Mail, the Gmail and Outlook apps, Outlook, Thunderbird and Apple Mail. People will see sign-in errors in them.": "スマートフォンやパソコンのメールアプリでメールの送受信ができなくなります。iPhone と iPad のメール、Gmail と Outlook のアプリ、Outlook、Thunderbird、Apple Mail が対象です。これらのアプリにはサインインエラーが表示されます。", + "Filters managed from a mail app (ManageSieve) will stop working. Filters set in {app} keep working.": "メールアプリから管理しているフィルタ(ManageSieve)は動作しなくなります。{app} で設定したフィルタは引き続き動作します。", + "Incoming mail is not affected. Calendars and contacts are not affected.": "受信メールには影響しません。カレンダーと連絡先にも影響しません。", + "People keep full access through {app}, which can be installed as an app on phones and computers.": "{app} からは引き続きすべて利用できます。{app} はスマートフォンやパソコンにアプリとしてインストールできます。", + "Sending from mail apps (SMTP submission) will stop working, but its ports stay open: mail apps will be told they cannot sign in. Incoming mail (SMTP) and {app} (JMAP) are not affected and cannot be turned off here.": "メールアプリからの送信(SMTP submission)は動作しなくなりますが、ポートは開いたままです。メールアプリにはサインインできないことが通知されます。受信メール(SMTP)と {app}(JMAP)には影響せず、ここでオフにすることはできません。", + "You can turn legacy protocols back on at any time.": "従来のメールプロトコルはいつでも再びオンにできます。", + "To confirm, type {phrase}": "確認のため {phrase} と入力してください", + "Turn off legacy protocols": "従来のメールプロトコルをオフにする", + "Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "組織では従来のメールプロトコルがオフになっています。サインインできるのは {app} と JMAP アプリのみです。", }, plurals: { + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "{n} accounts used a legacy mail app in the last 30 days.": {other: "過去 30 日間に {n} 件のアカウントが従来のプロトコルでメールアプリを使用しました。"}, // ── Administration: domains ──────────────────────────────────── "{n} accounts use this domain. Move or delete them first.": { other: "{n} 件のアカウントがこのドメインを使用しています。先に移動または削除してください。" }, "The server stops accepting mail for this domain, and its {n} DKIM keys are deleted. This can't be undone.": { other: "サーバーはこのドメイン宛てのメールを受け付けなくなり、{n} 個の DKIM 鍵も削除されます。元に戻すことはできません。" }, diff --git a/web/src/locales/nl.ts b/web/src/locales/nl.ts index 733eada..88c3540 100644 --- a/web/src/locales/nl.ts +++ b/web/src/locales/nl.ts @@ -1691,8 +1691,29 @@ export const catalog: Catalog = { "{app} will tell you if a later message from this address is signed by anybody else.": "{app} laat het weten als een later bericht van dit adres door iemand anders is ondertekend.", "itself, or an issuer it does not name": "zichzelf, of een uitgever die niet wordt genoemd", "no address": "geen adres", + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "Legacy mail apps": "Mail-apps met verouderde protocollen", + "Off for {tenant}. Only {app} and JMAP apps can sign in to its domains.": "Uit voor {tenant}. Alleen {app} en JMAP-apps kunnen inloggen op de domeinen ervan.", + "On for {tenant}. Mail apps can use IMAP, POP3 and ManageSieve on its domains.": "Aan voor {tenant}. Mail-apps kunnen IMAP, POP3 en ManageSieve gebruiken op de domeinen ervan.", + "Turn legacy protocols back on": "Verouderde mailprotocollen weer inschakelen", + "Turn off legacy protocols…": "Verouderde mailprotocollen uitschakelen…", + "No account used a legacy mail app in the last 30 days.": "Geen enkel account heeft de afgelopen 30 dagen een mail-app met verouderde protocollen gebruikt.", + "Their mail apps will stop working the moment you turn this on:": "Hun mail-apps werken niet meer zodra u dit inschakelt:", + "Only {app} and JMAP apps will work.": "Alleen {app} en JMAP-apps werken dan nog.", + "Legacy mail protocols (IMAP, POP3, ManageSieve and sending from mail apps) will be turned off for everyone in {tenant}.": "Verouderde mailprotocollen (IMAP, POP3, ManageSieve en verzenden vanuit mail-apps) worden uitgeschakeld voor iedereen in {tenant}.", + "Phone and desktop mail apps will stop receiving and sending mail. That's iPhone and iPad Mail, the Gmail and Outlook apps, Outlook, Thunderbird and Apple Mail. People will see sign-in errors in them.": "Mail-apps op telefoon en computer kunnen geen mail meer ontvangen en verzenden. Het gaat om Mail op iPhone en iPad, de Gmail- en Outlook-apps, Outlook, Thunderbird en Apple Mail. Gebruikers zien daarin inlogfouten.", + "Filters managed from a mail app (ManageSieve) will stop working. Filters set in {app} keep working.": "Filters die vanuit een mail-app worden beheerd (ManageSieve) werken niet meer. Filters die in {app} zijn ingesteld blijven werken.", + "Incoming mail is not affected. Calendars and contacts are not affected.": "Inkomende mail wordt niet beïnvloed. Agenda's en contacten worden niet beïnvloed.", + "People keep full access through {app}, which can be installed as an app on phones and computers.": "Iedereen houdt volledige toegang via {app}, dat als app op telefoons en computers kan worden geïnstalleerd.", + "Sending from mail apps (SMTP submission) will stop working, but its ports stay open: mail apps will be told they cannot sign in. Incoming mail (SMTP) and {app} (JMAP) are not affected and cannot be turned off here.": "Verzenden vanuit mail-apps (SMTP-submission) werkt niet meer, maar de poorten blijven open: mail-apps krijgen te horen dat ze niet kunnen inloggen. Inkomende mail (SMTP) en {app} (JMAP) worden niet beïnvloed en kunnen hier niet worden uitgeschakeld.", + "You can turn legacy protocols back on at any time.": "U kunt verouderde mailprotocollen op elk moment weer inschakelen.", + "To confirm, type {phrase}": "Typ ter bevestiging {phrase}", + "Turn off legacy protocols": "Verouderde mailprotocollen uitschakelen", + "Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Verouderde mailprotocollen zijn uitgeschakeld voor uw organisatie. Alleen {app} en JMAP-apps kunnen inloggen.", }, plurals: { + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "{n} accounts used a legacy mail app in the last 30 days.": {one: "{n} account heeft de afgelopen 30 dagen een mail-app met verouderde protocollen gebruikt.", other: "{n} accounts hebben de afgelopen 30 dagen een mail-app met verouderde protocollen gebruikt."}, // ── Administration: domains ──────────────────────────────────── "{n} accounts use this domain. Move or delete them first.": { one: "{n} account gebruikt dit domein. Verplaats of verwijder deze eerst.", other: "{n} accounts gebruiken dit domein. Verplaats of verwijder ze eerst." }, "The server stops accepting mail for this domain, and its {n} DKIM keys are deleted. This can't be undone.": { one: "De server accepteert geen e-mails meer voor dit domein en de {n} DKIM-sleutel wordt verwijderd. Dit kan niet ongedaan worden gemaakt.", other: "De server accepteert geen e-mails meer voor dit domein en de {n} DKIM-sleutels worden verwijderd. Dit kan niet ongedaan worden gemaakt." }, diff --git a/web/src/locales/pt-BR.ts b/web/src/locales/pt-BR.ts index 6784a28..c667dfb 100644 --- a/web/src/locales/pt-BR.ts +++ b/web/src/locales/pt-BR.ts @@ -1697,8 +1697,29 @@ export const catalog: Catalog = { "{app} will tell you if a later message from this address is signed by anybody else.": "O {app} avisará você se uma mensagem posterior deste endereço for assinada por outra pessoa.", "itself, or an issuer it does not name": "ele mesmo, ou um emissor que ele não nomeia", "no address": "nenhum endereço", + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "Legacy mail apps": "Aplicativos de e-mail com protocolos legados", + "Off for {tenant}. Only {app} and JMAP apps can sign in to its domains.": "Desativados para {tenant}. Só {app} e aplicativos JMAP podem entrar nos domínios dessa organização.", + "On for {tenant}. Mail apps can use IMAP, POP3 and ManageSieve on its domains.": "Ativados para {tenant}. Aplicativos de e-mail podem usar IMAP, POP3 e ManageSieve nos domínios dessa organização.", + "Turn legacy protocols back on": "Reativar os protocolos de e-mail legados", + "Turn off legacy protocols…": "Desativar os protocolos de e-mail legados…", + "No account used a legacy mail app in the last 30 days.": "Nenhuma conta usou um aplicativo de e-mail com protocolos legados nos últimos 30 dias.", + "Their mail apps will stop working the moment you turn this on:": "Os aplicativos de e-mail dessas contas vão parar de funcionar assim que você ativar isto:", + "Only {app} and JMAP apps will work.": "Só {app} e aplicativos JMAP vão funcionar.", + "Legacy mail protocols (IMAP, POP3, ManageSieve and sending from mail apps) will be turned off for everyone in {tenant}.": "Os protocolos de e-mail legados (IMAP, POP3, ManageSieve e o envio por aplicativos de e-mail) serão desativados para todos em {tenant}.", + "Phone and desktop mail apps will stop receiving and sending mail. That's iPhone and iPad Mail, the Gmail and Outlook apps, Outlook, Thunderbird and Apple Mail. People will see sign-in errors in them.": "Aplicativos de e-mail no celular e no computador vão parar de receber e enviar e-mail. Isso inclui o Mail do iPhone e do iPad, os aplicativos Gmail e Outlook, o Outlook, o Thunderbird e o Apple Mail. As pessoas verão erros de login neles.", + "Filters managed from a mail app (ManageSieve) will stop working. Filters set in {app} keep working.": "Filtros gerenciados por um aplicativo de e-mail (ManageSieve) vão parar de funcionar. Filtros configurados no {app} continuam funcionando.", + "Incoming mail is not affected. Calendars and contacts are not affected.": "O e-mail recebido não é afetado. Agendas e contatos não são afetados.", + "People keep full access through {app}, which can be installed as an app on phones and computers.": "Todos mantêm acesso completo pelo {app}, que pode ser instalado como aplicativo em celulares e computadores.", + "Sending from mail apps (SMTP submission) will stop working, but its ports stay open: mail apps will be told they cannot sign in. Incoming mail (SMTP) and {app} (JMAP) are not affected and cannot be turned off here.": "O envio por aplicativos de e-mail (SMTP submission) vai parar de funcionar, mas as portas continuam abertas: os aplicativos de e-mail serão informados de que não podem entrar. O e-mail recebido (SMTP) e o {app} (JMAP) não são afetados e não podem ser desativados aqui.", + "You can turn legacy protocols back on at any time.": "Você pode reativar os protocolos de e-mail legados a qualquer momento.", + "To confirm, type {phrase}": "Para confirmar, digite {phrase}", + "Turn off legacy protocols": "Desativar os protocolos de e-mail legados", + "Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Os protocolos de e-mail legados estão desativados para sua organização. Só {app} e aplicativos JMAP podem entrar.", }, plurals: { + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "{n} accounts used a legacy mail app in the last 30 days.": {one: "{n} conta usou um aplicativo de e-mail com protocolos legados nos últimos 30 dias.", other: "{n} contas usaram um aplicativo de e-mail com protocolos legados nos últimos 30 dias."}, // ── Administration: domains ──────────────────────────────────── "{n} accounts use this domain. Move or delete them first.": { one: "{n} conta usa este domínio. Mova-a ou exclua-a primeiro.", other: "{n} contas usam este domínio. Mova-as ou exclua-as primeiro." }, "The server stops accepting mail for this domain, and its {n} DKIM keys are deleted. This can't be undone.": { one: "O servidor deixa de aceitar e-mails para este domínio, e a {n} chave DKIM dele é excluída. Não é possível desfazer.", other: "O servidor deixa de aceitar e-mails para este domínio, e as {n} chaves DKIM dele são excluídas. Não é possível desfazer." }, diff --git a/web/src/locales/ru.ts b/web/src/locales/ru.ts index 29dd63c..fa81dea 100644 --- a/web/src/locales/ru.ts +++ b/web/src/locales/ru.ts @@ -1696,8 +1696,29 @@ export const catalog: Catalog = { "{app} will tell you if a later message from this address is signed by anybody else.": "{app} сообщит, если следующее письмо с этого адреса подпишет кто-то другой.", "itself, or an issuer it does not name": "самим собой или неназванным издателем", "no address": "нет адреса", + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "Legacy mail apps": "Почтовые приложения на устаревших протоколах", + "Off for {tenant}. Only {app} and JMAP apps can sign in to its domains.": "Отключены для {tenant}. Входить на домены этой организации могут только {app} и приложения JMAP.", + "On for {tenant}. Mail apps can use IMAP, POP3 and ManageSieve on its domains.": "Включены для {tenant}. Почтовые приложения могут использовать IMAP, POP3 и ManageSieve на доменах этой организации.", + "Turn legacy protocols back on": "Снова включить устаревшие почтовые протоколы", + "Turn off legacy protocols…": "Отключить устаревшие почтовые протоколы…", + "No account used a legacy mail app in the last 30 days.": "За последние 30 дней ни одна учётная запись не использовала почтовое приложение на устаревших протоколах.", + "Their mail apps will stop working the moment you turn this on:": "Их почтовые приложения перестанут работать сразу после того, как вы это включите:", + "Only {app} and JMAP apps will work.": "Работать будут только {app} и приложения JMAP.", + "Legacy mail protocols (IMAP, POP3, ManageSieve and sending from mail apps) will be turned off for everyone in {tenant}.": "Устаревшие почтовые протоколы (IMAP, POP3, ManageSieve и отправка из почтовых приложений) будут отключены для всех в {tenant}.", + "Phone and desktop mail apps will stop receiving and sending mail. That's iPhone and iPad Mail, the Gmail and Outlook apps, Outlook, Thunderbird and Apple Mail. People will see sign-in errors in them.": "Почтовые приложения на телефонах и компьютерах перестанут получать и отправлять почту. Это Почта на iPhone и iPad, приложения Gmail и Outlook, Outlook, Thunderbird и Apple Mail. В них появятся ошибки входа.", + "Filters managed from a mail app (ManageSieve) will stop working. Filters set in {app} keep working.": "Фильтры, управляемые из почтового приложения (ManageSieve), перестанут работать. Фильтры, настроенные в {app}, продолжат работать.", + "Incoming mail is not affected. Calendars and contacts are not affected.": "Входящая почта не затрагивается. Календари и контакты не затрагиваются.", + "People keep full access through {app}, which can be installed as an app on phones and computers.": "У всех остаётся полный доступ через {app}, который можно установить как приложение на телефоны и компьютеры.", + "Sending from mail apps (SMTP submission) will stop working, but its ports stay open: mail apps will be told they cannot sign in. Incoming mail (SMTP) and {app} (JMAP) are not affected and cannot be turned off here.": "Отправка из почтовых приложений (SMTP submission) перестанет работать, но её порты останутся открытыми: почтовым приложениям будет сообщено, что войти нельзя. Входящая почта (SMTP) и {app} (JMAP) не затрагиваются, и отключить их здесь нельзя.", + "You can turn legacy protocols back on at any time.": "Вы можете снова включить устаревшие почтовые протоколы в любой момент.", + "To confirm, type {phrase}": "Для подтверждения введите {phrase}", + "Turn off legacy protocols": "Отключить устаревшие почтовые протоколы", + "Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Устаревшие почтовые протоколы отключены для вашей организации. Входить могут только {app} и приложения JMAP.", }, plurals: { + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "{n} accounts used a legacy mail app in the last 30 days.": {one: "{n} учётная запись использовала почтовое приложение на устаревших протоколах за последние 30 дней.", few: "{n} учётные записи использовали почтовое приложение на устаревших протоколах за последние 30 дней.", many: "{n} учётных записей использовали почтовое приложение на устаревших протоколах за последние 30 дней.", other: "{n} учётной записи использовали почтовое приложение на устаревших протоколах за последние 30 дней."}, // ── Administration: domains ──────────────────────────────────── "{n} accounts use this domain. Move or delete them first.": { one: "Этот домен использует {n} учётная запись. Сначала перенесите или удалите её.", few: "Этот домен используют {n} учётные записи. Сначала перенесите или удалите их.", many: "Этот домен используют {n} учётных записей. Сначала перенесите или удалите их.", other: "Этот домен используют {n} учётной записи. Сначала перенесите или удалите их." }, "The server stops accepting mail for this domain, and its {n} DKIM keys are deleted. This can't be undone.": { one: "Сервер перестанет принимать почту для этого домена, и его {n} ключ DKIM будет удалён. Отменить это нельзя.", few: "Сервер перестанет принимать почту для этого домена, и его {n} ключа DKIM будут удалены. Отменить это нельзя.", many: "Сервер перестанет принимать почту для этого домена, и его {n} ключей DKIM будут удалены. Отменить это нельзя.", other: "Сервер перестанет принимать почту для этого домена, и его {n} ключа DKIM будут удалены. Отменить это нельзя." }, diff --git a/web/src/locales/uk.ts b/web/src/locales/uk.ts index 32ade90..8939cf8 100644 --- a/web/src/locales/uk.ts +++ b/web/src/locales/uk.ts @@ -1690,8 +1690,29 @@ export const catalog: Catalog = { "{app} will tell you if a later message from this address is signed by anybody else.": "{app} повідомить, якщо наступний лист із цієї адреси підпише хтось інший.", "itself, or an issuer it does not name": "самим собою або неназваним видавцем", "no address": "немає адреси", + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "Legacy mail apps": "Поштові програми на застарілих протоколах", + "Off for {tenant}. Only {app} and JMAP apps can sign in to its domains.": "Вимкнено для {tenant}. Входити на домени цієї організації можуть лише {app} і програми JMAP.", + "On for {tenant}. Mail apps can use IMAP, POP3 and ManageSieve on its domains.": "Увімкнено для {tenant}. Поштові програми можуть використовувати IMAP, POP3 і ManageSieve на доменах цієї організації.", + "Turn legacy protocols back on": "Знову увімкнути застарілі поштові протоколи", + "Turn off legacy protocols…": "Вимкнути застарілі поштові протоколи…", + "No account used a legacy mail app in the last 30 days.": "За останні 30 днів жоден обліковий запис не використовував поштову програму на застарілих протоколах.", + "Their mail apps will stop working the moment you turn this on:": "Їхні поштові програми перестануть працювати, щойно ви це ввімкнете:", + "Only {app} and JMAP apps will work.": "Працюватимуть лише {app} і програми JMAP.", + "Legacy mail protocols (IMAP, POP3, ManageSieve and sending from mail apps) will be turned off for everyone in {tenant}.": "Застарілі поштові протоколи (IMAP, POP3, ManageSieve і надсилання з поштових програм) буде вимкнено для всіх у {tenant}.", + "Phone and desktop mail apps will stop receiving and sending mail. That's iPhone and iPad Mail, the Gmail and Outlook apps, Outlook, Thunderbird and Apple Mail. People will see sign-in errors in them.": "Поштові програми на телефонах і комп'ютерах перестануть отримувати й надсилати пошту. Це Пошта на iPhone та iPad, програми Gmail і Outlook, Outlook, Thunderbird і Apple Mail. У них з'являтимуться помилки входу.", + "Filters managed from a mail app (ManageSieve) will stop working. Filters set in {app} keep working.": "Фільтри, якими керують із поштової програми (ManageSieve), перестануть працювати. Фільтри, налаштовані в {app}, працюватимуть і далі.", + "Incoming mail is not affected. Calendars and contacts are not affected.": "Вхідну пошту це не зачіпає. Календарі й контакти це не зачіпає.", + "People keep full access through {app}, which can be installed as an app on phones and computers.": "Усі зберігають повний доступ через {app}, який можна встановити як програму на телефони й комп'ютери.", + "Sending from mail apps (SMTP submission) will stop working, but its ports stay open: mail apps will be told they cannot sign in. Incoming mail (SMTP) and {app} (JMAP) are not affected and cannot be turned off here.": "Надсилання з поштових програм (SMTP submission) перестане працювати, але його порти залишаться відкритими: поштовим програмам повідомлять, що увійти не можна. Вхідну пошту (SMTP) і {app} (JMAP) це не зачіпає, і вимкнути їх тут не можна.", + "You can turn legacy protocols back on at any time.": "Ви можете знову ввімкнути застарілі поштові протоколи будь-коли.", + "To confirm, type {phrase}": "Для підтвердження введіть {phrase}", + "Turn off legacy protocols": "Вимкнути застарілі поштові протоколи", + "Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "Застарілі поштові протоколи вимкнено для вашої організації. Входити можуть лише {app} і програми JMAP.", }, plurals: { + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "{n} accounts used a legacy mail app in the last 30 days.": {one: "{n} обліковий запис використовував поштову програму на застарілих протоколах за останні 30 днів.", few: "{n} облікові записи використовували поштову програму на застарілих протоколах за останні 30 днів.", many: "{n} облікових записів використовували поштову програму на застарілих протоколах за останні 30 днів.", other: "{n} облікового запису використовували поштову програму на застарілих протоколах за останні 30 днів."}, // ── Administration: domains ──────────────────────────────────── "{n} accounts use this domain. Move or delete them first.": { one: "Цей домен використовує {n} обліковий запис. Спершу перенесіть або видаліть його.", few: "Цей домен використовують {n} облікові записи. Спершу перенесіть або видаліть їх.", many: "Цей домен використовують {n} облікових записів. Спершу перенесіть або видаліть їх.", other: "Цей домен використовують {n} облікового запису. Спершу перенесіть або видаліть їх." }, "The server stops accepting mail for this domain, and its {n} DKIM keys are deleted. This can't be undone.": { one: "Сервер перестане приймати пошту для цього домену, і його {n} ключ DKIM буде видалено. Скасувати це неможливо.", few: "Сервер перестане приймати пошту для цього домену, і його {n} ключі DKIM буде видалено. Скасувати це неможливо.", many: "Сервер перестане приймати пошту для цього домену, і його {n} ключів DKIM буде видалено. Скасувати це неможливо.", other: "Сервер перестане приймати пошту для цього домену, і його {n} ключа DKIM буде видалено. Скасувати це неможливо." }, diff --git a/web/src/locales/zh-Hans.ts b/web/src/locales/zh-Hans.ts index e6a91d0..c0a8621 100644 --- a/web/src/locales/zh-Hans.ts +++ b/web/src/locales/zh-Hans.ts @@ -1701,8 +1701,29 @@ export const catalog: Catalog = { "{app} will tell you if a later message from this address is signed by anybody else.": "如果此地址之后的邮件由他人签名,{app} 会提醒您。", "itself, or an issuer it does not name": "其自身,或一个未具名的颁发者", "no address": "无地址", + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "Legacy mail apps": "使用传统协议的邮件应用", + "Off for {tenant}. Only {app} and JMAP apps can sign in to its domains.": "{tenant} 已关闭。只有 {app} 和 JMAP 应用可以登录其域名。", + "On for {tenant}. Mail apps can use IMAP, POP3 and ManageSieve on its domains.": "{tenant} 已开启。邮件应用可以在其域名上使用 IMAP、POP3 和 ManageSieve。", + "Turn legacy protocols back on": "重新开启传统邮件协议", + "Turn off legacy protocols…": "关闭传统邮件协议…", + "No account used a legacy mail app in the last 30 days.": "过去 30 天内没有账户使用过传统协议的邮件应用。", + "Their mail apps will stop working the moment you turn this on:": "您开启此项后,这些账户的邮件应用将立即停止工作:", + "Only {app} and JMAP apps will work.": "只有 {app} 和 JMAP 应用可以使用。", + "Legacy mail protocols (IMAP, POP3, ManageSieve and sending from mail apps) will be turned off for everyone in {tenant}.": "{tenant} 中所有人的传统邮件协议(IMAP、POP3、ManageSieve 以及从邮件应用发信)都将被关闭。", + "Phone and desktop mail apps will stop receiving and sending mail. That's iPhone and iPad Mail, the Gmail and Outlook apps, Outlook, Thunderbird and Apple Mail. People will see sign-in errors in them.": "手机和电脑上的邮件应用将无法收发邮件,包括 iPhone 和 iPad 上的邮件、Gmail 和 Outlook 应用、Outlook、Thunderbird 以及 Apple Mail。用户会在这些应用中看到登录错误。", + "Filters managed from a mail app (ManageSieve) will stop working. Filters set in {app} keep working.": "通过邮件应用管理的过滤器(ManageSieve)将停止工作。在 {app} 中设置的过滤器会继续工作。", + "Incoming mail is not affected. Calendars and contacts are not affected.": "收件不受影响。日历和联系人不受影响。", + "People keep full access through {app}, which can be installed as an app on phones and computers.": "所有人仍可通过 {app} 完整访问,{app} 可以作为应用安装在手机和电脑上。", + "Sending from mail apps (SMTP submission) will stop working, but its ports stay open: mail apps will be told they cannot sign in. Incoming mail (SMTP) and {app} (JMAP) are not affected and cannot be turned off here.": "从邮件应用发信(SMTP submission)将停止工作,但其端口保持开放:邮件应用会被告知无法登录。收件(SMTP)和 {app}(JMAP)不受影响,也无法在此关闭。", + "You can turn legacy protocols back on at any time.": "您可以随时重新开启传统邮件协议。", + "To confirm, type {phrase}": "请输入 {phrase} 以确认", + "Turn off legacy protocols": "关闭传统邮件协议", + "Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.": "您的组织已关闭传统邮件协议。只有 {app} 和 JMAP 应用可以登录。", }, plurals: { + // ── Administration: legacy mail protocols (INBUXA) ────────────── + "{n} accounts used a legacy mail app in the last 30 days.": {other: "过去 30 天内有 {n} 个账户使用过传统协议的邮件应用。"}, // ── Administration: domains ──────────────────────────────────── "{n} accounts use this domain. Move or delete them first.": { other: "有 {n} 个账户正在使用此域名。请先移动或删除它们。" }, "The server stops accepting mail for this domain, and its {n} DKIM keys are deleted. This can't be undone.": { other: "服务器将不再接收此域名的邮件,其 {n} 个 DKIM 密钥也会被删除。此操作无法撤销。" }, diff --git a/web/src/styles/app.css b/web/src/styles/app.css index 10d6c48..6cd9063 100644 --- a/web/src/styles/app.css +++ b/web/src/styles/app.css @@ -1726,6 +1726,12 @@ select optgroup { background-color: var(--bg-elev); color: var(--fg); } .admin-notice { display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; border-radius: var(--radius-sm); background: var(--bg-sunken); color: var(--fg-muted); font-size: .92em; margin: 8px 0; } .admin-notice svg { flex: none; margin-top: 2px; } .admin-notice.warn { background: var(--warn-soft); color: var(--fg); } +/* INBUXA: a tenant's legacy mail protocols switch (legacy-protocols LP-15 to LP-17) */ +.admin-legacy-confirm { display: grid; gap: 12px; margin-top: 12px; } +.admin-legacy-box { padding: 10px 12px; border-radius: var(--radius-sm); background: var(--bg-sunken); font-size: .92em; } +.admin-legacy-box.warn { background: var(--warn-soft); color: var(--fg); } +.admin-legacy-box p { margin: 0 0 6px; } +.admin-legacy-box ul { margin: 6px 0; padding-left: 18px; } .admin-notice.warn svg { color: var(--warn); } .admin-notice.error { background: var(--danger-soft); color: var(--fg); } .admin-sheet { position: absolute; top: 0; right: 0; bottom: 0; width: min(460px, 100%); z-index: 20; display: flex; flex-direction: column; background: var(--bg-elev); border-left: 1px solid var(--border); box-shadow: var(--shadow-3); animation: admin-sheet-in .18s var(--ease); } diff --git a/web/src/views/admin/AdminDashboard.tsx b/web/src/views/admin/AdminDashboard.tsx index de4a88a..1694d6c 100644 --- a/web/src/views/admin/AdminDashboard.tsx +++ b/web/src/views/admin/AdminDashboard.tsx @@ -1,6 +1,8 @@ import { useEffect, useState, type ReactNode } from "react"; import { Link } from "wouter"; -import { ArrowDownToLine, ArrowUpFromLine, ExternalLink, Globe, Hourglass, LayoutDashboard, MemoryStick, RefreshCw, Users } from "lucide-react"; +import { ArrowDownToLine, ArrowUpFromLine, ExternalLink, Globe, Hourglass, LayoutDashboard, MemoryStick, RefreshCw, ShieldCheck, Users } from "lucide-react"; +import { legacyProtocolsOff } 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"; import { formatDayMonthTime, resolvedLocale } from "@/lib/datetime"; @@ -35,6 +37,8 @@ async function settle(work: Promise): Promise> { 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 app = useAppName(); const cards = dashboardCards(perms); const sections = adminSections(perms); const [reload, setReload] = useState(0); @@ -102,6 +106,13 @@ export function AdminDashboard() { + {legacyOff && ( + // INBUXA legacy-protocols LP-18: while it's off for the organization +

+ + {t("Legacy mail protocols are off for your organization. Only {app} and JMAP apps can sign in.", { app })} +

+ )} {shown.length ? (
diff --git a/web/src/views/admin/TenantLegacyProtocols.tsx b/web/src/views/admin/TenantLegacyProtocols.tsx new file mode 100644 index 0000000..e87e4f6 --- /dev/null +++ b/web/src/views/admin/TenantLegacyProtocols.tsx @@ -0,0 +1,152 @@ +import { useEffect, useState } from "react"; +import { useAppName } from "@/lib/brand"; +import { can } from "@/lib/admin/adminAccess"; +import { + CONFIRM_PHRASE, + fetchTenantLegacy, + impactEntries, + phraseMatches, + setTenantLegacy, + type TenantLegacy, +} from "@/lib/admin/adminLegacyProtocols"; +import { formatRelative } from "@/lib/format"; +import { plural, t, tNode } from "@/lib/i18n"; +import { toast } from "@/ui/toast"; +import { usePermissions } from "./usePermissions"; + +/** + * A tenant's legacy mail protocols switch, in its sheet (INBUXA + * legacy-protocols LP-9 to LP-17, at tenant scope). + * + * Nobody should turn it on by accident or without understanding it: turning + * it off shows who would notice (LP-15) and what it means (LP-16) before the + * typed phrase is asked for (LP-17). Turning it back on is one click -- undoing + * a restriction must never be the hard part. A tenant's switch closes no port, + * so the statement names none. + * + * Only INBUXA has it; on any other server the section isn't there. + */ +export function TenantLegacyProtocols({ tenantId, tenantName }: { tenantId: string; tenantName: string }) { + const perms = usePermissions(); + const app = useAppName(); + const canChange = can(perms, "Domain", "Update"); + const [state, setState] = useState(null); + const [confirming, setConfirming] = useState(false); + const [typed, setTyped] = useState(""); + const [busy, setBusy] = useState(false); + const [revision, setRevision] = useState(0); + + useEffect(() => { + let canceled = false; + void fetchTenantLegacy(tenantId).then((s) => !canceled && setState(s)); + return () => { + canceled = true; + }; + }, [tenantId, revision]); + + if (!state) return null; + + const turn = async (off: boolean) => { + setBusy(true); + try { + await setTenantLegacy(tenantId, off); + setConfirming(false); + setTyped(""); + setRevision((r) => r + 1); + } catch (err) { + toast.error((err as Error).message); + } finally { + setBusy(false); + } + }; + + const entries = state.recent ? impactEntries(state.recent) : null; + + return ( + <> +

{t("Legacy mail apps")}

+

+ {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 })} +

+ + {canChange && state.off && ( + + )} + {canChange && !state.off && !confirming && ( + + )} + + {!state.off && confirming && ( +
+ {entries && + (entries.length === 0 ? ( +

{t("No account used a legacy mail app in the last 30 days.")}

+ ) : ( +
+

+ + {plural(entries.length, { + one: "{n} account used a legacy mail app in the last 30 days.", + other: "{n} accounts used a legacy mail app in the last 30 days.", + }, { n: entries.length })} + {" "} + {t("Their mail apps will stop working the moment you turn this on:")} +

+
    + {entries.map((e) => ( +
  • + {e.name}: {e.protocols.join(", ")},{" "} + {formatRelative(new Date(e.lastUsedAt).toISOString())} +
  • + ))} +
+
+ ))} + +
+

{t("Only {app} and JMAP apps will work.", { app })}

+

+ {t("Legacy mail protocols (IMAP, POP3, ManageSieve and sending from mail apps) will be turned off for everyone in {tenant}.", { tenant: tenantName })} +

+
    +
  • {t("Phone and desktop mail apps will stop receiving and sending mail. That's iPhone and iPad Mail, the Gmail and Outlook apps, Outlook, Thunderbird and Apple Mail. People will see sign-in errors in them.")}
  • +
  • {t("Filters managed from a mail app (ManageSieve) will stop working. Filters set in {app} keep working.", { app })}
  • +
  • {t("Incoming mail is not affected. Calendars and contacts are not affected.")}
  • +
  • {t("People keep full access through {app}, which can be installed as an app on phones and computers.", { app })}
  • +
+

{t("Sending from mail apps (SMTP submission) will stop working, but its ports stay open: mail apps will be told they cannot sign in. Incoming mail (SMTP) and {app} (JMAP) are not affected and cannot be turned off here.", { app })}

+

{t("You can turn legacy protocols back on at any time.")}

+
+ +
+ + setTyped(e.target.value)} + /> +
+
+ + +
+
+ )} + + ); +} diff --git a/web/src/views/admin/TenantSheet.tsx b/web/src/views/admin/TenantSheet.tsx index ed18d0b..186d111 100644 --- a/web/src/views/admin/TenantSheet.tsx +++ b/web/src/views/admin/TenantSheet.tsx @@ -27,6 +27,7 @@ import { Dialog } from "@/ui/dialog"; import { Spinner } from "@/ui/misc"; import { toast } from "@/ui/toast"; import { usePermissions } from "./usePermissions"; +import { TenantLegacyProtocols } from "./TenantLegacyProtocols"; const GIB = 1024 ** 3; @@ -203,6 +204,8 @@ export function TenantSheet({ tenant, roles, onClose, onChanged, onCreated, onDe

{t("Domains")}

{ setRevision((n) => n + 1); onChanged(); }} /> + + )} diff --git a/web/src/views/admin/__tests__/tenant-legacy-protocols.test.tsx b/web/src/views/admin/__tests__/tenant-legacy-protocols.test.tsx new file mode 100644 index 0000000..29cf605 --- /dev/null +++ b/web/src/views/admin/__tests__/tenant-legacy-protocols.test.tsx @@ -0,0 +1,101 @@ +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { useSession } from "@/store/session"; +import type { JmapSession } from "@/jmap/types"; +import type { TenantLegacy } from "@/lib/admin/adminLegacyProtocols"; + +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +const api = vi.hoisted(() => ({ + state: null as TenantLegacy | null, + set: vi.fn(async () => {}), +})); +vi.mock("@/lib/admin/adminLegacyProtocols", async (original) => ({ + ...(await original()), + fetchTenantLegacy: vi.fn(async () => api.state), + setTenantLegacy: api.set, +})); + +const { TenantLegacyProtocols } = await import("../TenantLegacyProtocols"); + +const signIn = (permissions: string[]) => + useSession.setState({ session: { capabilities: {}, accounts: {}, primaryAccounts: {}, username: "a@example.com", ihasmail: { permissions } } as unknown as JmapSession }); +const button = (host: HTMLElement, label: string) => [...host.querySelectorAll("button")].find((b) => b.textContent?.trim() === label); +const type = async (el: HTMLInputElement, value: string) => { + await act(async () => { + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(el, value); + el.dispatchEvent(new Event("input", { bubbles: true })); + }); +}; + +/** + * INBUXA legacy-protocols at tenant scope: nobody turns it off by accident + * (the statement and a typed phrase first), and turning it back on is one + * click. + */ +describe("a tenant's legacy mail protocols switch", () => { + let host: HTMLDivElement; + let root: Root; + const render = async () => { + await act(async () => { + root.render(); + }); + await act(async () => {}); + }; + beforeEach(() => { + host = document.createElement("div"); + document.body.appendChild(host); + root = createRoot(host); + api.set.mockClear(); + }); + afterEach(async () => { + await act(async () => root.unmount()); + host.remove(); + }); + + it("isn't there on a server without the switch", async () => { + api.state = null; + signIn(["sysDomainGet", "sysDomainUpdate"]); + await render(); + expect(host.textContent).toBe(""); + }); + + 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 }] }; + signIn(["sysDomainGet", "sysDomainUpdate"]); + await render(); + await act(async () => button(host, "Turn off legacy protocols…")!.click()); + + expect(host.textContent).toContain("1 account used a legacy mail app in the last 30 days."); + expect(host.textContent).toContain("maria@acme.example: IMAP"); + expect(host.textContent).toContain("turned off for everyone in Acme Corp"); + // A tenant's switch closes no port, so the statement names none. + expect(host.textContent).not.toContain("firewall"); + + const confirm = button(host, "Turn off legacy protocols")!; + const box = host.querySelector("#admin-legacy-confirm")!; + await type(box, "Turn off legacy mail"); + expect(confirm.disabled).toBe(true); + await type(box, "turn off legacy mail"); + expect(confirm.disabled).toBe(false); + await act(async () => confirm.click()); + expect(api.set).toHaveBeenCalledWith("t1", true); + }); + + it("turns it back on with one click", async () => { + api.state = { off: true, recent: [] }; + signIn(["sysDomainGet", "sysDomainUpdate"]); + await render(); + await act(async () => button(host, "Turn legacy protocols back on")!.click()); + expect(api.set).toHaveBeenCalledWith("t1", false); + }); + + it("shows the state but no switch to someone who can't change domains", async () => { + api.state = { off: true, recent: null }; + signIn(["sysDomainGet"]); + await render(); + expect(host.textContent).toContain("Off for Acme Corp"); + expect(host.querySelector("button")).toBeNull(); + }); +});