From 5c08fb9fe9dde3a13135c88ec54f6c504573bf17 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Sun, 27 Sep 2026 19:32:33 -0700 Subject: [PATCH] Delete people in the console, not the webmail's Administration Deleting a person's account is the console's now, beside locking it and legal holds: the console asks why, for the audit log, and says when a hold keeps the data. Where Delete was, the account's page says so and links to the account in the console when the server names one. Groups, lists, domains and tenants keep their delete here. 2 new strings in all nine catalogs, unreviewed. --- web/src/locales/de.ts | 3 + web/src/locales/es.ts | 3 + web/src/locales/fr.ts | 3 + web/src/locales/ja.ts | 3 + web/src/locales/nl.ts | 3 + web/src/locales/pt-BR.ts | 3 + web/src/locales/ru.ts | 3 + web/src/locales/uk.ts | 3 + web/src/locales/zh-Hans.ts | 3 + web/src/views/admin/AccountSheet.tsx | 73 +++++-------------- web/src/views/admin/AccountsAdmin.tsx | 4 - .../admin/__tests__/account-sheet.test.tsx | 19 ++++- 12 files changed, 60 insertions(+), 63 deletions(-) diff --git a/web/src/locales/de.ts b/web/src/locales/de.ts index 3ada0e7..5ec180b 100644 --- a/web/src/locales/de.ts +++ b/web/src/locales/de.ts @@ -1754,6 +1754,9 @@ 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: 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", // inbuxa AL-7, AL-8: a locked account handed to the reader "You no longer have access to {name}. Back to your own mail.": "Sie haben keinen Zugriff mehr auf {name}. Zurück zu Ihren eigenen E-Mails.", "You can't send from {name}. It was handed to you to read, not to send as.": "Sie können nicht als {name} senden. Das Konto wurde Ihnen zum Lesen übergeben, nicht zum Senden.", diff --git a/web/src/locales/es.ts b/web/src/locales/es.ts index 19b32ce..96da847 100644 --- a/web/src/locales/es.ts +++ b/web/src/locales/es.ts @@ -1727,6 +1727,9 @@ 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: 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", // inbuxa AL-7, AL-8: a locked account handed to the reader "You no longer have access to {name}. Back to your own mail.": "Ya no tiene acceso a {name}. Vuelve a su propio correo.", "You can't send from {name}. It was handed to you to read, not to send as.": "No puede enviar desde {name}. Se le entregó para leerla, no para enviar en su nombre.", diff --git a/web/src/locales/fr.ts b/web/src/locales/fr.ts index 3a37a94..bebce11 100644 --- a/web/src/locales/fr.ts +++ b/web/src/locales/fr.ts @@ -1732,6 +1732,9 @@ 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: 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", // inbuxa AL-7, AL-8: a locked account handed to the reader "You no longer have access to {name}. Back to your own mail.": "Vous n’avez plus accès à {name}. Retour à votre propre courrier.", "You can't send from {name}. It was handed to you to read, not to send as.": "Vous ne pouvez pas envoyer depuis {name}. Ce compte vous a été confié pour le lire, pas pour envoyer en son nom.", diff --git a/web/src/locales/ja.ts b/web/src/locales/ja.ts index 517af6d..837347b 100644 --- a/web/src/locales/ja.ts +++ b/web/src/locales/ja.ts @@ -1735,6 +1735,9 @@ 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: 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": "コンソールで開く", // inbuxa AL-7, AL-8: a locked account handed to the reader "You no longer have access to {name}. Back to your own mail.": "{name} にアクセスできなくなりました。自分のメールに戻ります。", "You can't send from {name}. It was handed to you to read, not to send as.": "{name} から送信することはできません。このアカウントは閲覧のために委任されています。", diff --git a/web/src/locales/nl.ts b/web/src/locales/nl.ts index 789f5b4..79645a8 100644 --- a/web/src/locales/nl.ts +++ b/web/src/locales/nl.ts @@ -1727,6 +1727,9 @@ 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: 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", // inbuxa AL-7, AL-8: a locked account handed to the reader "You no longer have access to {name}. Back to your own mail.": "U hebt geen toegang meer tot {name}. Terug naar uw eigen mail.", "You can't send from {name}. It was handed to you to read, not to send as.": "U kunt niet verzenden vanuit {name}. Het account is u gegeven om te lezen, niet om namens te verzenden.", diff --git a/web/src/locales/pt-BR.ts b/web/src/locales/pt-BR.ts index c396e48..5308219 100644 --- a/web/src/locales/pt-BR.ts +++ b/web/src/locales/pt-BR.ts @@ -1730,6 +1730,9 @@ 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: 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", // inbuxa AL-7, AL-8: a locked account handed to the reader "You no longer have access to {name}. Back to your own mail.": "Você não tem mais acesso a {name}. De volta ao seu próprio e-mail.", "You can't send from {name}. It was handed to you to read, not to send as.": "Você não pode enviar de {name}. A conta foi entregue a você para leitura, não para enviar em nome dela.", diff --git a/web/src/locales/ru.ts b/web/src/locales/ru.ts index f4bbc00..fda1c8a 100644 --- a/web/src/locales/ru.ts +++ b/web/src/locales/ru.ts @@ -1729,6 +1729,9 @@ 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: 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": "Открыть в консоли", // inbuxa AL-7, AL-8: a locked account handed to the reader "You no longer have access to {name}. Back to your own mail.": "У вас больше нет доступа к {name}. Возвращаемся к вашей почте.", "You can't send from {name}. It was handed to you to read, not to send as.": "Вы не можете отправлять письма от имени {name}. Учётная запись передана вам для чтения, а не для отправки.", diff --git a/web/src/locales/uk.ts b/web/src/locales/uk.ts index e1b615c..0e30882 100644 --- a/web/src/locales/uk.ts +++ b/web/src/locales/uk.ts @@ -1723,6 +1723,9 @@ 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: 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": "Відкрити в консолі", // inbuxa AL-7, AL-8: a locked account handed to the reader "You no longer have access to {name}. Back to your own mail.": "У вас більше немає доступу до {name}. Повертаємося до вашої пошти.", "You can't send from {name}. It was handed to you to read, not to send as.": "Ви не можете надсилати листи від імені {name}. Обліковий запис передано вам для читання, а не для надсилання.", diff --git a/web/src/locales/zh-Hans.ts b/web/src/locales/zh-Hans.ts index 85aada8..8ca8a54 100644 --- a/web/src/locales/zh-Hans.ts +++ b/web/src/locales/zh-Hans.ts @@ -1734,6 +1734,9 @@ 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: 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": "在控制台中打开", // inbuxa AL-7, AL-8: a locked account handed to the reader "You no longer have access to {name}. Back to your own mail.": "您已无法访问 {name}。已返回您自己的邮件。", "You can't send from {name}. It was handed to you to read, not to send as.": "您不能从 {name} 发送邮件。该账户交给您是为了阅读,而不是代其发送。", diff --git a/web/src/views/admin/AccountSheet.tsx b/web/src/views/admin/AccountSheet.tsx index 8cdb3fb..6a04173 100644 --- a/web/src/views/admin/AccountSheet.tsx +++ b/web/src/views/admin/AccountSheet.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from "react"; -import { Copy, Dices, KeyRound, Lock, Plus, Trash2, X } from "lucide-react"; +import { Copy, Dices, ExternalLink, KeyRound, Lock, Plus, X } from "lucide-react"; import { ADMIN_BASELINE, can, @@ -12,7 +12,6 @@ import { aliasList, createAccount, describeDirectoryError, - destroyAccount, hasPassword, passwordPatch, quotasWithDisk, @@ -25,7 +24,7 @@ import { formatSize } from "@/lib/format"; import { t, tNode } from "@/lib/i18n"; import { Link } from "wouter"; import { Avatar } from "@/ui/misc"; -import { Dialog } from "@/ui/dialog"; +import { useSession } from "@/store/session"; import { toast } from "@/ui/toast"; import { isSelf, roleName, type DirectoryContext } from "./directoryContext"; import { usePermissions } from "./usePermissions"; @@ -39,7 +38,6 @@ interface Props { onClose: () => void; onChanged: () => void; onCreated: (id: string) => void; - onDeleted: () => void; } /** A role as one select value: "User", "Admin", or "custom:". */ @@ -71,7 +69,7 @@ const bytesOf = (gib: string) => { * a password and a delete are their own calls, because each is a decision of * its own and should never ride along with a renamed display name. */ -export function AccountSheet({ account, ctx, onClose, onChanged, onCreated, onDeleted }: Props) { +export function AccountSheet({ account, ctx, onClose, onChanged, onCreated }: Props) { const perms = usePermissions(); const creating = account === null; const self = account ? isSelf(account, ctx) : false; @@ -288,9 +286,7 @@ export function AccountSheet({ account, ctx, onClose, onChanged, onCreated, onDe {error &&

