import { useEffect, useMemo, useState } from "react"; import { MailServerSettingsPanel, addressesFromValue, hasMailImapSettings, mailImapSettingsPayload, mailNumberOrDefault, mailNumberOrNull, mailServerSecurityOptions, mailSmtpSettingsPayload, mailTransportCredentialsPayloadFromRecords, usePlatformModuleInstalled, usePlatformUiCapability, type MailProfilesUiCapability, type MailServerConnectionTestResult, type MailServerCredentialSettings, type MailServerFolderLookupResult, type MailServerImapSettings, type MailServerSmtpSettings } from "@govoplan/core-webui"; import type { ApiSettings } from "../../types"; import { Button } from "@govoplan/core-webui"; import { Card } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui"; import { PageTitle } from "@govoplan/core-webui"; import { LoadingFrame } from "@govoplan/core-webui"; import LockedVersionNotice from "./components/LockedVersionNotice"; import VersionLine from "./components/VersionLine"; import { DismissibleAlert, i18nMessage } from "@govoplan/core-webui"; import { createMailServerProfile, getMailProfilePolicy, listImapFolders, listMailProfileImapFolders, listMailServerProfiles, testImapSettings, testMailProfileImap, testMailProfileSmtp, testSmtpSettings, type MailProfilePolicy, type MailSecurity, type MailServerProfile } from "../../api/mail"; import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor"; import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView"; import { cloneJson, getBool, getNumber, getText } from "./utils/draftEditor"; import { campaignMailSettingsPolicyState } from "./policyUi"; const securityOptions = mailServerSecurityOptions as readonly MailSecurity[]; type MailSettingsView = "settings" | "policy"; type MailSettingsPageProps = { settings: ApiSettings; campaignId: string; view?: MailSettingsView; }; export default function MailSettingsPage({ settings, campaignId, view = "settings" }: MailSettingsPageProps) { const mailModuleInstalled = usePlatformModuleInstalled("mail"); const isPolicyView = view === "policy"; const mailProfilesUi = usePlatformUiCapability("mail.profiles"); const MailProfilePolicyEditor = mailProfilesUi?.MailProfilePolicyEditor ?? null; const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId); const [smtpTestResult, setSmtpTestResult] = useState(null); const [imapTestResult, setImapTestResult] = useState(null); const [folderResult, setFolderResult] = useState(null); const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | "folders" | null>(null); const [mailProfiles, setMailProfiles] = useState([]); const [policyProfiles, setPolicyProfiles] = useState([]); const [effectiveMailPolicy, setEffectiveMailPolicy] = useState(null); const [profilesLoading, setProfilesLoading] = useState(false); const [profileName, setProfileName] = useState(""); const [profileMessage, setProfileMessage] = useState(""); const [profileError, setProfileError] = useState(""); const version = data.currentVersion; const locked = isAuditLockedVersion(version, data.campaign?.current_version_id); const { draft, displayDraft, dirty, saveState, localError, setLocalError, patch, saveDraft } = useCampaignDraftEditor({ settings, campaignId, version, locked, reload, setError, currentStep: isPolicyView ? "mail-policy" : "mail-settings", unsavedTitle: isPolicyView ? "i18n:govoplan-campaign.unsaved_mail_policy_changes.c9327491" : "i18n:govoplan-campaign.unsaved_mail_settings.38e1536b", unsavedMessage: isPolicyView ? "i18n:govoplan-campaign.mail_policy_changes_have_unsaved_draft_changes_s.5aee7d4e" : "i18n:govoplan-campaign.mail_settings_have_unsaved_changes_save_them_bef.52644559", transformDraftBeforeSave: normalizeMailSettingsBeforeSave }); const server = asRecord(displayDraft.server); const smtp = asRecord(server.smtp); const imap = asRecord(server.imap); const credentials = asRecord(server.credentials); const smtpCredentials = asRecord(credentials.smtp); const imapCredentials = asRecord(credentials.imap); const delivery = asRecord(displayDraft.delivery); const imapAppend = asRecord(delivery.imap_append_sent); const selectedProfileId = mailModuleInstalled ? getText(server, "mail_profile_id") : ""; const selectedProfile = mailModuleInstalled ? mailProfiles.find((profile) => profile.id === selectedProfileId) ?? null : null; const usingMailProfile = mailModuleInstalled && Boolean(selectedProfileId); const selectedProfileHasImap = Boolean(selectedProfile?.imap); const imapUnavailable = usingMailProfile && !selectedProfileHasImap; const smtpCredentialsInherited = usingMailProfile ? effectiveMailPolicy?.smtp_credentials?.inherit !== false : false; const imapCredentialsInherited = usingMailProfile ? effectiveMailPolicy?.imap_credentials?.inherit !== false : false; const effectiveMailPolicyForState: MailProfilePolicy | null = mailModuleInstalled ? effectiveMailPolicy : { allow_campaign_profiles: true }; const mailPolicyState = campaignMailSettingsPolicyState({ effectivePolicy: effectiveMailPolicyForState, selectedProfileId, locked }); const campaignProfilesAllowed = mailPolicyState.campaignProfilesAllowed; const inlineMailSettingsBlocked = mailPolicyState.inlineMailSettingsBlocked; const smtpDisabled = locked || usingMailProfile || inlineMailSettingsBlocked; const smtpCredentialDisabled = locked || inlineMailSettingsBlocked || usingMailProfile && smtpCredentialsInherited; const imapServerDisabled = locked || inlineMailSettingsBlocked || usingMailProfile; const imapCredentialDisabled = locked || inlineMailSettingsBlocked || imapUnavailable || usingMailProfile && imapCredentialsInherited; const imapDisabled = locked || inlineMailSettingsBlocked || imapUnavailable; const imapAppendEnabled = getBool(imapAppend, "enabled"); const appendTargetFolderDisabled = imapDisabled || !imapAppendEnabled; const inlinePolicyMessages = useMemo(() => { const validateMailPolicy = mailProfilesUi?.validateMailPolicy; if (!mailModuleInstalled || usingMailProfile || !validateMailPolicy || !effectiveMailPolicy) return []; const recipients = asRecord(displayDraft.recipients); const fromEmail = firstAddressEmail(recipients.from); return validateMailPolicy(effectiveMailPolicy, { smtpHost: getText(smtp, "host"), imapHost: getText(imap, "host"), envelopeSender: fromEmail || getText(smtpCredentials, "username", getText(smtp, "username")), fromHeader: fromEmail, recipientDomains: collectRecipientDomains(displayDraft) }); }, [displayDraft, effectiveMailPolicy, imap, mailModuleInstalled, mailProfilesUi, smtp, smtpCredentials, usingMailProfile]); const displayedSmtp = { host: usingMailProfile && selectedProfile ? stringOrEmpty(selectedProfile.smtp.host) : getText(smtp, "host"), port: usingMailProfile && selectedProfile ? selectedProfile.smtp.port ?? 587 : getNumber(smtp, "port", 587), username: usingMailProfile && smtpCredentialsInherited ? profileUsername(selectedProfile, "smtp") : getText(smtpCredentials, "username", getText(smtp, "username")), password: usingMailProfile && smtpCredentialsInherited ? "" : getText(smtpCredentials, "password", getText(smtp, "password")), security: usingMailProfile && selectedProfile ? selectedProfile.smtp.security ?? "starttls" : getText(smtp, "security", "starttls"), timeout_seconds: usingMailProfile && selectedProfile ? selectedProfile.smtp.timeout_seconds ?? 30 : getNumber(smtp, "timeout_seconds", 30) }; const displayedImap = { host: usingMailProfile && selectedProfile?.imap ? stringOrEmpty(selectedProfile.imap.host) : getText(imap, "host"), port: usingMailProfile && selectedProfile?.imap ? selectedProfile.imap.port ?? 993 : getNumber(imap, "port", 993), username: usingMailProfile && imapCredentialsInherited ? profileUsername(selectedProfile, "imap") : getText(imapCredentials, "username", getText(imap, "username")), password: usingMailProfile && imapCredentialsInherited ? "" : getText(imapCredentials, "password", getText(imap, "password")), security: usingMailProfile && selectedProfile?.imap ? selectedProfile.imap.security ?? "tls" : getText(imap, "security", "tls"), sent_folder: usingMailProfile && selectedProfile?.imap ? selectedProfile.imap.sent_folder ?? "auto" : getText(imap, "sent_folder", "auto"), timeout_seconds: usingMailProfile && selectedProfile?.imap ? selectedProfile.imap.timeout_seconds ?? 30 : getNumber(imap, "timeout_seconds", 30) }; const selectedProfileNeedsLocalCredentials = usingMailProfile && (!smtpCredentialsInherited || selectedProfileHasImap && !imapCredentialsInherited); useEffect(() => { if (!mailModuleInstalled) { setMailProfiles([]); setPolicyProfiles([]); setEffectiveMailPolicy({ allow_campaign_profiles: true }); setProfileError(""); setProfilesLoading(false); return; } void refreshMailProfiles(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken, campaignId, mailModuleInstalled]); async function refreshMailProfiles() { if (!mailModuleInstalled) return; setProfilesLoading(true); setProfileError(""); try { const [allowedProfiles, visibleProfiles, policyResponse] = await Promise.all([ listMailServerProfiles(settings, false, campaignId), listMailServerProfiles(settings, true), getMailProfilePolicy(settings, "campaign", campaignId, campaignId)] ); setMailProfiles(allowedProfiles); setPolicyProfiles(visibleProfiles); setEffectiveMailPolicy(policyResponse.effective_policy ?? null); } catch (err) { setMailProfiles([]); setPolicyProfiles([]); setEffectiveMailPolicy(null); setProfileError(err instanceof Error ? err.message : String(err)); } finally { setProfilesLoading(false); } } function normalizeMailSettingsBeforeSave(value: Record): Record { if (mailModuleInstalled === false) return value; const next = cloneJson(value); const nextServer = { ...asRecord(next.server) }; const profileId = getText(nextServer, "mail_profile_id"); if (profileId.length === 0) return next; const nextCredentials = { ...asRecord(nextServer.credentials) }; normalizeProfileCredentialProtocol(nextServer, nextCredentials, "smtp", effectiveMailPolicy?.smtp_credentials?.inherit === false ? false : true); normalizeProfileCredentialProtocol(nextServer, nextCredentials, "imap", effectiveMailPolicy?.imap_credentials?.inherit === false ? false : true); nextServer.credentials = nextCredentials; next.server = nextServer; return next; } function normalizeProfileCredentialProtocol( serverValue: Record, credentialsValue: Record, protocol: "smtp" | "imap", inherit: boolean) { serverValue["inherit_" + protocol + "_credentials"] = inherit; if (inherit === false) return; const transport = { ...asRecord(serverValue[protocol]) }; delete transport.username; delete transport.password; serverValue[protocol] = transport; credentialsValue[protocol] = {}; } function selectMailProfile(profileId: string) { if (!mailModuleInstalled || locked) return; if (!profileId && !campaignProfilesAllowed) { setProfileError(mailPolicyState.inlineBlockedMessage); return; } patch(["server", "mail_profile_id"], profileId); setProfileMessage(""); setProfileError(""); if (profileId) { patch(["server", "smtp"], {}); patch(["server", "imap"], {}); patch(["server", "credentials"], {}); setSmtpTestResult(null); setImapTestResult(null); setFolderResult(null); } } async function saveCurrentSettingsAsProfile() { if (!mailModuleInstalled || locked || usingMailProfile || !campaignProfilesAllowed) return; setProfilesLoading(true); setProfileMessage(""); setProfileError(""); try { const created = await createMailServerProfile(settings, { name: profileName.trim() || `${data.campaign?.name || "Campaign"} mail profile`, scope_type: "campaign", scope_id: campaignId, smtp: smtpServerPayload(), imap: hasInlineImapSettings() ? imapServerPayload() : null, credentials: mailProfileCredentialsPayload(false), is_active: true }); setMailProfiles((current) => [...current.filter((profile) => profile.id !== created.id), created].sort((a, b) => a.name.localeCompare(b.name))); patch(["server", "mail_profile_id"], created.id); patch(["server", "smtp"], {}); patch(["server", "imap"], {}); patch(["server", "credentials"], {}); setProfileName(""); setProfileMessage(`Saved profile ${created.name}.`); } catch (err) { setProfileError(err instanceof Error ? err.message : String(err)); } finally { setProfilesLoading(false); } } function patchSmtpSettings(patchValue: Partial) { if (patchValue.host !== undefined) patch(["server", "smtp", "host"], String(patchValue.host ?? "")); if (patchValue.port !== undefined) patch(["server", "smtp", "port"], mailNumberOrNull(patchValue.port)); if (patchValue.security !== undefined) patch(["server", "smtp", "security"], String(patchValue.security || "starttls")); if (patchValue.timeout_seconds !== undefined) patch(["server", "smtp", "timeout_seconds"], mailNumberOrDefault(patchValue.timeout_seconds, 30)); } function patchImapSettings(patchValue: Partial) { if (patchValue.host !== undefined) patch(["server", "imap", "host"], String(patchValue.host ?? "")); if (patchValue.port !== undefined) patch(["server", "imap", "port"], mailNumberOrNull(patchValue.port)); if (patchValue.security !== undefined) patch(["server", "imap", "security"], String(patchValue.security || "tls")); if (patchValue.sent_folder !== undefined) patch(["server", "imap", "sent_folder"], String(patchValue.sent_folder ?? "")); if (patchValue.timeout_seconds !== undefined) patch(["server", "imap", "timeout_seconds"], mailNumberOrDefault(patchValue.timeout_seconds, 30)); } function patchSmtpCredentials(patchValue: Partial) { if (patchValue.username !== undefined) patch(["server", "credentials", "smtp", "username"], String(patchValue.username ?? "")); if (patchValue.password !== undefined) patch(["server", "credentials", "smtp", "password"], String(patchValue.password ?? "")); } function patchImapCredentials(patchValue: Partial) { if (patchValue.username !== undefined) patch(["server", "credentials", "imap", "username"], String(patchValue.username ?? "")); if (patchValue.password !== undefined) patch(["server", "credentials", "imap", "password"], String(patchValue.password ?? "")); } function profileScopeLabel(profile: MailServerProfile): string { if (profile.scope_type === "system") return "system"; if (profile.scope_type === "tenant") return "tenant"; if (profile.scope_type === "user") return "user"; if (profile.scope_type === "group") return "group"; return "campaign"; } function smtpServerPayload() { return mailSmtpSettingsPayload( { host: getText(smtp, "host"), port: getNumber(smtp, "port", 587), security: getText(smtp, "security", "starttls"), timeout_seconds: getNumber(smtp, "timeout_seconds", 30) }, { fallbackSecurity: "starttls", allowedSecurity: securityOptions } ); } function imapServerPayload() { return mailImapSettingsPayload( { host: getText(imap, "host"), port: getNumber(imap, "port", 993), security: getText(imap, "security", "tls"), sent_folder: getText(imap, "sent_folder", "auto"), timeout_seconds: getNumber(imap, "timeout_seconds", 30) }, { fallbackSecurity: "tls", allowedSecurity: securityOptions } ); } function mailProfileCredentialsPayload(preserveBlankPassword: boolean) { return { smtp: mailTransportCredentialsPayloadFromRecords(smtpCredentials, smtp, preserveBlankPassword), imap: mailTransportCredentialsPayloadFromRecords(imapCredentials, imap, preserveBlankPassword) }; } function rawSmtpPayload() { const serverPayload = selectedProfile && !smtpCredentialsInherited ? selectedProfile.smtp : smtpServerPayload(); return { ...serverPayload, ...mailTransportCredentialsPayloadFromRecords(smtpCredentials, smtp, false) }; } function rawImapPayload() { const serverPayload = selectedProfile?.imap && !imapCredentialsInherited ? selectedProfile.imap : imapServerPayload(); return { ...serverPayload, ...mailTransportCredentialsPayloadFromRecords(imapCredentials, imap, false) }; } function hasInlineImapSettings(): boolean { return hasMailImapSettings([getText(imap, "host"), getText(imapCredentials, "username", getText(imap, "username")), getText(imapCredentials, "password", getText(imap, "password"))]); } function profileUsername(profile: MailServerProfile | null, protocol: "smtp" | "imap"): string { if (!profile) return ""; if (protocol === "smtp") return stringOrEmpty(profile.credentials?.smtp?.username ?? profile.smtp.username); return stringOrEmpty(profile.credentials?.imap?.username ?? profile.imap?.username); } async function runSmtpTest() { if (!mailModuleInstalled) { setSmtpTestResult({ ok: false, protocol: "smtp", message: "i18n:govoplan-campaign.install_and_enable_the_mail_module_to_test_smtp_.a7ce04e1", details: {} }); return; } if (locked || inlineMailSettingsBlocked) return; setMailActionState("smtp"); setLocalError(""); try { setSmtpTestResult(selectedProfileId && smtpCredentialsInherited ? await testMailProfileSmtp(settings, selectedProfileId) : await testSmtpSettings(settings, rawSmtpPayload())); } catch (err) { setSmtpTestResult({ ok: false, protocol: "smtp", message: err instanceof Error ? err.message : String(err), details: {} }); } finally { setMailActionState(null); } } async function runImapTest() { if (!mailModuleInstalled) { setImapTestResult({ ok: false, protocol: "imap", message: "i18n:govoplan-campaign.install_and_enable_the_mail_module_to_test_imap_.d6537dfd", details: {} }); return; } if (imapDisabled) return; setMailActionState("imap"); setLocalError(""); try { setImapTestResult(selectedProfileId && imapCredentialsInherited ? await testMailProfileImap(settings, selectedProfileId) : await testImapSettings(settings, rawImapPayload())); } catch (err) { setImapTestResult({ ok: false, protocol: "imap", message: err instanceof Error ? err.message : String(err), details: {} }); } finally { setMailActionState(null); } } async function runFolderLookup() { if (!mailModuleInstalled) { setFolderResult({ ok: false, protocol: "imap", message: "i18n:govoplan-campaign.install_and_enable_the_mail_module_to_inspect_im.52535774", folders: [], details: {} }); return; } if (appendTargetFolderDisabled) return; setMailActionState("folders"); setLocalError(""); try { setFolderResult(selectedProfileId && imapCredentialsInherited ? await listMailProfileImapFolders(settings, selectedProfileId) : await listImapFolders(settings, rawImapPayload())); } catch (err) { setFolderResult({ ok: false, protocol: "imap", message: err instanceof Error ? err.message : String(err), folders: [], details: {} }); } finally { setMailActionState(null); } } function useDetectedSentFolder() { const folder = folderResult?.detected_sent_folder; if (!folder || appendTargetFolderDisabled) return; patch(["delivery", "imap_append_sent", "folder"], folder); } return (
{isPolicyView ? "i18n:govoplan-campaign.mail_policy.3eb5d32a" : "i18n:govoplan-campaign.mail_settings.19e07f55"}
{!isPolicyView && }
{error && {error}} {localError && {localError}} {locked && } <> {!mailModuleInstalled && i18n:govoplan-campaign.the_mail_module_is_not_installed_inline_smtp_and.8e0f802e } {isPolicyView && mailModuleInstalled && MailProfilePolicyEditor && } {isPolicyView && mailModuleInstalled && !MailProfilePolicyEditor && i18n:govoplan-campaign.the_mail_module_did_not_expose_profile_managemen.2cdb57e1 } {!isPolicyView && mailModuleInstalled &&
}>
setProfileName(event.target.value)} placeholder={data.campaign?.name ? i18nMessage("i18n:govoplan-campaign.value_campaign_local_profile.70cd9d43", { value0: data.campaign.name }) : "i18n:govoplan-campaign.campaign_local_profile.c24cf2d1"} />
{!campaignProfilesAllowed &&

i18n:govoplan-campaign.campaign_local_mail_settings_are_blocked_by_the_.0b2510aa

} {selectedProfile &&

i18n:govoplan-campaign.using.c25de2e8 {selectedProfile.name} ({profileScopeLabel(selectedProfile)}i18n:govoplan-campaign.smtp_credentials.10f75c8a {smtpCredentialsInherited ? "i18n:govoplan-campaign.inherited_from_profile.1947c2f3" : "i18n:govoplan-campaign.local_credentials_required.fdc9af1c"}i18n:govoplan-campaign.imap_credentials.7442b238 {selectedProfile.imap ? imapCredentialsInherited ? "i18n:govoplan-campaign.inherited_from_profile.1947c2f3" : "i18n:govoplan-campaign.local_credentials_required.fdc9af1c" : "i18n:govoplan-campaign.not_configured.67f2141f"}.

} {selectedProfileNeedsLocalCredentials &&

i18n:govoplan-campaign.the_selected_profile_supplies_the_server_setting.bdc830db

} {profileMessage && {profileMessage}} {profileError && {profileError}} } {!isPolicyView && {inlinePolicyMessages.length > 0 && `${item.key}:${item.value}`).join("|")} dismissible={false}> i18n:govoplan-campaign.effective_mail_policy_blocks_the_current_inline_.99c34c6b
    {inlinePolicyMessages.map((item) =>
  • {item.message}
  • )}
} patch(["delivery", "imap_append_sent", "enabled"], checked), onFolderChange: (folder) => patch(["delivery", "imap_append_sent", "folder"], folder) }} smtpTestLabel={usingMailProfile ? "i18n:govoplan-campaign.test_profile_smtp.884a0e66" : "i18n:govoplan-campaign.test_smtp_login.a1359755"} imapTestLabel={usingMailProfile ? "i18n:govoplan-campaign.test_profile_imap.e1cec0e0" : "i18n:govoplan-campaign.test_imap_login.c32e316e"} busyAction={mailActionState} onTestSmtp={runSmtpTest} onTestImap={runImapTest} onLookupFolders={runFolderLookup} smtpTestResult={smtpTestResult} imapTestResult={imapTestResult} folderLookupResult={folderResult} onUseDetectedFolder={useDetectedSentFolder} useDetectedFolderDisabled={appendTargetFolderDisabled} floatingResults />
} ); } function stringOrEmpty(value: unknown): string { return value === null || value === undefined ? "" : String(value); } function firstAddressEmail(value: unknown): string { return addressesFromValue(value)[0]?.email ?? ""; } function collectRecipientDomains(draft: Record): string[] { const domains = new Set(); const recipients = asRecord(draft.recipients); for (const key of ["to", "cc", "bcc"]) addAddressDomains(domains, recipients[key]); const entries = asArray(asRecord(draft.entries).inline).map(asRecord); for (const entry of entries) { for (const key of ["to", "cc", "bcc"]) addAddressDomains(domains, entry[key]); addAddressDomains(domains, entry.recipient); addAddressDomains(domains, entry); } return [...domains].sort(); } function addAddressDomains(domains: Set, value: unknown) { for (const address of addressesFromValue(value)) { const domain = address.email.split("@").pop()?.trim().toLowerCase(); if (domain) domains.add(domain); } }