import { useEffect, useState } from "react"; import { Button, Card, Dialog, DismissibleAlert, FormField, LoadingFrame, MailServerFolderLookupResultView, MetricCard, PageTitle, PasswordField, ToggleSwitch, usePlatformModuleInstalled, usePlatformUiCapability, type MailCredentialEnvelope, type MailProfilesUiCapability, type MailServerConnectionTestResult, type MailServerEndpoint, type MailServerFolderLookupResult } from "@govoplan/core-webui"; import type { ApiSettings } from "../../types"; import { createCampaignMailCredential, listMailProfileImapFolders, listMailServerProfiles, testMailProfileImap, testMailProfileSmtp, type MailServerProfile } from "../../api/mail"; import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor"; import LockedVersionNotice from "./components/LockedVersionNotice"; import VersionLine from "./components/VersionLine"; import { asRecord, isAuditLockedVersion } from "./utils/campaignView"; import { getBool, getText } from "./utils/draftEditor"; import { campaignMailProfileReferenceOnly } from "./utils/mailProfileReference"; type MailSettingsView = "settings" | "policy"; type MailSettingsPageProps = { settings: ApiSettings; campaignId: string; view?: MailSettingsView; }; type CampaignCredentialDraft = { name: string; username: string; password: string; useSmtp: boolean; useImap: boolean; }; export default function MailSettingsPage({ settings, campaignId, view = "settings" }: MailSettingsPageProps) { const mailModuleInstalled = usePlatformModuleInstalled("mail"); const mailProfilesUi = usePlatformUiCapability("mail.profiles"); const MailProfilePolicyEditor = mailProfilesUi?.MailProfilePolicyEditor ?? null; const isPolicyView = view === "policy"; const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId); const [mailProfiles, setMailProfiles] = useState([]); const [policyProfiles, setPolicyProfiles] = useState([]); const [profilesLoading, setProfilesLoading] = useState(false); const [profileError, setProfileError] = useState(""); const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | "folders" | null>(null); const [smtpTestResult, setSmtpTestResult] = useState(null); const [imapTestResult, setImapTestResult] = useState(null); const [folderResult, setFolderResult] = useState(null); const [credentialDialogOpen, setCredentialDialogOpen] = useState(false); const [credentialSaving, setCredentialSaving] = useState(false); const [credentialDraft, setCredentialDraft] = useState({ name: "", username: "", password: "", useSmtp: true, useImap: false }); const version = data.currentVersion; const locked = isAuditLockedVersion(version, data.campaign?.current_version_id); const migrationRequired = version?.mail_profile_migration_required === true; const { draft, displayDraft, dirty, saveState, localError, setLocalError, patch, discardDraft, 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: campaignMailProfileReferenceOnly, extraPayload: () => ({ migrate_legacy_mail_settings: migrationRequired && Boolean(selectedProfileId) }) }); const server = asRecord(displayDraft.server); const selectedProfileId = getText(server, "mail_profile_id"); const selectedProfile = mailProfiles.find((profile) => profile.id === selectedProfileId) ?? null; const smtpServers = (selectedProfile?.servers ?? []).filter((item) => item.protocol === "smtp" && item.is_active); const imapServers = (selectedProfile?.servers ?? []).filter((item) => item.protocol === "imap" && item.is_active); const selectedSmtpServer = smtpServers.find((item) => item.id === getText(server, "smtp_server_id")) ?? smtpServers.find((item) => item.is_default) ?? smtpServers[0] ?? null; const selectedImapServer = imapServers.find((item) => item.id === getText(server, "imap_server_id")) ?? imapServers.find((item) => item.is_default) ?? imapServers[0] ?? null; const selectedSmtpCredential = selectedSmtpServer?.credentials.find((item) => item.id === getText(server, "smtp_credential_id")) ?? selectedSmtpServer?.credentials.find((item) => item.is_default) ?? selectedSmtpServer?.credentials[0] ?? null; const selectedImapCredential = selectedImapServer?.credentials.find((item) => item.id === getText(server, "imap_credential_id")) ?? selectedImapServer?.credentials.find((item) => item.is_default) ?? selectedImapServer?.credentials[0] ?? null; const delivery = asRecord(displayDraft.delivery); const imapAppend = asRecord(delivery.imap_append_sent); const imapAppendEnabled = getBool(imapAppend, "enabled"); const selectedProfileHasImap = Boolean(selectedImapServer); const selectedProfileUnavailable = Boolean(selectedProfileId && !profilesLoading && !selectedProfile); const canSave = dirty && !locked && Boolean(draft) && (!migrationRequired || Boolean(selectedProfileId)); useEffect(() => { if (!mailModuleInstalled) { setMailProfiles([]); setPolicyProfiles([]); 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] = await Promise.all([ listMailServerProfiles(settings, false, campaignId), listMailServerProfiles(settings, true) ]); setMailProfiles(allowedProfiles); setPolicyProfiles(visibleProfiles); } catch (err) { setMailProfiles([]); setPolicyProfiles([]); setProfileError(err instanceof Error ? err.message : String(err)); } finally { setProfilesLoading(false); } } function selectMailProfile(profileId: string) { if (!mailModuleInstalled || locked) return; const profile = mailProfiles.find((item) => item.id === profileId); const smtpServer = profile?.servers?.find((item) => item.protocol === "smtp" && item.is_active && item.is_default) ?? profile?.servers?.find((item) => item.protocol === "smtp" && item.is_active); const imapServer = profile?.servers?.find((item) => item.protocol === "imap" && item.is_active && item.is_default) ?? profile?.servers?.find((item) => item.protocol === "imap" && item.is_active); const smtpCredential = smtpServer?.credentials.find((item) => item.is_active && item.is_default) ?? smtpServer?.credentials.find((item) => item.is_active); const imapCredential = imapServer?.credentials.find((item) => item.is_active && item.is_default) ?? imapServer?.credentials.find((item) => item.is_active); patch(["server"], profile ? mailReference( profile.id, smtpServer?.id, smtpCredential?.id, imapServer?.id, imapCredential?.id ) : {}); setSmtpTestResult(null); setImapTestResult(null); setFolderResult(null); setProfileError(""); } function selectServer(protocol: "smtp" | "imap", serverId: string) { if (!selectedProfile || locked) return; const availableServers = protocol === "smtp" ? smtpServers : imapServers; const selected = availableServers.find((item) => item.id === serverId) ?? null; const credential = selected?.credentials.find((item) => item.is_active && item.is_default) ?? selected?.credentials.find((item) => item.is_active) ?? null; patch(["server"], mailReference( selectedProfile.id, protocol === "smtp" ? selected?.id : selectedSmtpServer?.id, protocol === "smtp" ? credential?.id : selectedSmtpCredential?.id, protocol === "imap" ? selected?.id : selectedImapServer?.id, protocol === "imap" ? credential?.id : selectedImapCredential?.id )); if (protocol === "smtp") setSmtpTestResult(null); else { setImapTestResult(null); setFolderResult(null); } } function selectCredential(protocol: "smtp" | "imap", credentialId: string) { if (!selectedProfile || locked) return; patch(["server"], mailReference( selectedProfile.id, selectedSmtpServer?.id, protocol === "smtp" ? credentialId : selectedSmtpCredential?.id, selectedImapServer?.id, protocol === "imap" ? credentialId : selectedImapCredential?.id )); if (protocol === "smtp") setSmtpTestResult(null); else { setImapTestResult(null); setFolderResult(null); } } function openCredentialDialog() { setCredentialDraft({ name: `${data.campaign?.name || "Campaign"} credential`, username: "", password: "", useSmtp: Boolean(selectedSmtpServer), useImap: Boolean(selectedImapServer) }); setCredentialDialogOpen(true); setLocalError(""); } async function saveCampaignCredential() { if (!selectedProfile || credentialSaving) return; const serverIds = [ credentialDraft.useSmtp ? selectedSmtpServer?.id : null, credentialDraft.useImap ? selectedImapServer?.id : null ].filter((value): value is string => Boolean(value)); if ( !credentialDraft.name.trim() || !credentialDraft.username.trim() || !credentialDraft.password || serverIds.length === 0 ) return; setCredentialSaving(true); setLocalError(""); try { const credential = await createCampaignMailCredential( settings, selectedProfile.id, campaignId, { name: credentialDraft.name.trim(), username: credentialDraft.username.trim(), password: credentialDraft.password, server_ids: serverIds } ); patch(["server"], mailReference( selectedProfile.id, selectedSmtpServer?.id, credentialDraft.useSmtp ? credential.id : selectedSmtpCredential?.id, selectedImapServer?.id, credentialDraft.useImap ? credential.id : selectedImapCredential?.id )); setCredentialDialogOpen(false); await refreshMailProfiles(); } catch (err) { setLocalError(err instanceof Error ? err.message : String(err)); } finally { setCredentialSaving(false); } } async function runProfileTest(protocol: "smtp" | "imap") { if (!selectedProfileId || locked) return; setMailActionState(protocol); setLocalError(""); try { if (protocol === "smtp") { setSmtpTestResult(await testMailProfileSmtp( settings, selectedProfileId, selectedSmtpServer?.id, selectedSmtpCredential?.id, campaignId )); } else { setImapTestResult(await testMailProfileImap( settings, selectedProfileId, selectedImapServer?.id, selectedImapCredential?.id, campaignId )); } } catch (err) { const result = { ok: false, protocol, message: err instanceof Error ? err.message : String(err), details: {} }; if (protocol === "smtp") setSmtpTestResult(result); else setImapTestResult(result); } finally { setMailActionState(null); } } async function runFolderLookup() { if (!selectedProfileId || locked || !selectedProfileHasImap) return; setMailActionState("folders"); setLocalError(""); try { setFolderResult(await listMailProfileImapFolders( settings, selectedProfileId, selectedImapServer?.id, selectedImapCredential?.id, campaignId )); } 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 && !locked) 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.install_and_enable_the_mail_module_to_select_a_d.01c75fc4} {migrationRequired && i18n:govoplan-campaign.this_version_contains_legacy_campaign_local_mail.44c7a6fd } {isPolicyView && mailModuleInstalled && MailProfilePolicyEditor && } {isPolicyView && mailModuleInstalled && !MailProfilePolicyEditor && i18n:govoplan-campaign.the_mail_module_did_not_expose_profile_managemen.2cdb57e1} {!isPolicyView && mailModuleInstalled &&
}>

