'use client'; import { useEffect, useState } from 'react'; import { Download, Info } from '@/components/icons'; import { Button } from '@/components/ui/button'; import { SettingItem, SettingsSection, ToggleSwitch } from '@/components/settings/settings-section'; import { AdminLink } from '@/components/admin/admin-link'; import { parseDomainList } from '@/lib/admin/organisation-policy'; import { toUnicodeDomain } from '@/lib/idn'; import { apiFetch } from '@/lib/browser-navigation'; import { EXTERNAL_MAIL_WARNING_PLUGIN_ID } from '@/lib/plugin-sandbox/host-features'; import { InheritNote } from '../org-page'; import type { OrgPolicyDraft } from '../use-org-policy-draft'; /** The first release of the plugin core took over from that steps back on its own. */ export const PLUGIN_STEPS_BACK_FROM = '1.6.0'; function older(version: string, than: string): boolean { const a = version.split(/[-+]/)[0].split('.').map(Number); const b = than.split(/[-+]/)[0].split('.').map(Number); for (let i = 0; i < 3; i++) if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) < (b[i] || 0); return false; } type InstalledPlugin = { version: string }; /** The External Mail Warning plugin when the admin installed it, else null. */ function useInstalledPlugin(): InstalledPlugin | null { const [plugin, setPlugin] = useState(null); useEffect(() => { let cancelled = false; void (async () => { try { const res = await apiFetch('/api/admin/plugins'); const list = res.ok ? ((await res.json()) as Array<{ id: string; version?: string }>) : []; const found = Array.isArray(list) ? list.find((p) => p.id === EXTERNAL_MAIL_WARNING_PLUGIN_ID) : undefined; if (!cancelled) setPlugin(found ? { version: found.version ?? '' } : null); } catch { if (!cancelled) setPlugin(null); } })(); return () => { cancelled = true; }; }, []); return plugin; } const onOff = (v: boolean) => (v ? 'on' : 'off'); /** * Organisation > Security > External mail: the "External" sender label, * the confirm before sending outside, which domains are internal, and the * hand-over from the External Mail Warning plugin. */ export function ExternalMailSection({ draft }: { draft: OrgPolicyDraft }) { const senderLabel = draft.orgValue('externalMail', 'senderLabel'); const recipientWarning = draft.orgValue('externalMail', 'recipientWarning'); const domains = draft.orgValue('externalMail', 'domains'); const trustContacts = draft.orgValue('externalMail', 'trustContacts'); const plugin = useInstalledPlugin(); const anyOn = senderLabel.value || recipientWarning.value; // The list is typed as text and taken apart when the field is left, so a // half-typed domain isn't thrown away on every key. It is stored in ASCII // (xn--) form and shown in Unicode; entries that aren't domains are named. const stored = domains.value.join('\n'); const joined = domains.value.map(toUnicodeDomain).join('\n'); const [text, setText] = useState(joined); const [rejected, setRejected] = useState([]); useEffect(() => { setText(joined); }, [joined]); const commitDomains = (raw: string) => { const parsed = parseDomainList(raw) ?? { domains: [], rejected: [] }; setRejected(parsed.rejected); if (parsed.domains.join('\n') !== stored) draft.setOrgValue('externalMail', 'domains', parsed.domains); else setText(joined); }; const [importing, setImporting] = useState(false); const importFromPlugin = async () => { setImporting(true); try { const res = await apiFetch(`/api/admin/plugins/${EXTERNAL_MAIL_WARNING_PLUGIN_ID}/config`); if (!res.ok) throw new Error(String(res.status)); const config = (await res.json()) as Record; const parsed = parseDomainList(config.safeDomains) ?? { domains: [], rejected: [] }; const fromPlugin = parsed.domains; setRejected(parsed.rejected); const added = fromPlugin.filter((d) => !domains.value.includes(d)); if (added.length > 0) draft.setOrgValue('externalMail', 'domains', [...domains.value, ...added]); draft.setMessage({ type: 'success', text: added.length > 0 ? `Added ${added.length} ${added.length === 1 ? 'domain' : 'domains'} from the plugin. Save to apply them.` : fromPlugin.length > 0 ? 'The list already has every domain the plugin had.' : 'The plugin has no safe domains set.', }); } catch { draft.setMessage({ type: 'error', text: 'Could not read the plugin settings' }); } finally { setImporting(false); } }; const outdated = !!plugin && older(plugin.version, PLUGIN_STEPS_BACK_FROM); return (
draft.setOrgValue('externalMail', 'senderLabel', v)} testId="org-external-sender-label" /> draft.clearOrgValue('externalMail', 'senderLabel')} />
draft.setOrgValue('externalMail', 'recipientWarning', v)} testId="org-external-recipient-warning" /> draft.clearOrgValue('externalMail', 'recipientWarning')} />
draft.setOrgValue('externalMail', 'trustContacts', v)} testId="org-external-trust-contacts" /> draft.clearOrgValue('externalMail', 'trustContacts')} />

Your users' own domains always count as internal. Add partner or sister-company domains here, one per line or separated by commas. A domain covers its subdomains: example.com also covers eu.example.com.