{error}

} - {!creating && can(perms, "Account", "Destroy") && ( - - )} + {!creating && can(perms, "Account", "Destroy") && } {editable && ( @@ -433,59 +429,24 @@ export function Aliases({ aliases, setAliases, editable, domains, defaultDomain, ); } -function DeleteAccount({ account, blocked, onDeleted }: { account: DirectoryAccount; blocked: string | null; onDeleted: () => void }) { - const [open, setOpen] = useState(false); - const [typed, setTyped] = useState(""); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); - const address = account.emailAddress ?? account.name; +/** + * inbuxa: deleting a person's account is the console's, beside locking it + * and legal holds: it asks why, for the audit log, and says when a hold + * keeps the data. Only the pointer is here. + */ +function DeleteInConsole({ accountId }: { accountId: string }) { + const adminUrl = useSession((s) => s.session?.ihasmail?.server?.adminUrl ?? null); return ( <>

{t("Delete")}

-

{blocked ?? t("Deletes the mailbox and everything in it.")}

- +

{t("Deleting, locking and legal holds are done in the administration console, which records why and keeps what a hold covers.")}

+ {adminUrl && ( + + {t("Open in the console")} + + )}
- setOpen(false)} - title={t("Delete {address}?", { address })} - size="sm" - footer={ - <> - - - - } - > -

{t("This deletes the mail, calendars, contacts and files in this account. The server removes them in the background, and it can't be undone.")}

-
- - setTyped(e.target.value)} /> -
- {error &&

{error}

} -
); } diff --git a/web/src/views/admin/AccountsAdmin.tsx b/web/src/views/admin/AccountsAdmin.tsx index ebb3661..297d842 100644 --- a/web/src/views/admin/AccountsAdmin.tsx +++ b/web/src/views/admin/AccountsAdmin.tsx @@ -217,10 +217,6 @@ export function AccountsAdmin({ selectedId }: { selectedId?: string }) { changed(); navigate(`/admin/accounts/${id}`); }} - onDeleted={() => { - changed(); - close(); - }} /> )} diff --git a/web/src/views/admin/__tests__/account-sheet.test.tsx b/web/src/views/admin/__tests__/account-sheet.test.tsx index 5b304bb..baf4e51 100644 --- a/web/src/views/admin/__tests__/account-sheet.test.tsx +++ b/web/src/views/admin/__tests__/account-sheet.test.tsx @@ -48,7 +48,7 @@ describe("the account sheet", () => { await act(async () => { root.render( - {}} onChanged={() => {}} onCreated={() => {}} onDeleted={() => {}} /> + {}} onChanged={() => {}} onCreated={() => {}} /> , ); }); @@ -89,7 +89,20 @@ describe("the account sheet", () => { expect(host.querySelector('a[href="/settings/security"]')).not.toBeNull(); expect(button(host, "Set a new password")).toBeUndefined(); expect((host.querySelector('select[aria-label="Role"]') as HTMLSelectElement).disabled).toBe(true); - expect(button(host, "Delete account")?.disabled).toBe(true); + expect(host.textContent).not.toContain("Delete account"); + expect(host.textContent).toContain("administration console"); + }); + + it("sends deleting a person to the console, opened on that account (inbuxa)", async () => { + signIn([...HELPDESK, "sysAccountDestroy"]); + const s = useSession.getState().session!; + useSession.setState({ + session: { ...s, ihasmail: { ...s.ihasmail, server: { ...(s.ihasmail?.server ?? {}), adminUrl: "https://admin.example.com/" } } } as never, + }); + await render(account({ id: "k7" })); + expect(host.textContent).not.toContain("Delete account"); + const link = host.querySelector('a[href="https://admin.example.com/Management/x:Account/User/k7"]'); + expect(link?.textContent).toContain("Open in the console"); }); }); @@ -108,7 +121,7 @@ describe("an account's tenant", () => { const render = async (a: DirectoryAccount) => { const { hook } = memoryLocation({ path: `/admin/accounts/${a.id}` }); await act(async () => { - root.render( {}} onChanged={() => {}} onCreated={() => {}} onDeleted={() => {}} />); + root.render( {}} onChanged={() => {}} onCreated={() => {}} />); }); }; beforeEach(() => { -- 2.54.0