/* * SPDX-FileCopyrightText: 2026 Coffey Labs * * SPDX-License-Identifier: AGPL-3.0-only */ /** * INBUXA: one tenant's legacy mail protocols switch, on the tenant's page * (legacy-protocols spec, LP-9 to LP-18 at tenant scope). * * It closes no port -- other tenants share them (LP-13) -- so the statement * names no listener and carries no firewall note. It refuses sign-in over * legacy protocols on the tenant's domains. Turning it off takes the typed * phrase; turning it back on is one click, which the server refuses while * it has legacy protocols off itself (LP-9), and says so. */ import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Loader2, ShieldCheck, ShieldOff } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useAccountStore } from '@/stores/accountStore'; import { toast } from '@/hooks/use-toast'; import { cn } from '@/lib/utils'; import { getAccountId, jmapGet } from '@/services/jmap/client'; import { fetchTenantPolicy, PolicyUnavailable, updateTenantPolicy, type TenantPolicy } from './protocolPolicy'; import { ConfirmTurnOff, ImpactPanel, Statement } from './parts'; export function TenantLegacyProtocols({ tenantId }: { tenantId: string }) { const { t } = useTranslation(); const canGet = useAccountStore((s) => s.hasObjectPermission('sysDomain', 'Get')); const canUpdate = useAccountStore((s) => s.hasObjectPermission('sysDomain', 'Update')); const [policy, setPolicy] = useState(null); const [organization, setOrganization] = useState(''); const [confirming, setConfirming] = useState(false); const [busy, setBusy] = useState(false); const loaded = useCallback( (fetching: Promise, signal?: AbortSignal) => fetching .then((p) => { if (!signal?.aborted) setPolicy(p); }) .catch((e: unknown) => { // An older server has no tenant switch: show nothing rather than an error. if (!signal?.aborted && !(e instanceof PolicyUnavailable)) console.error(e); }), [], ); useEffect(() => { if (!canGet) return; const controller = new AbortController(); void loaded(fetchTenantPolicy(tenantId, controller.signal), controller.signal); // The organization's name, for the statement. jmapGet('x:Tenant', getAccountId('x:Tenant'), [tenantId], ['name'], controller.signal) .then((responses) => { const list = (responses[0]?.[1] as { list?: { name?: string }[] } | undefined)?.list; if (!controller.signal.aborted && list?.[0]?.name) setOrganization(list[0].name); }) .catch(() => {}); return () => controller.abort(); }, [tenantId, canGet, loaded]); const turn = useCallback( async (value: 'enabled' | 'disabled') => { setBusy(true); try { await updateTenantPolicy(tenantId, value); setConfirming(false); await loaded(fetchTenantPolicy(tenantId)); } catch (e) { toast({ variant: 'destructive', title: t('legacyProtocols.failed', 'The switch did not change'), description: e instanceof Error ? e.message : String(e), }); } finally { setBusy(false); } }, [tenantId, loaded, t], ); if (!policy) return null; const off = policy.legacyProtocols === 'disabled'; const name = organization || t('legacyProtocols.thisOrganization', 'this organization'); return ( // Aligned with the tenant form beneath it.
{off ? ( ) : ( )}

{t('legacyProtocols.title', 'Legacy mail protocols')}

{off ? t( 'legacyProtocols.tenantOff', 'Off for {{organization}}. Only inbuxa webmail and JMAP apps can sign in to its domains.', { organization: name }, ) : t( 'legacyProtocols.tenantOn', 'On for {{organization}}. Mail apps can use IMAP, POP3 and ManageSieve on its domains.', { organization: name }, )}

{canUpdate && off && ( )} {canUpdate && !off && !confirming && ( )}
{!off && confirming && ( <> {policy.recentLegacyUse && } void turn('disabled')} onCancel={() => setConfirming(false)} /> )}
); }