import { useEffect, useMemo, useState, type ReactNode } from "react"; import { FieldLabel, MailServerSettingsPanel, PolicyLockedHint, PolicySourcePath, ToggleSwitch, hasMailImapSettings, mailImapSettingsPayload, mailServerSecurityOptions, mailSmtpSettingsPayload, mailTextOrNull, mailTransportCredentialsPayload, normalizeMailServerSecurity, type MailServerConnectionTestResult, type MailServerCredentialSettings, type MailServerFolderLookupResult, type MailServerImapSettings, type MailServerSmtpSettings, type PolicySourcePathItem } from "@govoplan/core-webui"; import { Pencil, Plus, Trash2 } from "lucide-react"; import type { ApiSettings } from "../../types"; import { createMailServerProfile, deactivateMailServerProfile, getMailProfilePolicy, listImapFolders, listMailProfileImapFolders, listMailServerProfiles, mailProfilePatternKeys, mailProfilePolicyLimitKeys, updateMailProfilePolicy, testImapSettings, testMailProfileImap, testMailProfileSmtp, testSmtpSettings, updateMailServerProfile, type MailCredentialPolicy, type MailImapTestPayload, type MailProfilePatternKey, type MailProfilePatternRules, type MailProfilePolicy, type MailProfilePolicyLimitKey, type MailProfileScope, type MailSecurity, type MailServerProfile, type MailServerProfilePayload, type MailServerProfileUpdatePayload, type MailSmtpTestPayload } from "../../api/mail"; import { validateMailPolicy } from "./mailPolicyValidation"; import { Button } from "@govoplan/core-webui"; import { Card } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid"; import { Dialog } from "@govoplan/core-webui"; import { DismissibleAlert } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui"; export type MailProfileTargetOption = { id: string; label: string; secondary?: string | null; }; type ProfileDraft = { name: string; slug: string; description: string; isActive: boolean; smtpHost: string; smtpPort: string; smtpSecurity: MailSecurity; smtpUsername: string; smtpPassword: string; smtpTimeout: string; imapHost: string; imapPort: string; imapSecurity: MailSecurity; imapUsername: string; imapPassword: string; imapSentFolder: string; imapTimeout: string; }; type MailProfileScopeManagerProps = { settings: ApiSettings; scopeType: MailProfileScope; scopeId?: string | null; targetOptions?: MailProfileTargetOption[]; targetLabel?: string; profileTitle?: string; policyTitle?: string; canWriteProfiles: boolean; canManageCredentials: boolean; canWritePolicy: boolean; }; type MailProfilePolicyEditorProps = { settings: ApiSettings; scopeType: MailProfileScope; scopeId?: string | null; campaignId?: string | null; profiles: MailServerProfile[]; ownerUserId?: string | null; ownerGroupId?: string | null; canWrite: boolean; locked?: boolean; title?: string; description?: string; onSaved?: () => void | Promise; }; type EditingProfile = MailServerProfile | "new" | null; type PolicyFlagValue = "inherit" | "allow" | "deny"; type CredentialInheritanceValue = "inherit" | "profile" | "local"; const securityOptions = mailServerSecurityOptions as readonly MailSecurity[]; const patternLabels: Record = { smtp_hosts: "SMTP hostnames", imap_hosts: "IMAP hostnames", envelope_senders: "Envelope senders", from_headers: "From headers", recipient_domains: "Recipient domains" }; const blankPolicy: MailProfilePolicy = { allowed_profile_ids: [], allow_user_profiles: null, allow_group_profiles: null, allow_campaign_profiles: null, smtp_credentials: {}, imap_credentials: {}, whitelist: {}, blacklist: {}, allow_lower_level_limits: {} }; export function MailProfileScopeManager({ settings, scopeType, scopeId = null, targetOptions = [], targetLabel = "Target", profileTitle = "Mail server profiles", policyTitle = "Mail profile policy", canWriteProfiles, canManageCredentials, canWritePolicy }: MailProfileScopeManagerProps) { const [profiles, setProfiles] = useState([]); const [selectedTargetId, setSelectedTargetId] = useState(scopeId || targetOptions[0]?.id || ""); const [editing, setEditing] = useState(null); const [deactivating, setDeactivating] = useState(null); const [draft, setDraft] = useState(emptyProfileDraft()); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const [profileEffectivePolicy, setProfileEffectivePolicy] = useState(null); const requiresTarget = scopeType === "user" || scopeType === "group" || scopeType === "campaign"; const targetSelectionRequired = requiresTarget && !scopeId; const hasSelectableTarget = targetOptions.length > 0; const activeScopeId = scopeId || selectedTargetId || null; const scopeReady = !requiresTarget || Boolean(activeScopeId); const targetEmptyText = `No ${pluralizeLabel(targetLabel.toLowerCase())} available`; useEffect(() => { if (scopeId) { setSelectedTargetId(scopeId); return; } if (targetOptions.length > 0 && !targetOptions.some((option) => option.id === selectedTargetId)) { setSelectedTargetId(targetOptions[0].id); return; } if (targetOptions.length === 0 && selectedTargetId) setSelectedTargetId(""); }, [scopeId, selectedTargetId, targetOptions]); useEffect(() => { void loadProfiles(); }, [activeScopeId, scopeReady, scopeType, settings.accessToken, settings.apiBaseUrl, settings.apiKey]); async function loadProfiles() { setLoading(true); setError(""); if (!scopeReady) { setProfiles([]); setProfileEffectivePolicy(null); setLoading(false); return; } try { const [nextProfiles, policyResponse] = await Promise.all([ listMailServerProfiles(settings, true), getMailProfilePolicy(settings, scopeType, activeScopeId) ]); setProfiles(nextProfiles); setProfileEffectivePolicy(policyResponse?.effective_policy ?? null); } catch (err) { setProfiles([]); setProfileEffectivePolicy(null); setError(errorMessage(err)); } finally { setLoading(false); } } const scopedProfiles = useMemo( () => profiles.filter((profile) => profileBelongsToEditableScope(profile, scopeType, activeScopeId)), [activeScopeId, profiles, scopeType] ); function openCreate() { setDraft(emptyProfileDraft()); setEditing("new"); setError(""); setSuccess(""); } function openEdit(profile: MailServerProfile) { setDraft(profileToDraft(profile)); setEditing(profile); setError(""); setSuccess(""); } async function saveProfile() { if (!editing || !scopeReady) return; setBusy(true); setError(""); setSuccess(""); try { if (editing === "new") { const payload = createProfilePayload(draft, scopeType, activeScopeId); const created = await createMailServerProfile(settings, payload); setSuccess(`Profile ${created.name} created.`); } else { const payload = updateProfilePayload(draft, editing); const updated = await updateMailServerProfile(settings, editing.id, payload); setSuccess(`Profile ${updated.name} updated.`); } setEditing(null); await loadProfiles(); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } async function deactivateProfile() { if (!deactivating) return; setBusy(true); setError(""); setSuccess(""); try { await deactivateMailServerProfile(settings, deactivating.id); setSuccess(`Profile ${deactivating.name} deactivated.`); setDeactivating(null); await loadProfiles(); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } const columns = useMemo[]>(() => [ { id: "name", header: "Profile", width: "minmax(220px, 1fr)", sticky: "start", resizable: true, sortable: true, filterable: true, value: (profile) => `${profile.name} ${profile.slug}`, render: (profile) =>
{profile.name}
{profile.slug}
}, { id: "scope", header: "Scope", width: 120, resizable: true, sortable: true, filterable: true, value: (profile) => scopeLabel(profile) }, { id: "smtp", header: "SMTP", width: 220, sortable: true, filterable: true, value: (profile) => transportLabel(profile.smtp), render: (profile) => }, { id: "imap", header: "IMAP", width: 220, sortable: true, filterable: true, value: (profile) => profile.imap ? transportLabel(profile.imap) : "Not configured", render: (profile) => }, { id: "status", header: "Status", width: 110, sortable: true, filterable: true, value: (profile) => profile.is_active ? "Active" : "Inactive", render: (profile) => {profile.is_active ? "Active" : "Inactive"} }, { id: "actions", header: "Actions", width: 145, sticky: "end", align: "right", render: (profile) =>
} ], [busy, canWriteProfiles]); return (
{targetSelectionRequired && (
)} {scopeReady && ( <>
} >
profile.id} emptyText="No profiles in this scope." />
)} {error && {error}} {success && {success}} !busy && setEditing(null)} className="admin-dialog admin-dialog-wide mail-profile-dialog" closeDisabled={busy} footer={<>} > setDeactivating(null)} onConfirm={() => void deactivateProfile()} /> ); } export function MailProfilePolicyEditor({ settings, scopeType, scopeId = null, campaignId = null, profiles, ownerUserId = null, ownerGroupId = null, canWrite, locked = false, title = "Mail profile policy", description = "Allowed profiles and wildcard rules for this scope.", onSaved }: MailProfilePolicyEditorProps) { const [policy, setPolicy] = useState(blankPolicy); const [effectivePolicy, setEffectivePolicy] = useState(null); const [parentPolicy, setParentPolicy] = useState(null); const [effectivePolicySources, setEffectivePolicySources] = useState([]); const [loading, setLoading] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const [profileEffectivePolicy, setProfileEffectivePolicy] = useState(null); const requiresTarget = scopeType === "user" || scopeType === "group" || scopeType === "campaign"; const scopeReady = !requiresTarget || Boolean(scopeId); useEffect(() => { void loadPolicy(); }, [settings.accessToken, settings.apiBaseUrl, settings.apiKey, scopeType, scopeId, campaignId]); async function loadPolicy() { setError(""); setSuccess(""); if (!scopeReady) { setPolicy(blankPolicy); setEffectivePolicy(null); setParentPolicy(null); setEffectivePolicySources([]); return; } setLoading(true); try { const response = await getMailProfilePolicy(settings, scopeType, scopeId, campaignId); setPolicy(normalizePolicy(response.policy)); setEffectivePolicy(response.effective_policy ? normalizePolicy(response.effective_policy) : null); setParentPolicy(response.parent_policy ? normalizePolicy(response.parent_policy) : null); setEffectivePolicySources(response.effective_policy_sources ?? []); } catch (err) { setPolicy(blankPolicy); setEffectivePolicy(null); setParentPolicy(null); setEffectivePolicySources([]); setError(errorMessage(err)); } finally { setLoading(false); } } async function savePolicy() { if (!scopeReady) return; setBusy(true); setError(""); setSuccess(""); try { const response = await updateMailProfilePolicy(settings, scopeType, normalizePolicyForSave(policy, parentPolicy, scopeType), scopeId); setPolicy(normalizePolicy(response.policy)); setEffectivePolicy(response.effective_policy ? normalizePolicy(response.effective_policy) : null); setParentPolicy(response.parent_policy ? normalizePolicy(response.parent_policy) : null); setEffectivePolicySources(response.effective_policy_sources ?? []); setSuccess("Mail profile policy saved."); await onSaved?.(); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } const candidateProfiles = useMemo( () => profileCandidatesForPolicy(profiles, scopeType, scopeId, ownerUserId, ownerGroupId), [ownerGroupId, ownerUserId, profiles, scopeId, scopeType] ); const isSystem = scopeType === "system"; const displayPolicy = useMemo(() => isSystem ? concreteSystemPolicy(policy) : policy, [isSystem, policy]); const selectedProfileIds = new Set(policy.allowed_profile_ids ?? []); const disabled = locked || busy || loading || !canWrite || !scopeReady; const parentAllowedProfileIds = parentPolicy?.allowed_profile_ids?.length ? new Set(parentPolicy.allowed_profile_ids) : null; const parentBlocksUserProfiles = parentPolicy?.allow_user_profiles === false; const parentBlocksGroupProfiles = parentPolicy?.allow_group_profiles === false; const parentBlocksCampaignProfiles = parentPolicy?.allow_campaign_profiles === false; const smtpCredentialLocked = !parentAllowsMailLimit("smtp_credentials.inherit"); const imapCredentialLocked = !parentAllowsMailLimit("imap_credentials.inherit"); const showAllowColumn = scopeType !== "campaign"; const showEffectiveColumn = !isSystem; const profileAllowListLocked = !parentAllowsMailLimit("allowed_profile_ids"); const blockedProfileDefinitions = [ parentBlocksUserProfiles ? "user" : "", parentBlocksGroupProfiles ? "group" : "", parentBlocksCampaignProfiles ? "campaign-local settings" : "" ].filter(Boolean).join(", "); const lockedCredentialKinds = [smtpCredentialLocked ? "SMTP" : "", imapCredentialLocked ? "IMAP" : ""].filter(Boolean).join(" and "); const effectivePolicyPath = effectivePolicySources.length > 0 ? effectivePolicySources : mailPolicySourcePath(scopeType); function patchPolicy(patch: Partial) { setPolicy((current) => normalizePolicy({ ...current, ...patch })); } function toggleAllowedProfile(profileId: string, checked: boolean) { const next = new Set(policy.allowed_profile_ids ?? []); if (checked) next.add(profileId); else next.delete(profileId); patchPolicy({ allowed_profile_ids: [...next].sort() }); } function setFlag(key: "allow_user_profiles" | "allow_group_profiles" | "allow_campaign_profiles", value: PolicyFlagValue) { patchPolicy({ [key]: flagToBoolean(value) }); } function setCredentialPolicy(protocol: "smtp" | "imap", patch: Partial) { const key = protocol === "smtp" ? "smtp_credentials" : "imap_credentials"; patchPolicy({ [key]: normalizeCredentialPolicy({ ...(policy[key] ?? {}), ...patch }) }); } function setPattern(kind: "whitelist" | "blacklist", key: MailProfilePatternKey, text: string) { const nextRules = { ...(policy[kind] ?? {}) }; const parsed = parsePatternList(text); if (parsed.length > 0) nextRules[key] = parsed; else delete nextRules[key]; patchPolicy({ [kind]: nextRules }); } function parentAllowsMailLimit(key: MailProfilePolicyLimitKey): boolean { return !parentPolicy || parentPolicy.allow_lower_level_limits?.[key] !== false; } function localAllowsMailLimit(key: MailProfilePolicyLimitKey): boolean { const localValue = policy.allow_lower_level_limits?.[key]; if (localValue !== undefined) return localValue && parentAllowsMailLimit(key); return parentAllowsMailLimit(key); } function setAllowLowerLevelLimit(key: MailProfilePolicyLimitKey, allowed: boolean) { patchPolicy({ allow_lower_level_limits: { ...(policy.allow_lower_level_limits ?? {}), [key]: allowed } }); } function lowerLevelLimitToggle(key: MailProfilePolicyLimitKey, label: ReactNode = "Allow override"): ReactNode | undefined { if (!showAllowColumn) return undefined; const parentLocked = !parentAllowsMailLimit(key); return ( setAllowLowerLevelLimit(key, checked)} label={label} /> ); } return ( } >
{description &&

{description}

} {error && {error}} {success && {success}}

Profile allow-list

{lowerLevelLimitToggle("allowed_profile_ids")}

{selectedProfileIds.size === 0 ? "No local profile allow-list is set." : `${selectedProfileIds.size} profile(s) allowed by this scope.`}

{candidateProfiles.map((profile) => ( ))} {candidateProfiles.length === 0 &&

No profiles are visible for this policy scope.

}
{parentAllowedProfileIds &&

An ancestor allow-list limits selectable profiles to {parentAllowedProfileIds.size} profile(s).

}

Lower-level mail definitions

setFlag("allow_user_profiles", value)} />} effective={showEffectiveColumn ? effectiveBooleanLabel(effectivePolicy?.allow_user_profiles, effectivePolicy) : undefined} allowControl={lowerLevelLimitToggle("allow_user_profiles")} effectiveHelp={showEffectiveColumn ? mailBooleanPolicyPathHelp("allow_user_profiles", effectivePolicyPath) : undefined} /> setFlag("allow_group_profiles", value)} />} effective={showEffectiveColumn ? effectiveBooleanLabel(effectivePolicy?.allow_group_profiles, effectivePolicy) : undefined} allowControl={lowerLevelLimitToggle("allow_group_profiles")} effectiveHelp={showEffectiveColumn ? mailBooleanPolicyPathHelp("allow_group_profiles", effectivePolicyPath) : undefined} /> setFlag("allow_campaign_profiles", value)} />} effective={showEffectiveColumn ? effectiveBooleanLabel(effectivePolicy?.allow_campaign_profiles, effectivePolicy) : undefined} allowControl={lowerLevelLimitToggle("allow_campaign_profiles")} effectiveHelp={showEffectiveColumn ? mailBooleanPolicyPathHelp("allow_campaign_profiles", effectivePolicyPath) : undefined} /> {blockedProfileDefinitions && Explicit allow is unavailable for {blockedProfileDefinitions} because an ancestor policy blocks those definitions.}

