1830 lines
82 KiB
TypeScript
1830 lines
82 KiB
TypeScript
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
import { AdminSelectionList, ConnectionTree, FieldLabel, LoadingFrame, MailServerSettingsPanel, PolicyLockedHint, PolicyPathHelp, PolicyRow, PolicySourcePath, PolicyTable, StatusBadge, TableActionGroup, ToggleSwitch, hasMailImapSettings, mailImapSettingsPayload, mailServerSecurityOptions, mailSmtpSettingsPayload, mailTextOrNull, mailTransportCredentialsPayload, mergeDeltaRows, normalizeMailServerSecurity, normalizePolicySourcePathItems, useDeltaWatermarks, type ConnectionTreeColumn, type MailServerConnectionTestResult, type MailServerCredentialSettings, type MailServerImapSettings, type MailServerSmtpSettings, type NormalizedPolicySourcePathItem, type PolicySourcePathItem } from "@govoplan/core-webui";
|
|
import { Link2, Pencil, Plus, Trash2, Unlink } from "lucide-react";
|
|
import type { ApiSettings } from "../../types";
|
|
import {
|
|
bindMailServerCredential,
|
|
createMailServerCredential,
|
|
createMailServerEndpoint,
|
|
createMailServerProfile,
|
|
deactivateMailServerEndpoint,
|
|
deactivateMailServerProfile,
|
|
fetchMailSettingsDelta,
|
|
getMailProfilePolicy,
|
|
mailProfilePatternKeys,
|
|
mailProfilePolicyLimitKeys,
|
|
listAvailableMailCredentials,
|
|
updateMailProfilePolicy,
|
|
testImapSettings,
|
|
testMailProfileImap,
|
|
testMailProfileSmtp,
|
|
testSmtpSettings,
|
|
unlinkMailServerCredential,
|
|
updateMailServerCredential,
|
|
updateMailServerEndpoint,
|
|
updateMailServerProfile,
|
|
type MailCredentialEnvelope,
|
|
type MailCredentialPolicy,
|
|
type MailImapTestPayload,
|
|
type MailProfilePatternKey,
|
|
type MailProfilePatternRules,
|
|
type MailProfilePolicy,
|
|
type MailProfilePolicyLimitKey,
|
|
type MailProfileScope,
|
|
type MailSecurity,
|
|
type MailServerEndpoint,
|
|
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 { Dialog } from "@govoplan/core-webui";
|
|
import { DismissibleAlert } from "@govoplan/core-webui";
|
|
import { FormField, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
|
import {
|
|
mailProfileEditTargetInitialSection,
|
|
mailProfileEditTargetPanelMode,
|
|
mailProfileEditTargetShowsProfileFields,
|
|
mailProfileEditTargetShowsSettingsPanel,
|
|
mailProfileEditTargetVisibleSections,
|
|
mailProfileCreateCredentialsPayload,
|
|
type MailProfileEditTarget,
|
|
type MailProfileProtocol
|
|
} from "./mailProfileEditorModel";
|
|
export type MailProfileTargetOption = {
|
|
id: string;
|
|
label: string;
|
|
secondary?: string | null;
|
|
};
|
|
|
|
type ProfileDraft = {
|
|
name: string;
|
|
slug: string;
|
|
description: string;
|
|
isActive: boolean;
|
|
inheritToLowerScopes: boolean;
|
|
serverName: string;
|
|
serverIsDefault: boolean;
|
|
serverIsActive: boolean;
|
|
serverInheritToLowerScopes: boolean;
|
|
credentialName: string;
|
|
credentialDescription: string;
|
|
credentialAllowedModules: string;
|
|
credentialAllowedServerRefs: string;
|
|
credentialInheritToLowerScopes: boolean;
|
|
credentialIsDefault: 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<void>;
|
|
};
|
|
|
|
type EditingProfile = MailServerProfile | "new" | null;
|
|
type PolicyFlagValue = "inherit" | "allow" | "deny";
|
|
type MailProfileTreeRow =
|
|
{kind: "profile";id: string;profile: MailServerProfile;} |
|
|
{kind: "server";id: string;profile: MailServerProfile;protocol: MailProfileProtocol;server: MailServerEndpoint;} |
|
|
{kind: "credential";id: string;profile: MailServerProfile;protocol: MailProfileProtocol;server: MailServerEndpoint;credential: MailCredentialEnvelope;};
|
|
|
|
type PendingHierarchyRemoval =
|
|
| {kind: "server";profile: MailServerProfile;server: MailServerEndpoint;}
|
|
| {kind: "credential";profile: MailServerProfile;server: MailServerEndpoint;credential: MailCredentialEnvelope;}
|
|
| null;
|
|
|
|
const securityOptions = mailServerSecurityOptions as readonly MailSecurity[];
|
|
|
|
const patternLabels: Record<MailProfilePatternKey, string> = {
|
|
smtp_hosts: "i18n:govoplan-mail.smtp_hostnames.36eb51d8",
|
|
imap_hosts: "i18n:govoplan-mail.imap_hostnames.ac9c1d78",
|
|
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 MailProfileScopeManager({
|
|
settings,
|
|
scopeType,
|
|
scopeId = null,
|
|
targetOptions = [],
|
|
targetLabel = "i18n:govoplan-mail.target.61ad50a9",
|
|
profileTitle = "i18n:govoplan-mail.mail_server_profiles.b1726682",
|
|
policyTitle = "i18n:govoplan-mail.mail_profile_policy.f2ac4b92",
|
|
canWriteProfiles,
|
|
canManageCredentials,
|
|
canWritePolicy
|
|
}: MailProfileScopeManagerProps) {
|
|
const [profiles, setProfiles] = useState<MailServerProfile[]>([]);
|
|
const [selectedTargetId, setSelectedTargetId] = useState(scopeId || targetOptions[0]?.id || "");
|
|
const [editing, setEditing] = useState<EditingProfile>(null);
|
|
const [editingTarget, setEditingTarget] = useState<MailProfileEditTarget>({ kind: "create" });
|
|
const [deactivating, setDeactivating] = useState<MailServerProfile | null>(null);
|
|
const [pendingHierarchyRemoval, setPendingHierarchyRemoval] = useState<PendingHierarchyRemoval>(null);
|
|
const [availableCredentials, setAvailableCredentials] = useState<MailCredentialEnvelope[]>([]);
|
|
const [reuseCredentialId, setReuseCredentialId] = useState("");
|
|
const [savedReuseCredentialId, setSavedReuseCredentialId] = useState("");
|
|
const [draft, setDraft] = useState<ProfileDraft>(emptyProfileDraft());
|
|
const [savedProfileDraftKey, setSavedProfileDraftKey] = useState(profileDraftKey(emptyProfileDraft()));
|
|
const [loading, setLoading] = useState(true);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [success, setSuccess] = useState("");
|
|
const [profileEffectivePolicy, setProfileEffectivePolicy] = useState<MailProfilePolicy | null>(null);
|
|
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
|
|
|
|
const requiresTarget = scopeType === "user" || scopeType === "group" || scopeType === "campaign";
|
|
const targetSelectionRequired = requiresTarget && !scopeId;
|
|
const hasSelectableTarget = targetOptions.length > 0;
|
|
const activeScopeId = scopeId || selectedTargetId || null;
|
|
const deltaKey = `mail:settings:${scopeType}:${activeScopeId ?? ""}`;
|
|
const scopeReady = !requiresTarget || Boolean(activeScopeId);
|
|
const targetEmptyText = i18nMessage("i18n:govoplan-mail.no_value_available", { value0: targetPluralLabel(scopeType, targetLabel) });
|
|
const profileDirty = editing !== null && (
|
|
profileDraftKey(draft) !== savedProfileDraftKey
|
|
|| reuseCredentialId !== savedReuseCredentialId
|
|
);
|
|
|
|
useUnsavedDraftGuard({
|
|
dirty: profileDirty,
|
|
onSave: saveProfile,
|
|
onDiscard: closeProfileEditor
|
|
});
|
|
|
|
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(() => {
|
|
resetDeltaWatermark(deltaKey);
|
|
void loadProfiles(true);
|
|
}, [deltaKey, scopeReady, settings.accessToken, settings.apiBaseUrl, settings.apiKey, resetDeltaWatermark]);
|
|
|
|
async function loadProfiles(forceFull = false) {
|
|
setLoading(true);
|
|
setError("");
|
|
if (!scopeReady) {
|
|
setProfiles([]);
|
|
setProfileEffectivePolicy(null);
|
|
resetDeltaWatermark(deltaKey);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
try {
|
|
let nextProfiles = forceFull ? [] : profiles;
|
|
let nextEffectivePolicy = forceFull ? null : profileEffectivePolicy;
|
|
let nextWatermark = forceFull ? null : getDeltaWatermark(deltaKey);
|
|
let hasMore = false;
|
|
do {
|
|
const response = await fetchMailSettingsDelta(settings, {
|
|
scope_type: scopeType,
|
|
scope_id: activeScopeId,
|
|
include_inactive: true,
|
|
since: nextWatermark
|
|
});
|
|
nextProfiles = response.full ?
|
|
response.profiles :
|
|
mergeDeltaRows(nextProfiles, response.profiles, response.deleted, (profile) => profile.id, {
|
|
deletedResourceType: "mail_profile",
|
|
sort: sortMailProfiles
|
|
});
|
|
if (response.policy) {
|
|
nextEffectivePolicy = response.policy.effective_policy ?? null;
|
|
}
|
|
nextWatermark = response.watermark ?? null;
|
|
hasMore = response.has_more;
|
|
} while (hasMore);
|
|
setProfiles(nextProfiles);
|
|
setProfileEffectivePolicy(nextEffectivePolicy);
|
|
setDeltaWatermark(deltaKey, nextWatermark);
|
|
} catch (err) {
|
|
setProfiles([]);
|
|
setProfileEffectivePolicy(null);
|
|
resetDeltaWatermark(deltaKey);
|
|
setError(errorMessage(err));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
const scopedProfiles = useMemo(
|
|
() => profiles.filter((profile) => profileBelongsToEditableScope(profile, scopeType, activeScopeId)),
|
|
[activeScopeId, profiles, scopeType]
|
|
);
|
|
|
|
function openCreate() {
|
|
const nextDraft = emptyProfileDraft();
|
|
setDraft(nextDraft);
|
|
setSavedProfileDraftKey(profileDraftKey(nextDraft));
|
|
setAvailableCredentials([]);
|
|
setReuseCredentialId("");
|
|
setSavedReuseCredentialId("");
|
|
setEditingTarget({ kind: "create" });
|
|
setEditing("new");
|
|
setError("");
|
|
setSuccess("");
|
|
}
|
|
|
|
function openEdit(profile: MailServerProfile, target: MailProfileEditTarget = { kind: "profile" }) {
|
|
const nextDraft = profileToDraft(profile, target);
|
|
setDraft(nextDraft);
|
|
setSavedProfileDraftKey(profileDraftKey(nextDraft));
|
|
setAvailableCredentials([]);
|
|
setReuseCredentialId("");
|
|
setSavedReuseCredentialId("");
|
|
setEditingTarget(target);
|
|
setEditing(profile);
|
|
setError("");
|
|
setSuccess("");
|
|
if (target.kind === "credentials" && target.serverId && !target.credentialId) {
|
|
void loadReusableCredentials(profile.id, target.serverId);
|
|
}
|
|
}
|
|
|
|
function openCreateServer(profile: MailServerProfile, protocol: MailProfileProtocol) {
|
|
openEdit(profile, { kind: "server", protocol });
|
|
}
|
|
|
|
function openCreateCredential(profile: MailServerProfile, server: MailServerEndpoint) {
|
|
openEdit(profile, {
|
|
kind: "credentials",
|
|
protocol: server.protocol,
|
|
serverId: server.id
|
|
});
|
|
}
|
|
|
|
async function loadReusableCredentials(profileId: string, serverId: string) {
|
|
try {
|
|
setAvailableCredentials(await listAvailableMailCredentials(settings, profileId, serverId));
|
|
} catch (err) {
|
|
setAvailableCredentials([]);
|
|
setError(errorMessage(err));
|
|
}
|
|
}
|
|
|
|
function closeProfileEditor() {
|
|
setEditing(null);
|
|
const nextDraft = emptyProfileDraft();
|
|
setDraft(nextDraft);
|
|
setSavedProfileDraftKey(profileDraftKey(nextDraft));
|
|
setAvailableCredentials([]);
|
|
setReuseCredentialId("");
|
|
setSavedReuseCredentialId("");
|
|
setEditingTarget({ kind: "create" });
|
|
}
|
|
|
|
async function saveProfile(): Promise<boolean> {
|
|
if (!editing || !scopeReady) return false;
|
|
setBusy(true);
|
|
setError("");
|
|
setSuccess("");
|
|
try {
|
|
if (editing === "new") {
|
|
const payload = createProfilePayload(draft, scopeType, activeScopeId);
|
|
const created = await createMailServerProfile(settings, payload);
|
|
setSuccess(i18nMessage("i18n:govoplan-mail.profile_value_created.2a088d8d", { value0: created.name }));
|
|
} else if (editingTarget.kind === "server") {
|
|
const serverPayload = {
|
|
name: draft.serverName.trim(),
|
|
config: editingTarget.protocol === "smtp" ? smtpServerPayload(draft) : imapServerPayload(draft),
|
|
inherit_to_lower_scopes: draft.serverInheritToLowerScopes,
|
|
is_default: draft.serverIsDefault,
|
|
is_active: draft.serverIsActive
|
|
};
|
|
if (editingTarget.serverId) {
|
|
await updateMailServerEndpoint(settings, editing.id, editingTarget.serverId, serverPayload);
|
|
} else {
|
|
await createMailServerEndpoint(settings, editing.id, {
|
|
protocol: editingTarget.protocol,
|
|
...serverPayload
|
|
});
|
|
}
|
|
setSuccess(`${draft.serverName.trim()} saved`);
|
|
} else if (editingTarget.kind === "credentials" && editingTarget.serverId) {
|
|
if (!editingTarget.credentialId && reuseCredentialId) {
|
|
const reused = availableCredentials.find((credential) => credential.id === reuseCredentialId);
|
|
await bindMailServerCredential(
|
|
settings,
|
|
editing.id,
|
|
editingTarget.serverId,
|
|
reuseCredentialId,
|
|
draft.credentialIsDefault
|
|
);
|
|
setSuccess(`${reused?.name || "Credential"} linked`);
|
|
} else {
|
|
const payload = credentialPayload(draft, editingTarget.protocol, Boolean(editingTarget.credentialId));
|
|
if (editingTarget.credentialId) {
|
|
await updateMailServerCredential(
|
|
settings,
|
|
editing.id,
|
|
editingTarget.serverId,
|
|
editingTarget.credentialId,
|
|
payload
|
|
);
|
|
} else {
|
|
await createMailServerCredential(settings, editing.id, editingTarget.serverId, payload);
|
|
}
|
|
setSuccess(`${draft.credentialName.trim()} saved`);
|
|
}
|
|
} else {
|
|
const payload = updateProfilePayload(draft);
|
|
const updated = await updateMailServerProfile(settings, editing.id, payload);
|
|
setSuccess(i18nMessage("i18n:govoplan-mail.profile_value_updated.fdbad0ea", { value0: updated.name }));
|
|
}
|
|
closeProfileEditor();
|
|
await loadProfiles();
|
|
return true;
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
return false;
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
async function deactivateProfile() {
|
|
if (!deactivating) return;
|
|
setBusy(true);
|
|
setError("");
|
|
setSuccess("");
|
|
try {
|
|
await deactivateMailServerProfile(settings, deactivating.id);
|
|
setSuccess(i18nMessage("i18n:govoplan-mail.profile_value_deactivated.fa7fcc1a", { value0: deactivating.name }));
|
|
setDeactivating(null);
|
|
await loadProfiles();
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
async function removeHierarchyItem() {
|
|
if (!pendingHierarchyRemoval) return;
|
|
setBusy(true);
|
|
setError("");
|
|
setSuccess("");
|
|
try {
|
|
if (pendingHierarchyRemoval.kind === "server") {
|
|
await deactivateMailServerEndpoint(
|
|
settings,
|
|
pendingHierarchyRemoval.profile.id,
|
|
pendingHierarchyRemoval.server.id
|
|
);
|
|
setSuccess(`${pendingHierarchyRemoval.server.name} deactivated`);
|
|
} else {
|
|
await unlinkMailServerCredential(
|
|
settings,
|
|
pendingHierarchyRemoval.profile.id,
|
|
pendingHierarchyRemoval.server.id,
|
|
pendingHierarchyRemoval.credential.id
|
|
);
|
|
setSuccess(`${pendingHierarchyRemoval.credential.name} unlinked`);
|
|
}
|
|
setPendingHierarchyRemoval(null);
|
|
await loadProfiles();
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
const treeRows = useMemo<MailProfileTreeRow[]>(
|
|
() => scopedProfiles.map((profile) => ({ kind: "profile", id: `profile:${profile.id}`, profile })),
|
|
[scopedProfiles]
|
|
);
|
|
const columns = useMemo<ConnectionTreeColumn<MailProfileTreeRow>[]>(() => [
|
|
{
|
|
id: "profile",
|
|
header: "i18n:govoplan-mail.server_credential.7fb9a24e",
|
|
width: "minmax(260px, 1.4fr)",
|
|
render: (row) => row.kind === "profile" ?
|
|
<div className="connection-tree-main"><strong>{row.profile.name}</strong><div className="connection-tree-muted-line">{row.profile.slug} · {scopeLabel(row.profile)}</div></div> :
|
|
row.kind === "server" ?
|
|
<MailServerTreeCell server={row.server} /> :
|
|
<MailCredentialTreeCell credential={row.credential} />
|
|
},
|
|
{
|
|
id: "transport",
|
|
header: "i18n:govoplan-mail.transport.c10d76c9",
|
|
width: "minmax(220px, 1fr)",
|
|
render: (row) => row.kind === "profile" ?
|
|
<span>{transportLabel(row.profile.smtp)}{row.profile.imap ? i18nMessage("i18n:govoplan-mail.value.48afe802", { value0: transportLabel(row.profile.imap) }) : ""}</span> :
|
|
row.kind === "server" ?
|
|
<TransportCell server={row.server} /> :
|
|
<span>{String(row.credential.public_data?.username || "No username")}</span>
|
|
},
|
|
{
|
|
id: "policy",
|
|
header: "i18n:govoplan-mail.policy.bb9cf141",
|
|
width: "minmax(150px, 0.8fr)",
|
|
render: (row) => row.kind === "profile" ?
|
|
scopeLabel(row.profile) :
|
|
row.kind === "server" ?
|
|
(row.server.inherit_to_lower_scopes ? "Inherited by lower scopes" : "Current scope only") :
|
|
credentialAvailabilityLabel(row.credential)
|
|
},
|
|
{
|
|
id: "status",
|
|
header: "i18n:govoplan-mail.status.bae7d5be",
|
|
width: "110px",
|
|
render: (row) => row.kind === "profile" ?
|
|
<StatusBadge status={row.profile.is_active ? "active" : "inactive"} label={row.profile.is_active ? "i18n:govoplan-mail.active.a733b809" : "i18n:govoplan-mail.inactive.09af574c"} /> :
|
|
row.kind === "server" ?
|
|
<StatusBadge status={row.server.is_active ? "success" : "inactive"} label={row.server.is_active ? "i18n:govoplan-mail.active.a733b809" : "i18n:govoplan-mail.inactive.09af574c"} /> :
|
|
<StatusBadge status={row.credential.is_active && row.credential.secret_configured ? "success" : "inactive"} label={row.credential.is_active && row.credential.secret_configured ? "i18n:govoplan-mail.saved.c0ae8f6e" : "i18n:govoplan-core.not_configured.811931bb"} />
|
|
}],
|
|
[profileEffectivePolicy]);
|
|
|
|
function mailProfileChildren(row: MailProfileTreeRow): MailProfileTreeRow[] {
|
|
if (row.kind === "profile") {
|
|
return (row.profile.servers ?? []).map((server) => ({
|
|
kind: "server",
|
|
id: `server:${server.id}`,
|
|
profile: row.profile,
|
|
protocol: server.protocol,
|
|
server
|
|
}));
|
|
}
|
|
if (row.kind === "server") {
|
|
return row.server.credentials.map((credential) => ({
|
|
kind: "credential",
|
|
id: `credential:${row.server.id}:${credential.id}`,
|
|
profile: row.profile,
|
|
protocol: row.protocol,
|
|
server: row.server,
|
|
credential
|
|
}));
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function renderMailProfileActions(row: MailProfileTreeRow) {
|
|
if (row.kind === "server") {
|
|
const label = `Edit ${row.protocol.toUpperCase()} server`;
|
|
return <TableActionGroup actions={[
|
|
{
|
|
id: "add-credential",
|
|
label: "Add or link credential",
|
|
icon: <Link2 size={16} />,
|
|
disabled: !canWriteProfiles || !canManageCredentials || busy,
|
|
onClick: () => openCreateCredential(row.profile, row.server)
|
|
},
|
|
{
|
|
id: "edit-server",
|
|
label,
|
|
icon: <Pencil size={16} />,
|
|
disabled: !canWriteProfiles || busy,
|
|
onClick: () => openEdit(row.profile, {
|
|
kind: "server",
|
|
protocol: row.protocol,
|
|
serverId: row.server.id
|
|
})
|
|
},
|
|
{
|
|
id: "deactivate-server",
|
|
label: `Deactivate ${row.server.name}`,
|
|
icon: <Trash2 size={16} />,
|
|
variant: "danger",
|
|
applicable: row.server.is_active,
|
|
disabled: !canWriteProfiles || busy,
|
|
onClick: () => setPendingHierarchyRemoval({ kind: "server", profile: row.profile, server: row.server })
|
|
}
|
|
]} />;
|
|
|
|
}
|
|
if (row.kind === "credential") {
|
|
return <TableActionGroup actions={[
|
|
{
|
|
id: "edit-credentials",
|
|
label: `Edit ${row.credential.name}`,
|
|
icon: <Pencil size={16} />,
|
|
disabled: !canWriteProfiles || !canManageCredentials || busy,
|
|
onClick: () => openEdit(row.profile, {
|
|
kind: "credentials",
|
|
protocol: row.protocol,
|
|
serverId: row.server.id,
|
|
credentialId: row.credential.id
|
|
})
|
|
},
|
|
{
|
|
id: "unlink-credentials",
|
|
label: `Unlink ${row.credential.name}`,
|
|
icon: <Unlink size={16} />,
|
|
variant: "danger",
|
|
disabled: !canWriteProfiles || !canManageCredentials || busy,
|
|
onClick: () => setPendingHierarchyRemoval({
|
|
kind: "credential",
|
|
profile: row.profile,
|
|
server: row.server,
|
|
credential: row.credential
|
|
})
|
|
}
|
|
]} />;
|
|
|
|
}
|
|
const deactivationDeletesCredentials = Boolean(
|
|
row.profile.smtp_password_configured || row.profile.imap_password_configured
|
|
);
|
|
return <TableActionGroup actions={[
|
|
{ id: "edit", label: i18nMessage("i18n:govoplan-mail.edit_value.fad75899", { value0: row.profile.name }), icon: <Pencil size={16} />, disabled: !canWriteProfiles || busy, onClick: () => openEdit(row.profile) },
|
|
{ id: "add-smtp", label: "Add SMTP server", icon: <Plus size={16} />, disabled: !canWriteProfiles || busy, onClick: () => openCreateServer(row.profile, "smtp") },
|
|
{ id: "add-imap", label: "Add IMAP server", icon: <Plus size={16} />, disabled: !canWriteProfiles || busy, onClick: () => openCreateServer(row.profile, "imap") },
|
|
{ id: "deactivate", label: i18nMessage("i18n:govoplan-mail.deactivate_value.a276a667", { value0: row.profile.name }), icon: <Trash2 size={16} />, variant: "danger", applicable: row.profile.is_active, disabled: !canWriteProfiles || busy || (deactivationDeletesCredentials && !canManageCredentials), onClick: () => setDeactivating(row.profile) }
|
|
]} />;
|
|
|
|
}
|
|
|
|
return (
|
|
<div className="mail-profile-manager">
|
|
{targetSelectionRequired &&
|
|
<Card title={i18nMessage("i18n:govoplan-mail.value_scope", { value0: targetLabel })}>
|
|
<div className="settings-target-row">
|
|
<FormField label={targetLabel}>
|
|
<select value={selectedTargetId} onChange={(event) => setSelectedTargetId(event.target.value)} disabled={loading || busy || !hasSelectableTarget}>
|
|
{!hasSelectableTarget && <option value="">{targetEmptyText}</option>}
|
|
{targetOptions.map((option) => <option key={option.id} value={option.id}>{option.secondary ? i18nMessage("i18n:govoplan-mail.value_value.c189e8bc", { value0: option.label, value1: option.secondary }) : option.label}</option>)}
|
|
</select>
|
|
</FormField>
|
|
</div>
|
|
</Card>
|
|
}
|
|
|
|
{scopeReady &&
|
|
<>
|
|
<Card
|
|
title={profileTitle}
|
|
actions={
|
|
<div className="button-row compact-actions">
|
|
<Button onClick={() => void loadProfiles()} disabled={loading}>{loading ? "i18n:govoplan-mail.loading.33ce4174" : "i18n:govoplan-mail.reload.cce71553"}</Button>
|
|
<Button variant="primary" onClick={openCreate} disabled={!canWriteProfiles || busy}><Plus size={16} />i18n:govoplan-mail.new_profile.ca36da25</Button>
|
|
</div>
|
|
}>
|
|
|
|
<LoadingFrame loading={loading} label="i18n:govoplan-mail.loading_mail_profiles.87de3560">
|
|
<ConnectionTree
|
|
rows={treeRows}
|
|
columns={columns}
|
|
getRowKey={(row) => row.id}
|
|
getChildren={mailProfileChildren}
|
|
renderActions={renderMailProfileActions}
|
|
emptyText="i18n:govoplan-mail.no_profiles_in_this_scope.302b21d8" />
|
|
</LoadingFrame>
|
|
|
|
</Card>
|
|
<MailProfilePolicyEditor
|
|
settings={settings}
|
|
scopeType={scopeType}
|
|
scopeId={activeScopeId}
|
|
profiles={profiles}
|
|
canWrite={canWritePolicy}
|
|
title={policyTitle}
|
|
onSaved={loadProfiles} />
|
|
|
|
</>
|
|
}
|
|
|
|
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
|
{success && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>}
|
|
|
|
<Dialog
|
|
open={editing !== null}
|
|
title={profileDialogTitle(editing, editingTarget)}
|
|
onClose={() => !busy && closeProfileEditor()}
|
|
className="admin-dialog admin-dialog-wide mail-profile-dialog"
|
|
closeDisabled={busy}
|
|
footer={<><Button onClick={closeProfileEditor} disabled={busy}>i18n:govoplan-mail.cancel.77dfd213</Button><Button variant="primary" onClick={() => void saveProfile()} disabled={!canWriteProfiles || busy || !scopeReady || !profileEditorCanSave(draft, editing, editingTarget, reuseCredentialId)}>{busy ? "i18n:govoplan-mail.saving.56a2285c" : "i18n:govoplan-mail.save_profile.f597c0e8"}</Button></>}>
|
|
|
|
<ProfileForm
|
|
settings={settings}
|
|
draft={draft}
|
|
setDraft={setDraft}
|
|
editing={editing}
|
|
busy={busy}
|
|
canWrite={canWriteProfiles}
|
|
canManageCredentials={canManageCredentials}
|
|
effectivePolicy={profileEffectivePolicy}
|
|
editTarget={editingTarget}
|
|
availableCredentials={availableCredentials}
|
|
reuseCredentialId={reuseCredentialId}
|
|
setReuseCredentialId={setReuseCredentialId} />
|
|
</Dialog>
|
|
|
|
<ConfirmDialog
|
|
open={Boolean(deactivating)}
|
|
title="i18n:govoplan-mail.deactivate_mail_profile.0e0fd0b8"
|
|
message={i18nMessage("i18n:govoplan-mail.deactivate_value_campaign_drafts_using_it_will_n.656f1b9e", { value0: deactivating?.name || "i18n:govoplan-mail.this_profile.5fd9cdf1" })}
|
|
confirmLabel="i18n:govoplan-mail.deactivate.d65ded94"
|
|
tone="danger"
|
|
busy={busy}
|
|
onCancel={() => setDeactivating(null)}
|
|
onConfirm={() => void deactivateProfile()} />
|
|
|
|
<ConfirmDialog
|
|
open={Boolean(pendingHierarchyRemoval)}
|
|
title={pendingHierarchyRemoval?.kind === "credential" ? "Unlink credential" : "Deactivate mail server"}
|
|
message={pendingHierarchyRemoval?.kind === "credential"
|
|
? `${pendingHierarchyRemoval.credential.name} will no longer be available to ${pendingHierarchyRemoval.server.name}. The reusable credential is retained for other connections.`
|
|
: `${pendingHierarchyRemoval?.server.name || "This server"} will no longer be selectable for new mail operations.`}
|
|
confirmLabel={pendingHierarchyRemoval?.kind === "credential" ? "Unlink" : "Deactivate"}
|
|
tone="danger"
|
|
busy={busy}
|
|
onCancel={() => setPendingHierarchyRemoval(null)}
|
|
onConfirm={() => void removeHierarchyItem()} />
|
|
|
|
</div>);
|
|
|
|
}
|
|
|
|
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 [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 [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [success, setSuccess] = useState("");
|
|
const [profileEffectivePolicy, setProfileEffectivePolicy] = useState<MailProfilePolicy | null>(null);
|
|
|
|
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() {
|
|
setError("");
|
|
setSuccess("");
|
|
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 ?? []);
|
|
} 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) return false;
|
|
setBusy(true);
|
|
setError("");
|
|
setSuccess("");
|
|
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");
|
|
await onSaved?.();
|
|
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 || !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 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">
|
|
<Button onClick={() => void loadPolicy()} disabled={loading || !scopeReady}>{loading ? "i18n:govoplan-mail.loading.33ce4174" : "i18n:govoplan-mail.reload.cce71553"}</Button>
|
|
<Button variant="primary" onClick={() => void savePolicy()} disabled={disabled}>{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">
|
|
{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>}
|
|
|
|
<section className="mail-policy-section policy-section">
|
|
<div 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>
|
|
</div>
|
|
<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">
|
|
<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(i18nMessage("i18n:govoplan-mail.whitelist_value.d4cc3755", { value0: key }) as MailProfilePolicyLimitKey, "i18n:govoplan-mail.whitelist.53c2ad30")}
|
|
{lowerLevelLimitToggle(i18nMessage("i18n:govoplan-mail.blacklist_value.556334d0", { value0: 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 ProfileForm({
|
|
settings,
|
|
draft,
|
|
setDraft,
|
|
editing,
|
|
busy,
|
|
canWrite,
|
|
canManageCredentials,
|
|
effectivePolicy,
|
|
editTarget,
|
|
availableCredentials,
|
|
reuseCredentialId,
|
|
setReuseCredentialId
|
|
}: {
|
|
settings: ApiSettings;
|
|
draft: ProfileDraft;
|
|
setDraft: (draft: ProfileDraft) => void;
|
|
editing: EditingProfile;
|
|
busy: boolean;
|
|
canWrite: boolean;
|
|
canManageCredentials: boolean;
|
|
effectivePolicy?: MailProfilePolicy | null;
|
|
editTarget: MailProfileEditTarget;
|
|
availableCredentials: MailCredentialEnvelope[];
|
|
reuseCredentialId: string;
|
|
setReuseCredentialId: (credentialId: string) => void;
|
|
}) {
|
|
const [smtpTestResult, setSmtpTestResult] = useState<MailServerConnectionTestResult | null>(null);
|
|
const [imapTestResult, setImapTestResult] = useState<MailServerConnectionTestResult | null>(null);
|
|
const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | null>(null);
|
|
|
|
const disabled = busy || !canWrite;
|
|
const credentialDisabled = disabled || !canManageCredentials;
|
|
const existingProfile = editing !== "new" ? editing : null;
|
|
const initialSection = mailProfileEditTargetInitialSection(editTarget);
|
|
const settingsPanelMode = mailProfileEditTargetPanelMode(editTarget);
|
|
const visibleSections = mailProfileEditTargetVisibleSections(editTarget);
|
|
const showProfileFields = mailProfileEditTargetShowsProfileFields(editTarget);
|
|
const showSettingsPanel = mailProfileEditTargetShowsSettingsPanel(editTarget);
|
|
const draftHasImap = hasDraftImapSettings(draft);
|
|
const selectedServerId = editTarget.kind === "server" || editTarget.kind === "credentials" ? editTarget.serverId : undefined;
|
|
const selectedCredentialId = editTarget.kind === "credentials" ? editTarget.credentialId : undefined;
|
|
const selectedCredential = selectedCredentialId && existingProfile
|
|
? existingProfile.servers?.flatMap((server) => server.credentials).find((credential) => credential.id === selectedCredentialId)
|
|
: null;
|
|
const useSavedSmtpTest = Boolean(
|
|
existingProfile
|
|
&& (editTarget.kind === "server" || editTarget.kind === "credentials")
|
|
&& editTarget.protocol === "smtp"
|
|
&& selectedServerId
|
|
&& !draft.smtpPassword
|
|
);
|
|
const useSavedImapTest = Boolean(
|
|
existingProfile
|
|
&& (editTarget.kind === "server" || editTarget.kind === "credentials")
|
|
&& editTarget.protocol === "imap"
|
|
&& selectedServerId
|
|
&& !draft.imapPassword
|
|
);
|
|
const creatingCredential = editTarget.kind === "credentials" && !editTarget.credentialId;
|
|
const creatingNewCredential = creatingCredential && !reuseCredentialId;
|
|
const policyMessages = useMemo(() => validateMailPolicy(effectivePolicy, {
|
|
smtpHost: draft.smtpHost,
|
|
imapHost: draft.imapHost
|
|
}), [draft.imapHost, draft.smtpHost, effectivePolicy]);
|
|
|
|
useEffect(() => {
|
|
setSmtpTestResult(null);
|
|
setImapTestResult(null);
|
|
setMailActionState(null);
|
|
}, [editing, editTarget]);
|
|
|
|
function patchSmtpSettings(patch: Partial<MailServerSmtpSettings>) {
|
|
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<MailServerImapSettings>) {
|
|
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<MailServerCredentialSettings>) {
|
|
setDraft({
|
|
...draft,
|
|
smtpUsername: patch.username !== undefined ? String(patch.username ?? "") : draft.smtpUsername,
|
|
smtpPassword: patch.password !== undefined ? String(patch.password ?? "") : draft.smtpPassword
|
|
});
|
|
}
|
|
|
|
function patchImapCredentials(patch: Partial<MailServerCredentialSettings>) {
|
|
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, selectedServerId, selectedCredentialId) :
|
|
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, selectedServerId, selectedCredentialId) :
|
|
await testImapSettings(settings, rawImapPayload(draft, false)));
|
|
} catch (err) {
|
|
setImapTestResult({ ok: false, protocol: "imap", message: errorMessage(err), details: {} });
|
|
} finally {
|
|
setMailActionState(null);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="mail-profile-form">
|
|
{showProfileFields &&
|
|
<div className="admin-form-grid two-columns">
|
|
<FormField label="i18n:govoplan-mail.name.709a2322"><input value={draft.name} disabled={disabled} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
|
|
<FormField label="i18n:govoplan-mail.slug.094da9b9"><input value={draft.slug} disabled={disabled} onChange={(event) => setDraft({ ...draft, slug: event.target.value })} placeholder="i18n:govoplan-mail.generated_from_name.33d69a91" /></FormField>
|
|
<FormField label="i18n:govoplan-mail.status.bae7d5be"><select value={draft.isActive ? "active" : "inactive"} disabled={disabled} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">i18n:govoplan-mail.active.a733b809</option><option value="inactive">i18n:govoplan-mail.inactive.09af574c</option></select></FormField>
|
|
<FormField label="i18n:govoplan-mail.description.55f8ebc8"><textarea rows={3} value={draft.description} disabled={disabled} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
|
|
<div className="mail-server-field-span">
|
|
<ToggleSwitch
|
|
checked={draft.inheritToLowerScopes}
|
|
disabled={disabled}
|
|
onChange={(checked) => setDraft({ ...draft, inheritToLowerScopes: checked })}
|
|
label="Visible to lower scopes" />
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
{editTarget.kind === "profile" && existingProfile &&
|
|
<ProfileTransportSummary profile={existingProfile} />
|
|
}
|
|
|
|
{editTarget.kind === "server" &&
|
|
<div className="admin-form-grid two-columns">
|
|
<FormField label="Server name">
|
|
<input value={draft.serverName} disabled={disabled} onChange={(event) => setDraft({ ...draft, serverName: event.target.value })} />
|
|
</FormField>
|
|
<FormField label="Protocol">
|
|
<input value={editTarget.protocol.toUpperCase()} disabled />
|
|
</FormField>
|
|
<ToggleSwitch
|
|
checked={draft.serverIsActive}
|
|
disabled={disabled}
|
|
onChange={(checked) => setDraft({ ...draft, serverIsActive: checked })}
|
|
label="Active" />
|
|
<ToggleSwitch
|
|
checked={draft.serverIsDefault}
|
|
disabled={disabled}
|
|
onChange={(checked) => setDraft({ ...draft, serverIsDefault: checked })}
|
|
label={`Default ${editTarget.protocol.toUpperCase()} server`} />
|
|
<div className="mail-server-field-span">
|
|
<ToggleSwitch
|
|
checked={draft.serverInheritToLowerScopes}
|
|
disabled={disabled}
|
|
onChange={(checked) => setDraft({ ...draft, serverInheritToLowerScopes: checked })}
|
|
label="Visible to lower scopes" />
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
{editTarget.kind === "credentials" &&
|
|
<div className="admin-form-grid two-columns">
|
|
{creatingCredential &&
|
|
<FormField label="Reusable credential">
|
|
<select value={reuseCredentialId} disabled={credentialDisabled} onChange={(event) => setReuseCredentialId(event.target.value)}>
|
|
<option value="">Create a new credential</option>
|
|
{availableCredentials.map((credential) =>
|
|
<option key={credential.id} value={credential.id}>
|
|
{credential.name}{credential.public_data?.username ? ` (${String(credential.public_data.username)})` : ""}
|
|
</option>
|
|
)}
|
|
</select>
|
|
</FormField>
|
|
}
|
|
{creatingCredential && <div />}
|
|
{!reuseCredentialId &&
|
|
<>
|
|
<FormField label="Credential name">
|
|
<input value={draft.credentialName} disabled={credentialDisabled} onChange={(event) => setDraft({ ...draft, credentialName: event.target.value })} />
|
|
</FormField>
|
|
<FormField label="Description">
|
|
<input value={draft.credentialDescription} disabled={credentialDisabled} onChange={(event) => setDraft({ ...draft, credentialDescription: event.target.value })} />
|
|
</FormField>
|
|
<FormField label="Available to modules" help="Comma-separated module IDs. Keep mail to use this credential here.">
|
|
<input value={draft.credentialAllowedModules} disabled={credentialDisabled} onChange={(event) => setDraft({ ...draft, credentialAllowedModules: event.target.value })} />
|
|
</FormField>
|
|
<FormField
|
|
label="Limited to servers"
|
|
help="Optional comma-separated references, for example mail:server-id, files:connection-id, calendar:source-id, or addresses:source-id. Leave blank for every permitted server.">
|
|
<input value={draft.credentialAllowedServerRefs} disabled={credentialDisabled} onChange={(event) => setDraft({ ...draft, credentialAllowedServerRefs: event.target.value })} />
|
|
</FormField>
|
|
<ToggleSwitch
|
|
checked={draft.credentialIsDefault}
|
|
disabled={credentialDisabled}
|
|
onChange={(checked) => setDraft({ ...draft, credentialIsDefault: checked })}
|
|
label={`Default credential for this ${editTarget.protocol.toUpperCase()} server`} />
|
|
<div className="mail-server-field-span">
|
|
<ToggleSwitch
|
|
checked={draft.credentialInheritToLowerScopes}
|
|
disabled={credentialDisabled}
|
|
onChange={(checked) => setDraft({ ...draft, credentialInheritToLowerScopes: checked })}
|
|
label="Visible to lower scopes" />
|
|
</div>
|
|
</>
|
|
}
|
|
{reuseCredentialId &&
|
|
<div className="mail-server-field-span muted small-note">
|
|
The selected credential remains independently managed and will be linked to this server.
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
{policyMessages.length > 0 &&
|
|
<DismissibleAlert tone="warning" resetKey={policyMessages.map((item) => `${item.key}:${item.value}`).join("|")} dismissible={false}>
|
|
<strong>i18n:govoplan-mail.effective_mail_policy_blocks_the_current_profile.1b555820</strong>
|
|
<ul>{policyMessages.map((item) => <li key={`${item.key}:${item.value}`}>{item.message}</li>)}</ul>
|
|
</DismissibleAlert>
|
|
}
|
|
|
|
{showSettingsPanel && settingsPanelMode && (!creatingCredential || creatingNewCredential) &&
|
|
<MailServerSettingsPanel
|
|
smtp={{ host: draft.smtpHost, port: draft.smtpPort, security: draft.smtpSecurity, timeout_seconds: draft.smtpTimeout }}
|
|
imap={{ host: draft.imapHost, port: draft.imapPort, security: draft.imapSecurity, sent_folder: draft.imapSentFolder, timeout_seconds: draft.imapTimeout }}
|
|
smtpCredentials={{ username: draft.smtpUsername, password: draft.smtpPassword }}
|
|
imapCredentials={{ username: draft.imapUsername, password: draft.imapPassword }}
|
|
onSmtpChange={patchSmtpSettings}
|
|
onImapChange={patchImapSettings}
|
|
onSmtpCredentialsChange={patchSmtpCredentials}
|
|
onImapCredentialsChange={patchImapCredentials}
|
|
smtpDisabled={disabled}
|
|
smtpCredentialDisabled={credentialDisabled}
|
|
smtpPasswordSaved={Boolean(selectedCredential?.secret_configured || existingProfile?.smtp_password_configured)}
|
|
imapServerDisabled={disabled}
|
|
imapCredentialDisabled={credentialDisabled}
|
|
imapPasswordSaved={Boolean(selectedCredential?.secret_configured || existingProfile?.imap_password_configured)}
|
|
imapActionDisabled={disabled || !draftHasImap}
|
|
smtpTestLabel={useSavedSmtpTest ? "i18n:govoplan-mail.test_saved_smtp.008d8054" : "i18n:govoplan-mail.test_smtp.e5697981"}
|
|
imapTestLabel={useSavedImapTest ? "i18n:govoplan-mail.test_saved_imap.923dbe4a" : "i18n:govoplan-mail.test_imap.ef1bd79c"}
|
|
busyAction={mailActionState}
|
|
onTestSmtp={() => void runSmtpTest()}
|
|
onTestImap={() => void runImapTest()}
|
|
smtpTestResult={smtpTestResult}
|
|
imapTestResult={imapTestResult}
|
|
initialSection={initialSection}
|
|
visibleSections={visibleSections}
|
|
mode={settingsPanelMode} />
|
|
}
|
|
|
|
</div>);
|
|
|
|
}
|
|
|
|
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 targetPluralLabel(scopeType: MailProfileScope, fallback: string): string {
|
|
if (scopeType === "user") return "i18n:govoplan-mail.users";
|
|
if (scopeType === "group") return "i18n:govoplan-mail.groups";
|
|
if (scopeType === "campaign") return "i18n:govoplan-mail.campaigns";
|
|
return fallback;
|
|
}
|
|
|
|
function sortMailProfiles(left: MailServerProfile, right: MailServerProfile): number {
|
|
return scopeOrder(left.scope_type) - scopeOrder(right.scope_type) || left.name.localeCompare(right.name) || left.slug.localeCompare(right.slug);
|
|
}
|
|
|
|
function MailCredentialTreeCell({ credential }: {credential: MailCredentialEnvelope;}) {
|
|
const username = credential.public_data?.username;
|
|
return (
|
|
<div className="connection-tree-main">
|
|
<strong>{credential.name}</strong>
|
|
<div className="connection-tree-muted-line">
|
|
{String(username || "i18n:govoplan-mail.no_username.1c182624")} · {credential.secret_configured ? "i18n:govoplan-mail.password_saved.f6fab237" : "i18n:govoplan-mail.no_saved_password.32ce2b16"}
|
|
</div>
|
|
</div>);
|
|
|
|
}
|
|
|
|
function ProfileTransportSummary({ profile }: {profile: MailServerProfile;}) {
|
|
return (
|
|
<div className="mail-profile-transport-summary">
|
|
<div>
|
|
<span>SMTP server</span>
|
|
<strong>{transportLabel(profile.smtp)}</strong>
|
|
<small>{mailCredentialConfigured(profile, "smtp") ? "i18n:govoplan-mail.password_saved.f6fab237" : "i18n:govoplan-mail.no_saved_password.32ce2b16"}</small>
|
|
</div>
|
|
<div>
|
|
<span>IMAP server</span>
|
|
<strong>{transportLabel(profile.imap)}</strong>
|
|
<small>{profile.imap ? mailCredentialConfigured(profile, "imap") ? "i18n:govoplan-mail.password_saved.f6fab237" : "i18n:govoplan-mail.no_saved_password.32ce2b16" : "i18n:govoplan-mail.not_configured.811931bb"}</small>
|
|
</div>
|
|
</div>);
|
|
|
|
}
|
|
|
|
function MailServerTreeCell({ server }: {server: MailServerEndpoint;}) {
|
|
return (
|
|
<div className="connection-tree-main">
|
|
<strong>{server.name}</strong>
|
|
<div className="connection-tree-muted-line">{server.protocol.toUpperCase()} · {transportLabel(server.config)}</div>
|
|
</div>);
|
|
|
|
}
|
|
|
|
function TransportCell({ server }: {server: MailServerEndpoint;}) {
|
|
return <div><strong>{transportLabel(server.config)}</strong><div className="muted small-note">{server.credentials.length} credential{server.credentials.length === 1 ? "" : "s"}{server.is_default ? " · default" : ""}</div></div>;
|
|
}
|
|
|
|
function mailCredentialConfigured(profile: MailServerProfile, protocol: "smtp" | "imap"): boolean {
|
|
return protocol === "smtp" ? profile.smtp_password_configured : profile.imap_password_configured;
|
|
}
|
|
|
|
function credentialAvailabilityLabel(credential: MailCredentialEnvelope): string {
|
|
const modules = credential.allowed_modules.length > 0 ? credential.allowed_modules.join(", ") : "all modules";
|
|
const servers = credential.allowed_server_refs.length > 0
|
|
? `${credential.allowed_server_refs.length} server restriction${credential.allowed_server_refs.length === 1 ? "" : "s"}`
|
|
: "all permitted servers";
|
|
return `${credential.inherit_to_lower_scopes ? "Inherited" : "Current scope"} · ${modules} · ${servers}`;
|
|
}
|
|
|
|
function profileDialogTitle(editing: EditingProfile, target: MailProfileEditTarget): string {
|
|
if (editing === "new") return "i18n:govoplan-mail.create_mail_profile.4d2f8f9f";
|
|
if (target.kind === "server") return `${target.serverId ? "Edit" : "Add"} ${target.protocol.toUpperCase()} server`;
|
|
if (target.kind === "credentials") return `${target.credentialId ? "Edit" : "Add or link"} ${target.protocol.toUpperCase()} credential`;
|
|
return "i18n:govoplan-mail.edit_mail_profile.95d1af9c";
|
|
}
|
|
|
|
function emptyProfileDraft(): ProfileDraft {
|
|
return {
|
|
name: "",
|
|
slug: "",
|
|
description: "",
|
|
isActive: true,
|
|
inheritToLowerScopes: true,
|
|
serverName: "",
|
|
serverIsDefault: false,
|
|
serverIsActive: true,
|
|
serverInheritToLowerScopes: true,
|
|
credentialName: "",
|
|
credentialDescription: "",
|
|
credentialAllowedModules: "mail",
|
|
credentialAllowedServerRefs: "",
|
|
credentialInheritToLowerScopes: false,
|
|
credentialIsDefault: false,
|
|
smtpHost: "",
|
|
smtpPort: "587",
|
|
smtpSecurity: "starttls",
|
|
smtpUsername: "",
|
|
smtpPassword: "",
|
|
smtpTimeout: "30",
|
|
imapHost: "",
|
|
imapPort: "993",
|
|
imapSecurity: "tls",
|
|
imapUsername: "",
|
|
imapPassword: "",
|
|
imapSentFolder: "auto",
|
|
imapTimeout: "30"
|
|
};
|
|
}
|
|
|
|
function profileDraftKey(draft: ProfileDraft): string {
|
|
return JSON.stringify(draft);
|
|
}
|
|
|
|
function profileToDraft(profile: MailServerProfile, target: MailProfileEditTarget): ProfileDraft {
|
|
const targetServer = (
|
|
target.kind === "server" || target.kind === "credentials"
|
|
) && target.serverId
|
|
? profile.servers?.find((server) => server.id === target.serverId)
|
|
: undefined;
|
|
const targetCredential = target.kind === "credentials" && target.credentialId
|
|
? targetServer?.credentials.find((credential) => credential.id === target.credentialId)
|
|
: undefined;
|
|
const smtpConfig = targetServer?.protocol === "smtp" ? targetServer.config : profile.smtp;
|
|
const imapConfig = targetServer?.protocol === "imap" ? targetServer.config : profile.imap;
|
|
const targetUsername = stringValue(targetCredential?.public_data?.username);
|
|
return {
|
|
name: profile.name,
|
|
slug: profile.slug,
|
|
description: profile.description || "",
|
|
isActive: profile.is_active,
|
|
inheritToLowerScopes: profile.inherit_to_lower_scopes !== false,
|
|
serverName: targetServer?.name || `${target.kind === "server" || target.kind === "credentials" ? target.protocol.toUpperCase() : "Mail"} server`,
|
|
serverIsDefault: targetServer?.is_default ?? false,
|
|
serverIsActive: targetServer?.is_active ?? true,
|
|
serverInheritToLowerScopes: targetServer?.inherit_to_lower_scopes ?? (profile.inherit_to_lower_scopes !== false),
|
|
credentialName: targetCredential?.name || `${target.kind === "credentials" ? target.protocol.toUpperCase() : "Mail"} credential`,
|
|
credentialDescription: targetCredential?.description || "",
|
|
credentialAllowedModules: (targetCredential?.allowed_modules?.length ? targetCredential.allowed_modules : ["mail"]).join(", "),
|
|
credentialAllowedServerRefs: (targetCredential?.allowed_server_refs ?? []).join(", "),
|
|
credentialInheritToLowerScopes: targetCredential?.inherit_to_lower_scopes ?? false,
|
|
credentialIsDefault: targetCredential?.is_default ?? false,
|
|
smtpHost: stringValue(smtpConfig?.host),
|
|
smtpPort: stringValue(smtpConfig?.port ?? 587),
|
|
smtpSecurity: readSecurity(String(smtpConfig?.security || "starttls"), "starttls"),
|
|
smtpUsername: targetServer?.protocol === "smtp" && targetCredential
|
|
? targetUsername
|
|
: stringValue(profile.credentials?.smtp?.username ?? profile.smtp.username),
|
|
smtpPassword: "",
|
|
smtpTimeout: stringValue(smtpConfig?.timeout_seconds ?? 30),
|
|
imapHost: stringValue(imapConfig?.host),
|
|
imapPort: stringValue(imapConfig?.port ?? 993),
|
|
imapSecurity: readSecurity(String(imapConfig?.security || "tls"), "tls"),
|
|
imapUsername: targetServer?.protocol === "imap" && targetCredential
|
|
? targetUsername
|
|
: stringValue(profile.credentials?.imap?.username ?? profile.imap?.username),
|
|
imapPassword: "",
|
|
imapSentFolder: stringValue("sent_folder" in (imapConfig ?? {}) ? imapConfig?.sent_folder || "auto" : "auto"),
|
|
imapTimeout: stringValue(imapConfig?.timeout_seconds ?? 30)
|
|
};
|
|
}
|
|
|
|
function createProfilePayload(draft: ProfileDraft, scopeType: MailProfileScope, scopeId: string | null): MailServerProfilePayload {
|
|
const credentials = profileCreateCredentialsPayload(draft);
|
|
return {
|
|
name: draft.name.trim(),
|
|
slug: mailTextOrNull(draft.slug),
|
|
description: mailTextOrNull(draft.description),
|
|
is_active: draft.isActive,
|
|
inherit_to_lower_scopes: draft.inheritToLowerScopes,
|
|
scope_type: scopeType,
|
|
scope_id: scopeId,
|
|
smtp: smtpServerPayload(draft),
|
|
imap: hasDraftImapSettings(draft) ? imapServerPayload(draft) : null,
|
|
...(credentials ? { credentials } : {})
|
|
};
|
|
}
|
|
|
|
function updateProfilePayload(
|
|
draft: ProfileDraft
|
|
): MailServerProfileUpdatePayload {
|
|
return {
|
|
name: draft.name.trim(),
|
|
slug: mailTextOrNull(draft.slug),
|
|
description: draft.description.trim(),
|
|
is_active: draft.isActive,
|
|
inherit_to_lower_scopes: draft.inheritToLowerScopes
|
|
};
|
|
}
|
|
|
|
function credentialPayload(draft: ProfileDraft, protocol: MailProfileProtocol, preserveBlankPassword: boolean) {
|
|
const username = protocol === "smtp" ? draft.smtpUsername : draft.imapUsername;
|
|
const password = protocol === "smtp" ? draft.smtpPassword : draft.imapPassword;
|
|
const allowedModules = [...new Set(
|
|
draft.credentialAllowedModules
|
|
.split(",")
|
|
.map((value) => value.trim())
|
|
.filter(Boolean)
|
|
)];
|
|
const allowedServerRefs = [...new Set(
|
|
draft.credentialAllowedServerRefs
|
|
.split(",")
|
|
.map((value) => value.trim())
|
|
.filter(Boolean)
|
|
)];
|
|
const payload: {
|
|
name: string;
|
|
description: string | null;
|
|
credential_kind: string;
|
|
username: string | null;
|
|
password?: string;
|
|
inherit_to_lower_scopes: boolean;
|
|
allowed_modules: string[];
|
|
allowed_server_refs: string[];
|
|
is_default: boolean;
|
|
} = {
|
|
name: draft.credentialName.trim(),
|
|
description: mailTextOrNull(draft.credentialDescription),
|
|
credential_kind: "username_password",
|
|
username: mailTextOrNull(username),
|
|
inherit_to_lower_scopes: draft.credentialInheritToLowerScopes,
|
|
allowed_modules: allowedModules.length > 0 ? allowedModules : ["mail"],
|
|
allowed_server_refs: allowedServerRefs,
|
|
is_default: draft.credentialIsDefault
|
|
};
|
|
if (!preserveBlankPassword || password) payload.password = password;
|
|
return payload;
|
|
}
|
|
|
|
function profileEditorCanSave(
|
|
draft: ProfileDraft,
|
|
editing: EditingProfile,
|
|
target: MailProfileEditTarget,
|
|
reuseCredentialId: string
|
|
): boolean {
|
|
if (!editing) return false;
|
|
if (editing === "new") return Boolean(draft.name.trim() && draft.smtpHost.trim());
|
|
if (target.kind === "profile") return Boolean(draft.name.trim());
|
|
if (target.kind === "server") {
|
|
const host = target.protocol === "smtp" ? draft.smtpHost : draft.imapHost;
|
|
return Boolean(draft.serverName.trim() && host.trim());
|
|
}
|
|
if (target.kind === "credentials") {
|
|
if (!target.serverId) return false;
|
|
if (!target.credentialId && reuseCredentialId) return true;
|
|
const password = target.protocol === "smtp" ? draft.smtpPassword : draft.imapPassword;
|
|
return Boolean(draft.credentialName.trim() && (target.credentialId || password));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function smtpServerPayload(draft: ProfileDraft): MailSmtpTestPayload {
|
|
return mailSmtpSettingsPayload<MailSecurity>(
|
|
{ host: draft.smtpHost, port: draft.smtpPort, security: draft.smtpSecurity, timeout_seconds: draft.smtpTimeout },
|
|
{ fallbackSecurity: "starttls", allowedSecurity: securityOptions }
|
|
);
|
|
}
|
|
|
|
function imapServerPayload(draft: ProfileDraft): MailImapTestPayload {
|
|
return mailImapSettingsPayload<MailSecurity>(
|
|
{ host: draft.imapHost, port: draft.imapPort, security: draft.imapSecurity, sent_folder: draft.imapSentFolder, timeout_seconds: draft.imapTimeout },
|
|
{ fallbackSecurity: "tls", allowedSecurity: securityOptions }
|
|
);
|
|
}
|
|
|
|
function profileCredentialsPayload(draft: ProfileDraft, preserveBlankPassword: boolean) {
|
|
return {
|
|
smtp: mailTransportCredentialsPayload(draft.smtpUsername, draft.smtpPassword, preserveBlankPassword),
|
|
imap: mailTransportCredentialsPayload(draft.imapUsername, draft.imapPassword, preserveBlankPassword)
|
|
};
|
|
}
|
|
|
|
function profileCreateCredentialsPayload(draft: ProfileDraft) {
|
|
return mailProfileCreateCredentialsPayload(
|
|
mailTransportCredentialsPayload(draft.smtpUsername, draft.smtpPassword, true),
|
|
mailTransportCredentialsPayload(draft.imapUsername, draft.imapPassword, true)
|
|
);
|
|
}
|
|
|
|
function rawSmtpPayload(draft: ProfileDraft, preserveBlankPassword: boolean): MailSmtpTestPayload {
|
|
return { ...smtpServerPayload(draft), ...mailTransportCredentialsPayload(draft.smtpUsername, draft.smtpPassword, preserveBlankPassword) };
|
|
}
|
|
|
|
function rawImapPayload(draft: ProfileDraft, preserveBlankPassword: boolean): MailImapTestPayload {
|
|
return { ...imapServerPayload(draft), ...mailTransportCredentialsPayload(draft.imapUsername, draft.imapPassword, preserveBlankPassword) };
|
|
}
|
|
|
|
function hasDraftImapSettings(draft: ProfileDraft): boolean {
|
|
return hasMailImapSettings([draft.imapHost]);
|
|
}
|
|
|
|
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 profileBelongsToEditableScope(profile: MailServerProfile, scopeType: MailProfileScope, scopeId: string | null): boolean {
|
|
if (profile.scope_type !== scopeType) return false;
|
|
if (scopeType === "system") return true;
|
|
if (scopeType === "tenant") return true;
|
|
return Boolean(scopeId) && profile.scope_id === scopeId;
|
|
}
|
|
|
|
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}`));
|
|
}
|
|
|
|
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 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 effectiveProfileLabel(value: string[] | null | undefined): string {
|
|
if (!value || value.length === 0) return "i18n:govoplan-mail.no_explicit_intersection.f2d62c71";
|
|
return i18nMessage("i18n:govoplan-mail.value_profile_s.d6b9d0af", { value0: value.length });
|
|
}
|
|
|
|
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"];
|
|
}
|
|
|
|
function transportLabel(transport: MailSmtpTestPayload | MailImapTestPayload | null | undefined): string {
|
|
if (!transport) return "i18n:govoplan-mail.not_configured.811931bb";
|
|
const host = transport.host || "i18n:govoplan-mail.no_host.4c710d7d";
|
|
const port = transport.port ? `:${transport.port}` : "";
|
|
return `${host}${port}`;
|
|
}
|
|
|
|
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";
|
|
}
|
|
|
|
function readSecurity(value: string, fallback: MailSecurity): MailSecurity {
|
|
return normalizeMailServerSecurity<MailSecurity>(value, { fallback, allowedSecurity: securityOptions });
|
|
}
|
|
|
|
function stringValue(value: unknown): string {
|
|
if (value === null || value === undefined) return "";
|
|
return String(value);
|
|
}
|
|
|
|
function errorMessage(err: unknown): string {
|
|
return err instanceof Error ? err.message : String(err);
|
|
}
|