The campaign stores stable references to the envelope, selected servers, and credentials.

{selectedProfileUnavailable && i18n:govoplan-campaign.the_referenced_mail_profile_is_inactive_unavaila.abeebe26} {selectedProfile &&
}
{smtpTestResult && {smtpTestResult.message}} {imapTestResult && {imapTestResult.message}} {profileError && {profileError}} } setCredentialDialogOpen(false)} closeDisabled={credentialSaving} className="admin-dialog admin-dialog-wide adaptive-config-dialog" footerClassName="button-row compact-actions" footer={ <> } >

Campaign login

This credential belongs to this campaign and can use only the selected mail servers.

setCredentialDraft({ ...credentialDraft, name: event.target.value })} autoFocus /> setCredentialDraft({ ...credentialDraft, username: event.target.value })} /> setCredentialDraft({ ...credentialDraft, password })} disabled={credentialSaving} autoComplete="new-password" />
{selectedSmtpServer && setCredentialDraft({ ...credentialDraft, useSmtp })} label={`Use for SMTP: ${selectedSmtpServer.name}`} />} {selectedImapServer && setCredentialDraft({ ...credentialDraft, useImap })} label={`Use for IMAP: ${selectedImapServer.name}`} />}
{!isPolicyView &&
patch(["delivery", "imap_append_sent", "enabled"], checked)} />
patch(["delivery", "imap_append_sent", "folder"], event.target.value)} />
} ); } function profileScopeLabel(profile: MailServerProfile): string { if (profile.scope_type === "system") return "i18n:govoplan-campaign.system.bc0792d8"; if (profile.scope_type === "tenant") return "i18n:govoplan-campaign.tenant.3ca93c78"; if (profile.scope_type === "user") return "i18n:govoplan-campaign.user.9f8a2389"; if (profile.scope_type === "group") return "i18n:govoplan-campaign.group.171a0606"; return "i18n:govoplan-campaign.campaign_scoped_mail_profile.9cbf3505"; } function mailReference( profileId: string, smtpServerId?: string | null, smtpCredentialId?: string | null, imapServerId?: string | null, imapCredentialId?: string | null ): Record { const value: Record = { mail_profile_id: profileId }; if (smtpServerId) value.smtp_server_id = smtpServerId; if (smtpServerId && smtpCredentialId) value.smtp_credential_id = smtpCredentialId; if (imapServerId) value.imap_server_id = imapServerId; if (imapServerId && imapCredentialId) value.imap_credential_id = imapCredentialId; return value; } function serverEndpointLabel(config: MailServerEndpoint["config"]): string { const host = typeof config.host === "string" && config.host ? config.host : "No host"; return config.port ? `${host}:${config.port}` : host; } function credentialLabel(credential: MailCredentialEnvelope): string { const username = credential.public_data?.username; return username ? `${credential.name} (${String(username)})` : credential.name; }