713 lines
38 KiB
TypeScript
713 lines
38 KiB
TypeScript
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
import { ActionToolbar, ActionBlockerHint, AdminSelectionList, DocumentationHelpLink, FieldLabel, LoadingFrame, PolicyLockedHint, PolicyPathHelp, PolicyRow, PolicySourcePath, PolicyTable, ToggleSwitch, normalizePolicySourcePathItems, type MailJmapTransportSettings, type NormalizedPolicySourcePathItem, type PolicySourcePathItem } from "@govoplan/core-webui";
|
|
import type { ApiSettings } from "../../types";
|
|
import { getMailProfilePolicy, mailProfilePatternKeys, mailProfilePolicyLimitKeys, updateMailProfilePolicy, type MailCredentialPolicy, type MailImapTestPayload, type MailProfilePatternKey, type MailProfilePatternRules, type MailProfilePolicy, type MailProfilePolicyLimitKey, type MailProfileScope, type MailServerProfile, type MailSmtpTestPayload } from "../../api/mail";
|
|
import { Button } from "@govoplan/core-webui";
|
|
import { Card } from "@govoplan/core-webui";
|
|
import { DismissibleAlert } from "@govoplan/core-webui";
|
|
import { i18nMessage, usePlatformLanguage, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
|
|
|
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<void>;
|
|
};
|
|
|
|
type PolicyFlagValue = "inherit" | "allow" | "deny";
|
|
|
|
export const MAIL_PROFILE_DOCUMENTATION = {
|
|
topicId: "mail.profiles-and-policy",
|
|
documentationType: "admin"
|
|
} as const;
|
|
|
|
const patternLabels: Record<MailProfilePatternKey, string> = {
|
|
smtp_hosts: "i18n:govoplan-mail.smtp_hostnames.36eb51d8",
|
|
imap_hosts: "i18n:govoplan-mail.imap_hostnames.ac9c1d78",
|
|
jmap_hosts: "JMAP hostnames",
|
|
envelope_senders: "i18n:govoplan-mail.envelope_senders.269065cd",
|
|
from_headers: "i18n:govoplan-mail.from_headers.b3ea473b",
|
|
recipient_domains: "i18n:govoplan-mail.recipient_domains.cb9b7b44"
|
|
};
|
|
|
|
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 MailProfilePolicyEditor({
|
|
settings,
|
|
scopeType,
|
|
scopeId = null,
|
|
campaignId = null,
|
|
profiles,
|
|
ownerUserId = null,
|
|
ownerGroupId = null,
|
|
canWrite,
|
|
locked = false,
|
|
title = "i18n:govoplan-mail.mail_profile_policy.f2ac4b92",
|
|
description = "i18n:govoplan-mail.allowed_profiles_and_wildcard_rules_for_this_sco.0f82b3e4",
|
|
onSaved
|
|
}: MailProfilePolicyEditorProps) {
|
|
const { translateText } = usePlatformLanguage();
|
|
const [policy, setPolicy] = useState<MailProfilePolicy>(blankPolicy);
|
|
const [effectivePolicy, setEffectivePolicy] = useState<MailProfilePolicy | null>(null);
|
|
const [parentPolicy, setParentPolicy] = useState<MailProfilePolicy | null>(null);
|
|
const [effectivePolicySources, setEffectivePolicySources] = useState<PolicySourcePathItem[]>([]);
|
|
const [savedPolicyKey, setSavedPolicyKey] = useState(policyDraftKey(blankPolicy));
|
|
const [loading, setLoading] = useState(false);
|
|
const [policyLoaded, setPolicyLoaded] = useState(false);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [success, setSuccess] = useState("");
|
|
const [refreshWarning, setRefreshWarning] = useState("");
|
|
|
|
const requiresTarget = scopeType === "user" || scopeType === "group" || scopeType === "campaign";
|
|
const scopeReady = !requiresTarget || Boolean(scopeId);
|
|
const policyDirty = scopeReady && policyDraftKey(policy) !== savedPolicyKey;
|
|
|
|
useUnsavedDraftGuard({
|
|
dirty: policyDirty,
|
|
onSave: savePolicy,
|
|
onDiscard: () => setPolicy(JSON.parse(savedPolicyKey) as MailProfilePolicy)
|
|
});
|
|
|
|
useEffect(() => {void loadPolicy();}, [settings.accessToken, settings.apiBaseUrl, settings.apiKey, scopeType, scopeId, campaignId]);
|
|
|
|
async function loadPolicy() {
|
|
setPolicyLoaded(false);
|
|
setError("");
|
|
setSuccess("");
|
|
setRefreshWarning("");
|
|
if (!scopeReady) {
|
|
setPolicy(blankPolicy);
|
|
setSavedPolicyKey(policyDraftKey(blankPolicy));
|
|
setEffectivePolicy(null);
|
|
setParentPolicy(null);
|
|
setEffectivePolicySources([]);
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
try {
|
|
const response = await getMailProfilePolicy(settings, scopeType, scopeId, campaignId);
|
|
const loadedPolicy = normalizePolicy(response.policy);
|
|
setPolicy(loadedPolicy);
|
|
setSavedPolicyKey(policyDraftKey(loadedPolicy));
|
|
setEffectivePolicy(response.effective_policy ? normalizePolicy(response.effective_policy) : null);
|
|
setParentPolicy(response.parent_policy ? normalizePolicy(response.parent_policy) : null);
|
|
setEffectivePolicySources(response.effective_policy_sources ?? []);
|
|
setPolicyLoaded(true);
|
|
} catch (err) {
|
|
setPolicy(blankPolicy);
|
|
setSavedPolicyKey(policyDraftKey(blankPolicy));
|
|
setEffectivePolicy(null);
|
|
setParentPolicy(null);
|
|
setEffectivePolicySources([]);
|
|
setError(errorMessage(err));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function savePolicy(): Promise<boolean> {
|
|
if (!scopeReady || !canWrite || locked || loading || !policyLoaded || busy) return false;
|
|
setBusy(true);
|
|
setError("");
|
|
setSuccess("");
|
|
setRefreshWarning("");
|
|
try {
|
|
const response = await updateMailProfilePolicy(settings, scopeType, normalizePolicyForSave(policy, parentPolicy, scopeType), scopeId);
|
|
const savedPolicy = normalizePolicy(response.policy);
|
|
setPolicy(savedPolicy);
|
|
setSavedPolicyKey(policyDraftKey(savedPolicy));
|
|
setEffectivePolicy(response.effective_policy ? normalizePolicy(response.effective_policy) : null);
|
|
setParentPolicy(response.parent_policy ? normalizePolicy(response.parent_policy) : null);
|
|
setEffectivePolicySources(response.effective_policy_sources ?? []);
|
|
setSuccess("i18n:govoplan-mail.mail_profile_policy_saved.666847bf");
|
|
try {
|
|
await onSaved?.();
|
|
} catch (refreshError) {
|
|
// The policy write has committed. A dependent refresh must never turn
|
|
// this into a failed save or encourage replaying the accepted write.
|
|
setRefreshWarning(i18nMessage("i18n:govoplan-mail.policy_saved_refresh_failed", { value0: errorMessage(refreshError) }));
|
|
}
|
|
return true;
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
return false;
|
|
} 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 || !policyLoaded || !canWrite || !scopeReady;
|
|
const policySaveBlocker = mailPolicyDisabledReason(locked, canWrite, scopeReady, loading || !policyLoaded, busy, policyDirty);
|
|
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 showAllowColumn = scopeType !== "campaign";
|
|
const showEffectiveColumn = !isSystem;
|
|
const profileAllowListLocked = !parentAllowsMailLimit("allowed_profile_ids");
|
|
const blockedProfileDefinitions = [
|
|
parentBlocksUserProfiles ? "user" : "",
|
|
parentBlocksGroupProfiles ? "group" : "",
|
|
parentBlocksCampaignProfiles ? "i18n:govoplan-mail.campaign_local_settings.920ecb62" : ""].
|
|
filter(Boolean).join(", ");
|
|
const effectivePolicyPath = effectivePolicySources.length > 0 ? effectivePolicySources : mailPolicySourcePath(scopeType);
|
|
|
|
function patchPolicy(patch: Partial<MailProfilePolicy>) {
|
|
setPolicy((current) => normalizePolicy({ ...current, ...patch }));
|
|
}
|
|
|
|
function setFlag(key: "allow_user_profiles" | "allow_group_profiles" | "allow_campaign_profiles", value: PolicyFlagValue) {
|
|
patchPolicy({ [key]: flagToBoolean(value) });
|
|
}
|
|
|
|
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 = "i18n:govoplan-mail.allow_override.ffa6e9a0"): ReactNode | undefined {
|
|
if (!showAllowColumn) return undefined;
|
|
const parentLocked = !parentAllowsMailLimit(key);
|
|
return (
|
|
<ToggleSwitch
|
|
checked={localAllowsMailLimit(key)}
|
|
disabled={disabled || parentLocked}
|
|
onChange={(checked) => setAllowLowerLevelLimit(key, checked)}
|
|
label={label} />);
|
|
|
|
|
|
}
|
|
|
|
return (
|
|
<Card
|
|
title={title}
|
|
actions={
|
|
<div className="button-row compact-actions">
|
|
<DocumentationHelpLink reference={MAIL_PROFILE_DOCUMENTATION} />
|
|
<Button onClick={() => void loadPolicy()} disabled={loading || busy || !scopeReady} disabledReason={loading ? "Mail policy is already loading." : busy ? "Wait for the current policy change to finish." : !scopeReady ? "Select a policy target before reloading." : undefined}>{loading ? "i18n:govoplan-mail.loading.33ce4174" : "i18n:govoplan-mail.reload.cce71553"}</Button>
|
|
<Button variant="primary" onClick={() => void savePolicy()} disabled={Boolean(policySaveBlocker)} disabledReason={policySaveBlocker}>{busy ? "i18n:govoplan-mail.saving.56a2285c" : "i18n:govoplan-mail.save_policy.77d67ce3"}</Button>
|
|
</div>
|
|
}>
|
|
|
|
<LoadingFrame loading={loading} label="i18n:govoplan-mail.loading_mail_profile_policy.b746a2e8">
|
|
<div className="mail-policy-editor">
|
|
{(locked || !canWrite || !scopeReady) &&
|
|
<ActionBlockerHint
|
|
tone={locked ? "warning" : "info"}
|
|
reason={mailPolicyBlockerReason(locked, canWrite, scopeReady)}
|
|
documentation={MAIL_PROFILE_DOCUMENTATION} />
|
|
}
|
|
{description && <p className="muted small-note mail-policy-description">{description}</p>}
|
|
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
|
{success && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>}
|
|
{refreshWarning && <DismissibleAlert tone="warning" resetKey={refreshWarning}>{refreshWarning}</DismissibleAlert>}
|
|
|
|
<section className="mail-policy-section policy-section">
|
|
<ActionToolbar surface="section-header" className="subsection-heading split">
|
|
<h3>i18n:govoplan-mail.profile_allow_list.507dfe6c</h3>
|
|
<div className="button-row compact-actions">
|
|
{lowerLevelLimitToggle("allowed_profile_ids")}
|
|
<Button onClick={() => patchPolicy({ allowed_profile_ids: [] })} disabled={disabled || profileAllowListLocked || selectedProfileIds.size === 0}>i18n:govoplan-mail.clear_allow_list.f69c8c67</Button>
|
|
</div>
|
|
</ActionToolbar>
|
|
<p className="muted small-note">{selectedProfileIds.size === 0 ? "i18n:govoplan-mail.no_local_profile_allow_list_is_set.31072e39" : i18nMessage("i18n:govoplan-mail.value_profile_s_allowed_by_this_scope.6fe9ba44", { value0: selectedProfileIds.size })}</p>
|
|
<AdminSelectionList
|
|
options={candidateProfiles.map((profile) => ({
|
|
id: profile.id,
|
|
label: profile.name,
|
|
description: `${scopeLabel(profile)} · ${transportLabel(profile.smtp)}`,
|
|
disabled: disabled || profileAllowListLocked || Boolean(parentAllowedProfileIds && !parentAllowedProfileIds.has(profile.id) && !selectedProfileIds.has(profile.id))
|
|
}))}
|
|
selected={[...selectedProfileIds]}
|
|
onChange={(allowedProfileIds) => patchPolicy({ allowed_profile_ids: [...allowedProfileIds].sort() })}
|
|
emptyText="i18n:govoplan-mail.no_profiles_are_visible_for_this_policy_scope.1ec7bd85"
|
|
/>
|
|
{parentAllowedProfileIds && <p className="muted small-note">i18n:govoplan-mail.an_ancestor_allow_list_limits_selectable_profile.499ec179 {parentAllowedProfileIds.size} i18n:govoplan-mail.profile_s.742e9200</p>}
|
|
</section>
|
|
|
|
<section className="mail-policy-section policy-section">
|
|
<h3>i18n:govoplan-mail.lower_level_mail_definitions.d39a0a1d</h3>
|
|
<PolicyTable className="mail-policy-table" rowClassName="mail-policy-row" headerClassName="mail-policy-row-header" fieldLabel="i18n:govoplan-mail.policy.bb9cf141" settingLabel={isSystem ? "i18n:govoplan-mail.value.8dce170d" : "i18n:govoplan-mail.local_setting.967607a9"} effectiveLabel="i18n:govoplan-mail.effective_policy.feedb950" lowerLevelLabel="i18n:govoplan-mail.lower_levels.940821ee" showAllowColumn={showAllowColumn} showEffectiveColumn={showEffectiveColumn}>
|
|
<PolicyRow
|
|
className="mail-policy-row"
|
|
labelClassName="mail-policy-field-label"
|
|
controlClassName="mail-policy-control"
|
|
effectiveCellClassName="mail-policy-effective-cell"
|
|
effectiveClassName="mail-policy-effective-value"
|
|
label="i18n:govoplan-mail.user_profiles.57730285"
|
|
help={policyHelp("i18n:govoplan-mail.controls_whether_user_scoped_mail_profiles_may_b.00c0e0e7")}
|
|
control={<PolicyFlagControl value={booleanToFlag(displayPolicy.allow_user_profiles)} disabled={disabled} includeInherit={!isSystem} inheritOnly={!isSystem && !parentAllowsMailLimit("allow_user_profiles")} allowDisabled={parentBlocksUserProfiles} onChange={(value) => setFlag("allow_user_profiles", value)} />}
|
|
effective={showEffectiveColumn ? effectiveBooleanLabel(effectivePolicy?.allow_user_profiles, effectivePolicy) : undefined}
|
|
allowControl={showAllowColumn ? <div className="mail-policy-lower-cell policy-lower-cell">{lowerLevelLimitToggle("allow_user_profiles")}</div> : undefined}
|
|
effectiveHelp={showEffectiveColumn ? mailBooleanPolicyPathHelp("allow_user_profiles", effectivePolicyPath) : undefined} />
|
|
|
|
<PolicyRow
|
|
className="mail-policy-row"
|
|
labelClassName="mail-policy-field-label"
|
|
controlClassName="mail-policy-control"
|
|
effectiveCellClassName="mail-policy-effective-cell"
|
|
effectiveClassName="mail-policy-effective-value"
|
|
label="i18n:govoplan-mail.group_profiles.74568838"
|
|
help={policyHelp("i18n:govoplan-mail.controls_whether_group_scoped_mail_profiles_may_.0b832ea4")}
|
|
control={<PolicyFlagControl value={booleanToFlag(displayPolicy.allow_group_profiles)} disabled={disabled} includeInherit={!isSystem} inheritOnly={!isSystem && !parentAllowsMailLimit("allow_group_profiles")} allowDisabled={parentBlocksGroupProfiles} onChange={(value) => setFlag("allow_group_profiles", value)} />}
|
|
effective={showEffectiveColumn ? effectiveBooleanLabel(effectivePolicy?.allow_group_profiles, effectivePolicy) : undefined}
|
|
allowControl={showAllowColumn ? <div className="mail-policy-lower-cell policy-lower-cell">{lowerLevelLimitToggle("allow_group_profiles")}</div> : undefined}
|
|
effectiveHelp={showEffectiveColumn ? mailBooleanPolicyPathHelp("allow_group_profiles", effectivePolicyPath) : undefined} />
|
|
|
|
<PolicyRow
|
|
className="mail-policy-row"
|
|
labelClassName="mail-policy-field-label"
|
|
controlClassName="mail-policy-control"
|
|
effectiveCellClassName="mail-policy-effective-cell"
|
|
effectiveClassName="mail-policy-effective-value"
|
|
label="i18n:govoplan-mail.campaign_local_settings.eb0f1061"
|
|
help={policyHelp("i18n:govoplan-mail.controls_whether_campaigns_may_use_inline_smtp_i.fa45cbbc")}
|
|
control={<PolicyFlagControl value={booleanToFlag(displayPolicy.allow_campaign_profiles)} disabled={disabled} includeInherit={!isSystem} inheritOnly={!isSystem && !parentAllowsMailLimit("allow_campaign_profiles")} allowDisabled={parentBlocksCampaignProfiles} onChange={(value) => setFlag("allow_campaign_profiles", value)} />}
|
|
effective={showEffectiveColumn ? effectiveBooleanLabel(effectivePolicy?.allow_campaign_profiles, effectivePolicy) : undefined}
|
|
allowControl={showAllowColumn ? <div className="mail-policy-lower-cell policy-lower-cell">{lowerLevelLimitToggle("allow_campaign_profiles")}</div> : undefined}
|
|
effectiveHelp={showEffectiveColumn ? mailBooleanPolicyPathHelp("allow_campaign_profiles", effectivePolicyPath) : undefined} />
|
|
|
|
</PolicyTable>
|
|
{blockedProfileDefinitions && <PolicyLockedHint>i18n:govoplan-mail.explicit_allow_is_unavailable_for.8d05fd4a {blockedProfileDefinitions} i18n:govoplan-mail.because_an_ancestor_policy_blocks_those_definiti.5de3e30d</PolicyLockedHint>}
|
|
</section>
|
|
|
|
<section className="mail-policy-section policy-section" data-testid="mail-credential-policy">
|
|
<h3>i18n:govoplan-mail.credential_selection_policy</h3>
|
|
<p className="muted small-note">i18n:govoplan-mail.credential_selection_policy_help</p>
|
|
<PolicyTable className="mail-policy-table" rowClassName="mail-policy-row" headerClassName="mail-policy-row-header" fieldLabel="i18n:govoplan-mail.policy.bb9cf141" settingLabel={isSystem ? "i18n:govoplan-mail.value.8dce170d" : "i18n:govoplan-mail.local_setting.967607a9"} effectiveLabel="i18n:govoplan-mail.effective_policy.feedb950" lowerLevelLabel="i18n:govoplan-mail.lower_levels.940821ee" showAllowColumn={showAllowColumn} showEffectiveColumn={showEffectiveColumn}>
|
|
{(["smtp", "imap"] as const).map((protocol) => {
|
|
const key = `${protocol}_credentials` as const;
|
|
const limitKey = `${key}.inherit` as MailProfilePolicyLimitKey;
|
|
const parentLocked = !parentAllowsMailLimit(limitKey);
|
|
const local = displayPolicy[key]?.inherit;
|
|
return <PolicyRow key={key}
|
|
className="mail-policy-row"
|
|
labelClassName="mail-policy-field-label"
|
|
controlClassName="mail-policy-control"
|
|
effectiveCellClassName="mail-policy-effective-cell"
|
|
effectiveClassName="mail-policy-effective-value"
|
|
label={`i18n:govoplan-mail.${protocol}_credential_selection`}
|
|
help={policyHelp("i18n:govoplan-mail.credential_selection_policy_help")}
|
|
control={<select aria-label={translateText(`i18n:govoplan-mail.${protocol}_credential_selection`)}
|
|
value={parentLocked || local == null ? "inherit" : local ? "profile" : "explicit"}
|
|
disabled={disabled || parentLocked}
|
|
onChange={(event) => patchPolicy({ [key]: { inherit: event.target.value === "inherit" ? null : event.target.value === "profile" } })}>
|
|
{(!isSystem || parentLocked) && <option value="inherit">i18n:govoplan-mail.credential_policy_parent</option>}
|
|
<option value="profile">i18n:govoplan-mail.credential_policy_profile</option>
|
|
<option value="explicit">i18n:govoplan-mail.credential_policy_explicit</option>
|
|
</select>}
|
|
effective={showEffectiveColumn ? effectivePolicy ? credentialSelectionLabel(effectivePolicy[key]?.inherit) : "i18n:govoplan-mail.loading.b04ba49f" : undefined}
|
|
effectiveHelp={showEffectiveColumn ? <PolicyPathHelp lines={mailCredentialPolicyPathLines(key, normalizePolicySourcePathItems(effectivePolicyPath))} /> : undefined}
|
|
allowControl={showAllowColumn ? <div className="mail-policy-lower-cell policy-lower-cell">{lowerLevelLimitToggle(limitKey)}</div> : undefined} />;
|
|
})}
|
|
</PolicyTable>
|
|
{(["smtp_credentials.inherit", "imap_credentials.inherit"] as const).some((key) => !parentAllowsMailLimit(key)) && <PolicyLockedHint>i18n:govoplan-mail.credential_policy_parent_locked</PolicyLockedHint>}
|
|
</section>
|
|
|
|
<section className="mail-policy-section policy-section">
|
|
<h3>i18n:govoplan-mail.wildcard_rules.54fb3fc0</h3>
|
|
<div className={`mail-policy-pattern-table policy-table${showAllowColumn ? " with-allow-column" : ""}`}>
|
|
<div className="mail-policy-pattern-row policy-row mail-policy-row-header policy-row-header">
|
|
<span>i18n:govoplan-mail.policy_target.a19dcee9</span>
|
|
<span>i18n:govoplan-mail.whitelist.53c2ad30</span>
|
|
<span>i18n:govoplan-mail.blacklist.7b2dd04c</span>
|
|
{showAllowColumn && <span>i18n:govoplan-mail.lower_levels.940821ee</span>}
|
|
</div>
|
|
{mailProfilePatternKeys.map((key) =>
|
|
<div className="mail-policy-pattern-row policy-row" key={key}>
|
|
<div className="mail-policy-field-label policy-field-label">
|
|
<FieldLabel className="mail-policy-field-title policy-field-title" help={policyHelp(patternPolicyNote(key))}>{patternLabels[key]}</FieldLabel>
|
|
</div>
|
|
<PatternTextareaControl value={patternsToText(policy.whitelist?.[key])} disabled={disabled || !parentAllowsMailLimit(`whitelist.${key}` as MailProfilePolicyLimitKey)} onChange={(text) => setPattern("whitelist", key, text)} />
|
|
<PatternTextareaControl value={patternsToText(policy.blacklist?.[key])} disabled={disabled || !parentAllowsMailLimit(`blacklist.${key}` as MailProfilePolicyLimitKey)} onChange={(text) => setPattern("blacklist", key, text)} />
|
|
{showAllowColumn &&
|
|
<div className="mail-policy-pattern-limits">
|
|
{lowerLevelLimitToggle(`whitelist.${key}` as MailProfilePolicyLimitKey, "i18n:govoplan-mail.whitelist.53c2ad30")}
|
|
{lowerLevelLimitToggle(`blacklist.${key}` as MailProfilePolicyLimitKey, "i18n:govoplan-mail.blacklist.7b2dd04c")}
|
|
</div>
|
|
}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
{showEffectiveColumn && effectivePolicy &&
|
|
<section className="mail-policy-section policy-section mail-policy-effective">
|
|
<h3>i18n:govoplan-mail.policy_path.1ba91ee5</h3>
|
|
<PolicySourcePath items={effectivePolicyPath} />
|
|
<p className="muted small-note">i18n:govoplan-mail.effective_values_are_shown_in_the_table_rows_abo.b27b900d</p>
|
|
</section>
|
|
}
|
|
</div>
|
|
</LoadingFrame>
|
|
</Card>);
|
|
|
|
}
|
|
|
|
function PolicyFlagControl({ value, disabled, includeInherit = true, inheritOnly = false, allowDisabled = false, onChange }: {value: PolicyFlagValue;disabled: boolean;includeInherit?: boolean;inheritOnly?: boolean;allowDisabled?: boolean;onChange: (value: PolicyFlagValue) => void;}) {
|
|
const selectedValue = inheritOnly ? "inherit" : value;
|
|
return (
|
|
<select value={selectedValue} disabled={disabled} onChange={(event) => onChange(event.target.value as PolicyFlagValue)}>
|
|
{(includeInherit || inheritOnly) && <option value="inherit">i18n:govoplan-mail.inherit.18f99833</option>}
|
|
{!inheritOnly && <option value="allow" disabled={allowDisabled}>i18n:govoplan-mail.explicit_allow.6a7946f8</option>}
|
|
{!inheritOnly && <option value="deny">i18n:govoplan-mail.deny.53577bb5</option>}
|
|
</select>);
|
|
|
|
}
|
|
|
|
function PatternTextareaControl({ value, disabled, onChange }: {value: string;disabled: boolean;onChange: (value: string) => void;}) {
|
|
return <textarea rows={3} value={value} disabled={disabled} onChange={(event) => onChange(event.target.value)} placeholder="*.example.org" />;
|
|
}
|
|
|
|
function mailPolicyDisabledReason(
|
|
locked: boolean,
|
|
canWrite: boolean,
|
|
scopeReady: boolean,
|
|
loading: boolean,
|
|
busy: boolean,
|
|
dirty: boolean
|
|
): string {
|
|
if (locked) return "This policy is locked by the owning workflow or a higher-scope decision.";
|
|
if (!canWrite) return "Mail policy administration permission is required to save changes.";
|
|
if (!scopeReady) return "Select a policy target before saving.";
|
|
if (loading) return "Wait until the effective Mail policy has loaded.";
|
|
if (busy) return "Wait for the current policy change to finish.";
|
|
if (!dirty) return "There are no unsaved Mail policy changes.";
|
|
return "";
|
|
}
|
|
|
|
function mailPolicyBlockerReason(locked: boolean, _canWrite: boolean, scopeReady: boolean) {
|
|
if (locked) {
|
|
return {
|
|
summary: "This Mail policy is locked in the current context.",
|
|
details: "The effective values remain visible, but this workflow or a higher-scope decision owns the editable policy.",
|
|
requiredAction: "Change the owning policy or leave the governed workflow before editing.",
|
|
actor: "The administrator or workflow owner responsible for the source policy",
|
|
target: "The source shown in the effective policy path"
|
|
};
|
|
}
|
|
if (!scopeReady) {
|
|
return {
|
|
summary: "Select a target before editing Mail policy.",
|
|
details: "User, group, and campaign policy must be resolved against one concrete target.",
|
|
requiredAction: "Choose the target in the scope selector.",
|
|
actor: "Mail policy administrator",
|
|
target: "The target selector above"
|
|
};
|
|
}
|
|
return {
|
|
summary: "You can review effective Mail policy here, but cannot change it.",
|
|
details: "Policy changes require Mail policy administration authority at this scope.",
|
|
requiredAction: "Ask an authorized administrator to apply the change.",
|
|
actor: "System or tenant policy administrator",
|
|
target: "Administration > Mail profiles and policy"
|
|
};
|
|
}
|
|
|
|
function normalizePolicy(value: MailProfilePolicy | null | undefined): MailProfilePolicy {
|
|
return {
|
|
allowed_profile_ids: [...(value?.allowed_profile_ids ?? [])].filter(Boolean),
|
|
allow_user_profiles: value?.allow_user_profiles ?? null,
|
|
allow_group_profiles: value?.allow_group_profiles ?? null,
|
|
allow_campaign_profiles: value?.allow_campaign_profiles ?? null,
|
|
smtp_credentials: normalizeCredentialPolicy(value?.smtp_credentials),
|
|
imap_credentials: normalizeCredentialPolicy(value?.imap_credentials),
|
|
whitelist: normalizeRules(value?.whitelist),
|
|
blacklist: normalizeRules(value?.blacklist),
|
|
allow_lower_level_limits: normalizeMailLowerLevelLimits(value?.allow_lower_level_limits)
|
|
};
|
|
}
|
|
|
|
function policyDraftKey(policy: MailProfilePolicy): string {
|
|
return JSON.stringify(normalizePolicy(policy));
|
|
}
|
|
|
|
function normalizePolicyForSave(policy: MailProfilePolicy, parentPolicy: MailProfilePolicy | null, scopeType: MailProfileScope): MailProfilePolicy {
|
|
const normalized = normalizePolicy(policy);
|
|
if (scopeType === "system") return concreteSystemPolicy(normalized);
|
|
const localLimits = { ...(normalized.allow_lower_level_limits ?? {}) };
|
|
const parentLimits = parentPolicy?.allow_lower_level_limits ?? null;
|
|
|
|
function parentAllows(key: MailProfilePolicyLimitKey): boolean {
|
|
return !parentLimits || parentLimits[key] !== false;
|
|
}
|
|
|
|
function clearLimit(key: MailProfilePolicyLimitKey) {
|
|
delete localLimits[key];
|
|
}
|
|
|
|
if (!parentAllows("allowed_profile_ids")) {
|
|
normalized.allowed_profile_ids = [];
|
|
clearLimit("allowed_profile_ids");
|
|
}
|
|
for (const key of ["allow_user_profiles", "allow_group_profiles", "allow_campaign_profiles"] as const) {
|
|
if (!parentAllows(key)) {
|
|
normalized[key] = null;
|
|
clearLimit(key);
|
|
}
|
|
}
|
|
for (const protocol of ["smtp_credentials", "imap_credentials"] as const) {
|
|
const credential = normalizeCredentialPolicy(normalized[protocol]);
|
|
const inheritKey = `${protocol}.inherit` as MailProfilePolicyLimitKey;
|
|
if (!parentAllows(inheritKey)) {
|
|
credential.inherit = null;
|
|
clearLimit(inheritKey);
|
|
}
|
|
normalized[protocol] = credential;
|
|
}
|
|
for (const key of mailProfilePatternKeys) {
|
|
const whitelistKey = `whitelist.${key}` as MailProfilePolicyLimitKey;
|
|
const blacklistKey = `blacklist.${key}` as MailProfilePolicyLimitKey;
|
|
if (!parentAllows(whitelistKey)) {
|
|
delete normalized.whitelist?.[key];
|
|
clearLimit(whitelistKey);
|
|
}
|
|
if (!parentAllows(blacklistKey)) {
|
|
delete normalized.blacklist?.[key];
|
|
clearLimit(blacklistKey);
|
|
}
|
|
}
|
|
if (scopeType === "campaign") {
|
|
normalized.allow_lower_level_limits = {};
|
|
} else {
|
|
normalized.allow_lower_level_limits = localLimits;
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function normalizeMailLowerLevelLimits(value: MailProfilePolicy["allow_lower_level_limits"]): Partial<Record<MailProfilePolicyLimitKey, boolean>> {
|
|
const result: Partial<Record<MailProfilePolicyLimitKey, boolean>> = {};
|
|
for (const key of mailProfilePolicyLimitKeys) {
|
|
if (typeof value?.[key] === "boolean") result[key] = value[key];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function fullMailLowerLevelLimits(value: MailProfilePolicy["allow_lower_level_limits"]): Record<MailProfilePolicyLimitKey, boolean> {
|
|
const result = {} as Record<MailProfilePolicyLimitKey, boolean>;
|
|
for (const key of mailProfilePolicyLimitKeys) {
|
|
result[key] = value?.[key] !== false;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function concreteSystemPolicy(policy: MailProfilePolicy): MailProfilePolicy {
|
|
const normalized = normalizePolicy(policy);
|
|
return {
|
|
...normalized,
|
|
allow_user_profiles: normalized.allow_user_profiles ?? true,
|
|
allow_group_profiles: normalized.allow_group_profiles ?? true,
|
|
allow_campaign_profiles: normalized.allow_campaign_profiles ?? true,
|
|
smtp_credentials: concreteSystemCredentialPolicy(normalized.smtp_credentials),
|
|
imap_credentials: concreteSystemCredentialPolicy(normalized.imap_credentials),
|
|
allow_lower_level_limits: fullMailLowerLevelLimits(normalized.allow_lower_level_limits)
|
|
};
|
|
}
|
|
|
|
function concreteSystemCredentialPolicy(value: MailCredentialPolicy | null | undefined): MailCredentialPolicy {
|
|
const normalized = normalizeCredentialPolicy(value);
|
|
return { inherit: normalized.inherit ?? true };
|
|
}
|
|
|
|
function normalizeCredentialPolicy(value: MailCredentialPolicy | null | undefined): MailCredentialPolicy {
|
|
return { inherit: typeof value?.inherit === "boolean" ? value.inherit : null };
|
|
}
|
|
|
|
function normalizeRules(value: MailProfilePatternRules | null | undefined): MailProfilePatternRules {
|
|
const result: MailProfilePatternRules = {};
|
|
for (const key of mailProfilePatternKeys) {
|
|
const patterns = (value?.[key] ?? []).map((pattern) => pattern.trim()).filter(Boolean);
|
|
if (patterns.length > 0) result[key] = patterns;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function profileCandidatesForPolicy(profiles: MailServerProfile[], scopeType: MailProfileScope, scopeId: string | null, ownerUserId: string | null, ownerGroupId: string | null): MailServerProfile[] {
|
|
return profiles.
|
|
filter((profile) => {
|
|
if (scopeType === "system") return profile.scope_type === "system";
|
|
if (profile.scope_type === "system" || profile.scope_type === "tenant") return true;
|
|
if (scopeType === "user") return profile.scope_type === "user" && profile.scope_id === scopeId;
|
|
if (scopeType === "group") return profile.scope_type === "group" && profile.scope_id === scopeId;
|
|
if (scopeType === "campaign") {
|
|
if (profile.scope_type === "campaign") return profile.scope_id === scopeId;
|
|
if (profile.scope_type === "user") return Boolean(ownerUserId) && profile.scope_id === ownerUserId;
|
|
if (profile.scope_type === "group") return Boolean(ownerGroupId) && profile.scope_id === ownerGroupId;
|
|
}
|
|
return false;
|
|
}).
|
|
sort((a, b) => `${scopeOrder(a.scope_type)}:${a.name}`.localeCompare(`${scopeOrder(b.scope_type)}:${b.name}`));
|
|
}
|
|
|
|
export function scopeOrder(scopeType: MailProfileScope): number {
|
|
if (scopeType === "system") return 0;
|
|
if (scopeType === "tenant") return 1;
|
|
if (scopeType === "user" || scopeType === "group") return 2;
|
|
return 3;
|
|
}
|
|
|
|
function policyHelp(description: string): ReactNode {
|
|
return <span>{description}</span>;
|
|
}
|
|
|
|
function credentialSelectionLabel(inherit: boolean | null | undefined): string {
|
|
return inherit === false ? "i18n:govoplan-mail.credential_policy_explicit" : "i18n:govoplan-mail.credential_policy_profile";
|
|
}
|
|
|
|
function mailCredentialPolicyPathLines(key: "smtp_credentials" | "imap_credentials", items: NormalizedPolicySourcePathItem[]): string[] {
|
|
const lines: string[] = [];
|
|
for (const [index, item] of items.entries()) {
|
|
const policy = policySourceRecord(item);
|
|
const value = asRecord(policy[key]).inherit;
|
|
const label = typeof value === "boolean" ? credentialSelectionLabel(value) : "i18n:govoplan-mail.credential_policy_parent";
|
|
const locked = asRecord(policy.allow_lower_level_limits)[`${key}.inherit`] === false;
|
|
lines.push(i18nMessage(locked ? "i18n:govoplan-mail.credential_policy_path_locked" : "i18n:govoplan-mail.credential_policy_path", { value0: `${policyPathPrefix(index)}${item.label}`, value1: label }));
|
|
if (locked) break;
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
function mailBooleanPolicyPathHelp(key: "allow_user_profiles" | "allow_group_profiles" | "allow_campaign_profiles", sources: PolicySourcePathItem[]): ReactNode {
|
|
return <PolicyPathHelp lines={mailBooleanPolicyPathLines(key, normalizePolicySourcePathItems(sources))} />;
|
|
}
|
|
|
|
function mailBooleanPolicyPathLines(key: "allow_user_profiles" | "allow_group_profiles" | "allow_campaign_profiles", items: NormalizedPolicySourcePathItem[]): string[] {
|
|
if (items.length === 0) return ["i18n:govoplan-mail.system_allow.ed6744b1"];
|
|
const lines: string[] = [];
|
|
for (const [index, item] of items.entries()) {
|
|
const policy = policySourceRecord(item);
|
|
const rawValue = policy[key];
|
|
const value = rawValue === true ? "i18n:govoplan-mail.allow.3ad0e369" : rawValue === false ? "i18n:govoplan-mail.deny.53577bb5" : "i18n:govoplan-mail.inherit.18f99833";
|
|
const lowerLocked = asRecord(policy.allow_lower_level_limits)[key] === false;
|
|
const stops = rawValue === false || lowerLocked;
|
|
lines.push(`${policyPathPrefix(index)}${item.label}: ${stops ? `${value} without override` : value}`);
|
|
if (stops) break;
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
function policySourceRecord(item: NormalizedPolicySourcePathItem): Record<string, unknown> {
|
|
return asRecord(item.policy);
|
|
}
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
}
|
|
|
|
function policyPathPrefix(index: number): string {
|
|
return index === 0 ? "" : `${" ".repeat(index - 1)}> `;
|
|
}
|
|
|
|
function effectiveBooleanLabel(value: boolean | null | undefined, policy: MailProfilePolicy | null): string {
|
|
if (!policy) return "i18n:govoplan-mail.loading.b04ba49f";
|
|
return value ? "i18n:govoplan-mail.allowed.77c7b490" : "i18n:govoplan-mail.blocked.99613c74";
|
|
}
|
|
|
|
function patternPolicyNote(key: MailProfilePatternKey): string {
|
|
if (key === "smtp_hosts") return "i18n:govoplan-mail.smtp_server_host_patterns.cf6120c3";
|
|
if (key === "imap_hosts") return "i18n:govoplan-mail.imap_server_host_patterns.52b20b83";
|
|
if (key === "envelope_senders") return "i18n:govoplan-mail.smtp_envelope_sender_patterns.8c1fd95e";
|
|
if (key === "from_headers") return "i18n:govoplan-mail.visible_from_header_patterns.ea77d99d";
|
|
return "i18n:govoplan-mail.recipient_domain_patterns.68466f5b";
|
|
}
|
|
|
|
function booleanToFlag(value: boolean | null | undefined): PolicyFlagValue {
|
|
if (value === true) return "allow";
|
|
if (value === false) return "deny";
|
|
return "inherit";
|
|
}
|
|
|
|
function flagToBoolean(value: PolicyFlagValue): boolean | null {
|
|
if (value === "allow") return true;
|
|
if (value === "deny") return false;
|
|
return null;
|
|
}
|
|
|
|
function parsePatternList(value: string): string[] {
|
|
const seen = new Set<string>();
|
|
const result: string[] = [];
|
|
for (const item of value.split(/[\n,]+/)) {
|
|
const pattern = item.trim();
|
|
if (pattern && !seen.has(pattern)) {
|
|
seen.add(pattern);
|
|
result.push(pattern);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function patternsToText(value: string[] | undefined): string {
|
|
return (value ?? []).join("\n");
|
|
}
|
|
|
|
function mailPolicySourcePath(scopeType: MailProfileScope): string[] {
|
|
if (scopeType === "system") return ["i18n:govoplan-mail.system.bc0792d8"];
|
|
if (scopeType === "tenant") return ["i18n:govoplan-mail.system.bc0792d8", "i18n:govoplan-mail.tenant.3ca93c78"];
|
|
if (scopeType === "user") return ["i18n:govoplan-mail.system.bc0792d8", "i18n:govoplan-mail.tenant.3ca93c78", "i18n:govoplan-mail.user.9f8a2389"];
|
|
if (scopeType === "group") return ["i18n:govoplan-mail.system.bc0792d8", "i18n:govoplan-mail.tenant.3ca93c78", "i18n:govoplan-mail.group.171a0606"];
|
|
return ["i18n:govoplan-mail.system.bc0792d8", "i18n:govoplan-mail.tenant.3ca93c78", "i18n:govoplan-mail.owner_policy.1e8df143", "i18n:govoplan-mail.campaign.69390e16"];
|
|
}
|
|
|
|
export function transportLabel(transport: MailSmtpTestPayload | MailImapTestPayload | MailJmapTransportSettings | null | undefined): string {
|
|
if (!transport) return "i18n:govoplan-mail.not_configured.811931bb";
|
|
if ("session_url" in transport) return transport.session_url || "No Session URL";
|
|
const host = transport.host || "i18n:govoplan-mail.no_host.4c710d7d";
|
|
const port = transport.port ? `:${transport.port}` : "";
|
|
return `${host}${port}`;
|
|
}
|
|
|
|
export function scopeLabel(profile: MailServerProfile): string {
|
|
if (profile.scope_type === "system") return "i18n:govoplan-mail.system.bc0792d8";
|
|
if (profile.scope_type === "tenant") return "i18n:govoplan-mail.tenant.3ca93c78";
|
|
if (profile.scope_type === "user") return "i18n:govoplan-mail.user.9f8a2389";
|
|
if (profile.scope_type === "group") return "i18n:govoplan-mail.group.171a0606";
|
|
return "i18n:govoplan-mail.campaign.69390e16";
|
|
}
|
|
|
|
export function errorMessage(err: unknown): string {
|
|
return err instanceof Error ? err.message : String(err);
|
|
}
|