Credential inheritance

setCredentialPolicy("smtp", { inherit: valueToCredentialInheritance(value) })} />} effective={showEffectiveColumn ? (effectivePolicy ? credentialInheritanceLabel(effectivePolicy.smtp_credentials) : "Loading...") : undefined} allowControl={lowerLevelLimitToggle("smtp_credentials.inherit")} effectiveHelp={showEffectiveColumn ? mailCredentialPolicyPathHelp("smtp_credentials", effectivePolicyPath) : undefined} /> setCredentialPolicy("imap", { inherit: valueToCredentialInheritance(value) })} />} effective={showEffectiveColumn ? (effectivePolicy ? credentialInheritanceLabel(effectivePolicy.imap_credentials) : "Loading...") : undefined} allowControl={lowerLevelLimitToggle("imap_credentials.inherit")} effectiveHelp={showEffectiveColumn ? mailCredentialPolicyPathHelp("imap_credentials", effectivePolicyPath) : undefined} /> {lockedCredentialKinds && {lockedCredentialKinds} credential inheritance is locked by an ancestor policy.}

Wildcard rules

Policy target Whitelist Blacklist {showAllowColumn && Lower levels}
{mailProfilePatternKeys.map((key) => (
{patternLabels[key]}
setPattern("whitelist", key, text)} /> setPattern("blacklist", key, text)} /> {showAllowColumn && (
{lowerLevelLimitToggle(`whitelist.${key}` as MailProfilePolicyLimitKey, "Whitelist")} {lowerLevelLimitToggle(`blacklist.${key}` as MailProfilePolicyLimitKey, "Blacklist")}
)}
))}
{showEffectiveColumn && effectivePolicy && (

Policy path

Effective values are shown in the table rows above.

)}
); } function ProfileForm({ settings, draft, setDraft, editing, busy, canWrite, canManageCredentials, effectivePolicy }: { settings: ApiSettings; draft: ProfileDraft; setDraft: (draft: ProfileDraft) => void; editing: EditingProfile; busy: boolean; canWrite: boolean; canManageCredentials: boolean; effectivePolicy?: MailProfilePolicy | null; }) { const [smtpTestResult, setSmtpTestResult] = useState(null); const [imapTestResult, setImapTestResult] = useState(null); const [folderResult, setFolderResult] = useState(null); const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | "folders" | null>(null); const disabled = busy || !canWrite; const credentialDisabled = disabled || !canManageCredentials; const existingProfile = editing !== "new" ? editing : null; const draftHasImap = hasDraftImapSettings(draft); const useSavedSmtpTest = Boolean(existingProfile && !draft.smtpPassword && existingProfile.smtp_password_configured); const useSavedImapTest = Boolean(existingProfile && !draft.imapPassword && existingProfile.imap_password_configured); const policyMessages = useMemo(() => validateMailPolicy(effectivePolicy, { smtpHost: draft.smtpHost, imapHost: draft.imapHost }), [draft.imapHost, draft.smtpHost, effectivePolicy]); useEffect(() => { setSmtpTestResult(null); setImapTestResult(null); setFolderResult(null); setMailActionState(null); }, [editing]); function patchSmtpSettings(patch: Partial) { setDraft({ ...draft, smtpHost: patch.host !== undefined ? String(patch.host ?? "") : draft.smtpHost, smtpPort: patch.port !== undefined ? String(patch.port ?? "") : draft.smtpPort, smtpSecurity: patch.security !== undefined ? readSecurity(String(patch.security || "starttls"), "starttls") : draft.smtpSecurity, smtpTimeout: patch.timeout_seconds !== undefined ? String(patch.timeout_seconds ?? "") : draft.smtpTimeout }); } function patchImapSettings(patch: Partial) { setDraft({ ...draft, imapHost: patch.host !== undefined ? String(patch.host ?? "") : draft.imapHost, imapPort: patch.port !== undefined ? String(patch.port ?? "") : draft.imapPort, imapSecurity: patch.security !== undefined ? readSecurity(String(patch.security || "tls"), "tls") : draft.imapSecurity, imapSentFolder: patch.sent_folder !== undefined ? String(patch.sent_folder ?? "") : draft.imapSentFolder, imapTimeout: patch.timeout_seconds !== undefined ? String(patch.timeout_seconds ?? "") : draft.imapTimeout }); } function patchSmtpCredentials(patch: Partial) { setDraft({ ...draft, smtpUsername: patch.username !== undefined ? String(patch.username ?? "") : draft.smtpUsername, smtpPassword: patch.password !== undefined ? String(patch.password ?? "") : draft.smtpPassword }); } function patchImapCredentials(patch: Partial) { setDraft({ ...draft, imapUsername: patch.username !== undefined ? String(patch.username ?? "") : draft.imapUsername, imapPassword: patch.password !== undefined ? String(patch.password ?? "") : draft.imapPassword }); } async function runSmtpTest() { setMailActionState("smtp"); setSmtpTestResult(null); try { setSmtpTestResult(useSavedSmtpTest && existingProfile ? await testMailProfileSmtp(settings, existingProfile.id) : await testSmtpSettings(settings, rawSmtpPayload(draft, false))); } catch (err) { setSmtpTestResult({ ok: false, protocol: "smtp", message: errorMessage(err), details: {} }); } finally { setMailActionState(null); } } async function runImapTest() { if (!draftHasImap) return; setMailActionState("imap"); setImapTestResult(null); try { setImapTestResult(useSavedImapTest && existingProfile ? await testMailProfileImap(settings, existingProfile.id) : await testImapSettings(settings, rawImapPayload(draft, false))); } catch (err) { setImapTestResult({ ok: false, protocol: "imap", message: errorMessage(err), details: {} }); } finally { setMailActionState(null); } } async function runFolderLookup() { if (!draftHasImap) return; setMailActionState("folders"); setFolderResult(null); try { setFolderResult(useSavedImapTest && existingProfile ? await listMailProfileImapFolders(settings, existingProfile.id) : await listImapFolders(settings, rawImapPayload(draft, false))); } catch (err) { setFolderResult({ ok: false, message: errorMessage(err), folders: [] }); } finally { setMailActionState(null); } } function useDetectedSentFolder() { const folder = folderResult?.detected_sent_folder; if (!folder || disabled || !draftHasImap) return; setDraft({ ...draft, imapSentFolder: folder }); } return (
setDraft({ ...draft, name: event.target.value })} /> setDraft({ ...draft, slug: event.target.value })} placeholder="Generated from name" />