diff --git a/webui/package.json b/webui/package.json index 4615ea4..9ef9f89 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/campaign-webui", - "version": "0.1.8", + "version": "0.1.9", "private": true, "type": "module", "main": "src/index.ts", diff --git a/webui/src/api/campaigns.ts b/webui/src/api/campaigns.ts index a5b982e..72a840e 100644 --- a/webui/src/api/campaigns.ts +++ b/webui/src/api/campaigns.ts @@ -81,6 +81,7 @@ export type CampaignVersionListItem = { export type CampaignVersionDetail = CampaignVersionListItem & { raw_json: Record; campaign_json?: Record; + mail_profile_migration_required?: boolean; }; export type CampaignWorkspaceResponse = { @@ -209,6 +210,7 @@ export type CampaignVersionUpdatePayload = { editor_state?: Record | null; source_filename?: string | null; source_base_path?: string | null; + migrate_legacy_mail_settings?: boolean; }; export type CampaignPartialValidationPayload = { @@ -894,7 +896,7 @@ export async function resolveCampaignJobOutcome( settings: ApiSettings, campaignId: string, jobId: string, -decision: "smtp_accepted" | "not_sent", +decision: "smtp_accepted" | "not_sent" | "imap_appended" | "imap_not_appended", note?: string) : Promise> { return apiFetch>(settings, `/api/v1/campaigns/${campaignId}/jobs/${jobId}/resolve-outcome`, { diff --git a/webui/src/api/mail.ts b/webui/src/api/mail.ts index f01076d..2d4277d 100644 --- a/webui/src/api/mail.ts +++ b/webui/src/api/mail.ts @@ -2,17 +2,10 @@ import type { ApiSettings, MailConnectionTestResponse, MailImapFolderListResponse, - MailImapTestPayload, - MailProfilePolicy, - MailProfilePolicyResponse, - MailProfileScope, - MailSecurity, MailServerProfile, - MailServerProfilePayload, - MailSmtpTestPayload, MockMailboxMessageResponse } from "@govoplan/core-webui"; -import { apiFetch, apiGetList, apiPath, apiPost, apiPostJson } from "./client"; +import { apiFetch, apiGetList, apiPost } from "./client"; const profileActionEndpoints = { smtp: "test-smtp", @@ -20,12 +13,6 @@ const profileActionEndpoints = { folders: "list-imap-folders" } as const; -const rawSettingsEndpoints = { - smtp: "/api/v1/mail/test-smtp", - imap: "/api/v1/mail/test-imap", - folders: "/api/v1/mail/list-imap-folders" -} as const; - function runProfileAction( settings: ApiSettings, profileId: string, @@ -37,14 +24,6 @@ function runProfileAction( ); } -function runRawSettingsAction( - settings: ApiSettings, - payload: TPayload, - action: keyof typeof rawSettingsEndpoints -): Promise { - return apiPostJson(settings, rawSettingsEndpoints[action], payload); -} - export async function listMailServerProfiles(settings: ApiSettings, includeInactive = false, campaignId?: string): Promise { return apiGetList(settings, "/api/v1/mail/profiles", "profiles", { include_inactive: includeInactive ? true : undefined, @@ -52,25 +31,6 @@ export async function listMailServerProfiles(settings: ApiSettings, includeInact }); } -export async function createMailServerProfile(settings: ApiSettings, payload: MailServerProfilePayload): Promise { - return apiFetch(settings, "/api/v1/mail/profiles", { - method: "POST", - body: JSON.stringify(payload) - }); -} - -export async function getMailProfilePolicy( - settings: ApiSettings, - scopeType: MailProfileScope, - scopeId?: string | null, - campaignId?: string | null -): Promise { - return apiFetch(settings, apiPath(`/api/v1/mail/policies/${encodeURIComponent(scopeType)}`, { - scope_id: scopeId, - campaign_id: campaignId - })); -} - export async function testMailProfileSmtp(settings: ApiSettings, profileId: string): Promise { return runProfileAction(settings, profileId, "smtp"); } @@ -83,22 +43,8 @@ export async function listMailProfileImapFolders(settings: ApiSettings, profileI return runProfileAction(settings, profileId, "folders"); } -export async function testSmtpSettings(settings: ApiSettings, payload: MailSmtpTestPayload): Promise { - return runRawSettingsAction(settings, payload, "smtp"); -} - -export async function testImapSettings(settings: ApiSettings, payload: MailImapTestPayload): Promise { - return runRawSettingsAction(settings, payload, "imap"); -} - -export async function listImapFolders(settings: ApiSettings, payload: MailImapTestPayload): Promise { - return runRawSettingsAction(settings, payload, "folders"); -} - export async function getMockMailboxMessage(settings: ApiSettings, id: string): Promise { return apiFetch(settings, `/api/v1/dev/mailbox/messages/${encodeURIComponent(id)}`); } -export { mailProfilePatternKeys, mailProfilePolicyLimitKeys } from "@govoplan/core-webui"; -export type { MailConnectionTestResponse, MailCredentialPolicy, MailImapFolderListResponse, MailImapFolderResponse, MailImapTestPayload, MailProfilePatternKey, MailProfilePatternRules, MailProfilePolicy, MailProfilePolicyLimitKey, MailProfilePolicyLimitPermissions, MailProfilePolicyResponse, MailProfileScope, MailSecurity, MailServerProfile, MailServerProfileCredentialsPayload, MailServerProfileListResponse, MailServerProfilePayload, MailSmtpTestPayload, MailTransportCredentialsPayload, MockMailboxMessage, MockMailboxMessageResponse } from "@govoplan/core-webui"; -export type { MailPolicySourceStep as PolicySourceStep } from "@govoplan/core-webui"; +export type { MailConnectionTestResponse, MailImapFolderListResponse, MailImapFolderResponse, MailServerProfile, MailServerProfileListResponse, MockMailboxMessage, MockMailboxMessageResponse } from "@govoplan/core-webui"; diff --git a/webui/src/features/campaigns/MailSettingsPage.tsx b/webui/src/features/campaigns/MailSettingsPage.tsx index 3cc4e6a..a2aaa49 100644 --- a/webui/src/features/campaigns/MailSettingsPage.tsx +++ b/webui/src/features/campaigns/MailSettingsPage.tsx @@ -1,35 +1,35 @@ -import { useEffect, useMemo, useState } from "react"; -import { MailServerFolderLookupResultView, MailServerSettingsPanel, ToggleSwitch, 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 { useEffect, useState } from "react"; +import { + Button, + Card, + DismissibleAlert, + FormField, + LoadingFrame, + MailServerFolderLookupResultView, + MetricCard, + PageTitle, + ToggleSwitch, + usePlatformModuleInstalled, + usePlatformUiCapability, + type MailProfilesUiCapability, + type MailServerConnectionTestResult, + type MailServerFolderLookupResult +} from "@govoplan/core-webui"; +import type { ApiSettings } from "../../types"; import { - createMailServerProfile, - getMailProfilePolicy, - listImapFolders, listMailProfileImapFolders, listMailServerProfiles, - testImapSettings, testMailProfileImap, testMailProfileSmtp, - testSmtpSettings, - type MailProfilePolicy, - type MailSecurity, - type MailServerProfile } from -"../../api/mail"; + 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[]; +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"; @@ -41,24 +41,22 @@ type MailSettingsPageProps = { 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 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 [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 migrationRequired = version?.mail_profile_migration_required === true; const { draft, displayDraft, dirty, saveState, localError, setLocalError, patch, discardDraft, saveDraft } = useCampaignDraftEditor({ settings, campaignId, @@ -68,76 +66,29 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting 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 + 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 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 selectedProfileId = getText(server, "mail_profile_id"); + const selectedProfile = mailProfiles.find((profile) => profile.id === selectedProfileId) ?? null; 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 imapAppendFolder = getText(imapAppend, "folder", getText(imap, "sent_folder", "auto")); - 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); + const selectedProfileHasImap = Boolean(selectedProfile?.imap); + const selectedProfileUnavailable = Boolean(selectedProfileId && !profilesLoading && !selectedProfile); + const canSave = dirty && !locked && Boolean(draft) && (!migrationRequired || Boolean(selectedProfileId)); useEffect(() => { if (!mailModuleInstalled) { setMailProfiles([]); setPolicyProfiles([]); - setEffectiveMailPolicy({ allow_campaign_profiles: true }); - setProfileError(""); setProfilesLoading(false); return; } @@ -149,230 +100,55 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting setProfilesLoading(true); setProfileError(""); try { - const [allowedProfiles, visibleProfiles, policyResponse] = await Promise.all([ - listMailServerProfiles(settings, false, campaignId), - listMailServerProfiles(settings, true), - getMailProfilePolicy(settings, "campaign", campaignId, campaignId)] - ); + const [allowedProfiles, visibleProfiles] = await Promise.all([ + listMailServerProfiles(settings, false, campaignId), + listMailServerProfiles(settings, true) + ]); 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(""); + patch(["server"], profileId ? { mail_profile_id: profileId } : {}); + setSmtpTestResult(null); + setImapTestResult(null); + setFolderResult(null); 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"); + async function runProfileTest(protocol: "smtp" | "imap") { + if (!selectedProfileId || locked) return; + setMailActionState(protocol); setLocalError(""); try { - setSmtpTestResult(selectedProfileId && smtpCredentialsInherited ? - await testMailProfileSmtp(settings, selectedProfileId) : - await testSmtpSettings(settings, rawSmtpPayload())); + if (protocol === "smtp") { + setSmtpTestResult(await testMailProfileSmtp(settings, selectedProfileId)); + } else { + setImapTestResult(await testMailProfileImap(settings, selectedProfileId)); + } } 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: {} }); + 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 (!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; + if (!selectedProfileId || locked || !selectedProfileHasImap) return; setMailActionState("folders"); setLocalError(""); try { - setFolderResult(selectedProfileId && imapCredentialsInherited ? - await listMailProfileImapFolders(settings, selectedProfileId) : - await listImapFolders(settings, rawImapPayload())); + setFolderResult(await listMailProfileImapFolders(settings, selectedProfileId)); } catch (err) { setFolderResult({ ok: false, protocol: "imap", message: err instanceof Error ? err.message : String(err), folders: [], details: {} }); } finally { @@ -382,8 +158,7 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting function useDetectedSentFolder() { const folder = folderResult?.detected_sent_folder; - if (!folder || appendTargetFolderDisabled) return; - patch(["delivery", "imap_append_sent", "folder"], folder); + if (folder && !locked) patch(["delivery", "imap_append_sent", "folder"], folder); } return ( @@ -394,8 +169,8 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
- - {!isPolicyView && } + {!isPolicyView && } +
@@ -405,14 +180,13 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting <> - {!mailModuleInstalled && - - i18n:govoplan-campaign.the_mail_module_is_not_installed_inline_smtp_and.8e0f802e - - } + {!mailModuleInstalled && i18n:govoplan-campaign.install_and_enable_the_mail_module_to_select_a_d.01c75fc4} - {isPolicyView && mailModuleInstalled && MailProfilePolicyEditor && - + i18n:govoplan-campaign.this_version_contains_legacy_campaign_local_mail.44c7a6fd + } + + {isPolicyView && mailModuleInstalled && MailProfilePolicyEditor && + description="i18n:govoplan-campaign.mail_policy_limits_which_mail_owned_profiles_thi.824b1b2e" + onSaved={refreshMailProfiles} />} - } + {isPolicyView && mailModuleInstalled && !MailProfilePolicyEditor && i18n:govoplan-campaign.the_mail_module_did_not_expose_profile_managemen.2cdb57e1} - {isPolicyView && mailModuleInstalled && !MailProfilePolicyEditor && - i18n:govoplan-campaign.the_mail_module_did_not_expose_profile_managemen.2cdb57e1 - } - - {!isPolicyView && mailModuleInstalled && - - - - - }> - + actions={}> +

i18n:govoplan-campaign.campaign_stores_only_this_stable_profile_referen.de554809

- - 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}} -
- } + {selectedProfileUnavailable && i18n:govoplan-campaign.the_referenced_mail_profile_is_inactive_unavaila.abeebe26} + {selectedProfile &&
+ + + + +
} +
+ + +
+ {smtpTestResult && {smtpTestResult.message}} + {imapTestResult && {imapTestResult.message}} + {profileError && {profileError}} + } - {!isPolicyView && - + {!isPolicyView &&
patch(["delivery", "imap_append_sent", "enabled"], checked)} />
patch(["delivery", "imap_append_sent", "folder"], event.target.value)} - placeholder="i18n:govoplan-core.auto.0d612c12" /> -
- +
-
- } - - {!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}
  • )}
-
- } - - -
- } - +
}
- ); - + + ); } - -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); - } +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"; } diff --git a/webui/src/features/campaigns/ReviewSendPage.tsx b/webui/src/features/campaigns/ReviewSendPage.tsx index 604804e..b4dd062 100644 --- a/webui/src/features/campaigns/ReviewSendPage.tsx +++ b/webui/src/features/campaigns/ReviewSendPage.tsx @@ -136,12 +136,7 @@ export default function ReviewSendPage({ settings, campaignId }: {settings: ApiS const version = data.currentVersion; const campaignJson = useMemo(() => getCampaignJson(version), [version]); const server = asRecord(campaignJson.server); - const smtpServer = asRecord(server.smtp); - const imapServer = asRecord(server.imap); - const serverCredentials = asRecord(server.credentials); - const smtpCredentials = asRecord(serverCredentials.smtp); - const imapCredentials = asRecord(serverCredentials.imap); - const selectedMailProfileId = getText(server, "mail_profile_id", getText(server, "profile_id")); + const selectedMailProfileId = getText(server, "mail_profile_id"); const inlineEntries = useMemo( () => asArray(asRecord(campaignJson.entries).inline).map(asRecord), [campaignJson] @@ -398,7 +393,9 @@ export default function ReviewSendPage({ settings, campaignId }: {settings: ApiS const imapAppendResultRows = asArray(imapAppendResult?.results).map(asRecord); const imapDiagnosticRows = imapDiagnostics.jobs.map(asRecord); const imapDiagnosticsPending = imapDiagnosticRows.filter((job) => String(job.imap_status ?? "").toLowerCase() === "pending").length; - const imapDiagnosticsFailed = imapDiagnosticRows.filter((job) => String(job.imap_status ?? "").toLowerCase() === "failed").length; + const imapDiagnosticsFailed = imapDiagnosticRows.filter((job) => + ["failed", "outcome_unknown"].includes(String(job.imap_status ?? "").toLowerCase()), + ).length; const imapPendingForDisplay = Math.max(imapPending, imapDiagnosticsPending); const imapFailedForDisplay = Math.max(imapFailed, imapDiagnosticsFailed); const canAppendPendingImap = Boolean(imapAppend.enabled) && imapPendingForDisplay > 0 && !historicalVersion && !userLockedVersion; @@ -1032,13 +1029,12 @@ export default function ReviewSendPage({ settings, campaignId }: {settings: ApiS const ambiguousAttachments = numberFrom(attachmentSummary, ["ambiguous_configs"]); const messagesPerMinute = numberFrom(rateLimit, ["messages_per_minute"]); const estimatedMinutes = messagesPerMinute > 0 && jobsTotal > 0 ? Math.ceil(jobsTotal / messagesPerMinute) : null; - const smtpConfigured = Boolean(selectedMailProfileId || getText(smtpServer, "host") || getText(smtpCredentials, "username")); - const imapConfigured = Boolean(selectedMailProfileId || getText(imapServer, "host") || getText(imapCredentials, "username")); + const mailProfileSelected = Boolean(selectedMailProfileId); const deliverabilityPreflightItems: DeliverabilityPreflightItem[] = [ { label: "Transport", - detail: smtpConfigured ? selectedMailProfileId ? "Reusable mail profile selected." : "Inline SMTP settings are present." : "Select a reusable mail profile or configure SMTP before live delivery.", - state: smtpConfigured ? "ready" : "blocked" + detail: mailProfileSelected ? "i18n:govoplan-campaign.mail_owned_delivery_profile_selected.4f44778e" : "i18n:govoplan-campaign.select_an_authorized_mail_profile_before_live_de.45c80a42", + state: mailProfileSelected ? "ready" : "blocked" }, { label: "Policy", @@ -1062,8 +1058,8 @@ export default function ReviewSendPage({ settings, campaignId }: {settings: ApiS }, { label: "Sent copy", - detail: Boolean(imapAppend.enabled) ? imapConfigured ? `IMAP append enabled for ${String(imapAppend.folder ?? "auto")}.` : "IMAP append is enabled, but no IMAP server/profile is visible." : "IMAP append is disabled for this campaign.", - state: Boolean(imapAppend.enabled) ? imapConfigured ? "ready" : "blocked" : "info" + detail: Boolean(imapAppend.enabled) ? mailProfileSelected ? i18nMessage("i18n:govoplan-campaign.imap_append_requested_for_value0_validation_chec.51527a20", { value0: String(imapAppend.folder ?? "auto") }) : "i18n:govoplan-campaign.imap_append_is_enabled_but_no_mail_profile_is_se.cf50419e" : "i18n:govoplan-campaign.imap_append_is_disabled_for_this_campaign.7757f7f1", + state: Boolean(imapAppend.enabled) ? mailProfileSelected ? "ready" : "blocked" : "info" }]; const canCompleteInspection = blockingReviewCount === 0 && reviewRequiredCount > 0 && diff --git a/webui/src/features/campaigns/components/LockedVersionNotice.tsx b/webui/src/features/campaigns/components/LockedVersionNotice.tsx index f608d46..bb6895f 100644 --- a/webui/src/features/campaigns/components/LockedVersionNotice.tsx +++ b/webui/src/features/campaigns/components/LockedVersionNotice.tsx @@ -43,6 +43,9 @@ export default function LockedVersionNotice({ settings, campaignId, version, cur const permanentUserLock = isPermanentUserLockedVersion(version); const finalLock = isFinalLockedVersion(version); const canCreateEditableCopy = !historicalVersion && (permanentUserLock || finalLock); + const mailProfileMigrationRequired = Boolean( + version && "mail_profile_migration_required" in version && version.mail_profile_migration_required + ); const presentation = lockPresentation(version, { historicalVersion, validationLock, @@ -83,7 +86,8 @@ export default function LockedVersionNotice({ settings, campaignId, version, cur try { const result = await forkCampaignVersion(settings, campaignId, version.id, { current_flow: "manual", - current_step: version.current_step ?? null + current_step: version.current_step ?? null, + migrate_legacy_mail_settings: mailProfileMigrationRequired }); setLocalMessage(`Created editable version #${result.version.version_number}.`); await reload(); @@ -100,6 +104,7 @@ export default function LockedVersionNotice({ settings, campaignId, version, cur {presentation.title}{" "} {presentation.description} {message && {message}} + {mailProfileMigrationRequired && i18n:govoplan-campaign.the_editable_copy_will_preserve_this_audit_recor.aac956f1} {presentation.info && {presentation.info}} {localMessage && {localMessage}} {localError && {localError}} @@ -232,4 +237,4 @@ function confirmDialogLabel(action: ConfirmAction): string { if (action === "unlock-user") return "i18n:govoplan-campaign.unlock.1526a17e"; if (action === "permanent") return "i18n:govoplan-campaign.lock_permanently.cc0ce9e7"; return "i18n:govoplan-campaign.confirm.04a21221"; -} \ No newline at end of file +} diff --git a/webui/src/features/campaigns/policyUi.ts b/webui/src/features/campaigns/policyUi.ts deleted file mode 100644 index 0c199c0..0000000 --- a/webui/src/features/campaigns/policyUi.ts +++ /dev/null @@ -1,36 +0,0 @@ -export const INLINE_MAIL_SETTINGS_BLOCKED_MESSAGE = "i18n:govoplan-campaign.inline_smtp_imap_settings_are_blocked_by_the_eff.90c94538"; - -export type CampaignMailPolicy = { - allow_campaign_profiles?: boolean | null; -}; - -export type CampaignMailSettingsPolicyState = { - campaignProfilesAllowed: boolean; - usingMailProfile: boolean; - inlineMailSettingsBlocked: boolean; - inlineOptionDisabled: boolean; - canSelectInlineSettings: boolean; - inlineBlockedMessage: string; -}; - -export function campaignMailSettingsPolicyState({ - effectivePolicy, - selectedProfileId, - locked = false - - - - -}: {effectivePolicy: CampaignMailPolicy | null | undefined;selectedProfileId: string | null | undefined;locked?: boolean;}): CampaignMailSettingsPolicyState { - const campaignProfilesAllowed = effectivePolicy?.allow_campaign_profiles === true; - const usingMailProfile = Boolean(selectedProfileId); - const inlineMailSettingsBlocked = !campaignProfilesAllowed && !usingMailProfile; - return { - campaignProfilesAllowed, - usingMailProfile, - inlineMailSettingsBlocked, - inlineOptionDisabled: !campaignProfilesAllowed, - canSelectInlineSettings: !locked && campaignProfilesAllowed, - inlineBlockedMessage: INLINE_MAIL_SETTINGS_BLOCKED_MESSAGE - }; -} \ No newline at end of file diff --git a/webui/src/features/campaigns/utils/mailProfileReference.ts b/webui/src/features/campaigns/utils/mailProfileReference.ts new file mode 100644 index 0000000..3ae198c --- /dev/null +++ b/webui/src/features/campaigns/utils/mailProfileReference.ts @@ -0,0 +1,13 @@ +export function campaignMailProfileReferenceOnly(value: Record): Record { + const server = isRecord(value.server) ? value.server : {}; + const rawProfileId = server.mail_profile_id; + const profileId = typeof rawProfileId === "string" ? rawProfileId.trim() : ""; + return { + ...value, + server: profileId ? { mail_profile_id: profileId } : {} + }; +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} diff --git a/webui/src/features/campaigns/wizard/steps/CreateWizardSteps.tsx b/webui/src/features/campaigns/wizard/steps/CreateWizardSteps.tsx index 0db14b4..a4a1774 100644 --- a/webui/src/features/campaigns/wizard/steps/CreateWizardSteps.tsx +++ b/webui/src/features/campaigns/wizard/steps/CreateWizardSteps.tsx @@ -49,8 +49,6 @@ export function SenderStep({ draft, patch }: WizardStepProps) { const globalCc = addressesFromValue(recipients.cc); const globalBcc = addressesFromValue(recipients.bcc); const globalReplyTo = addressesFromValue(recipients.reply_to); - const server = asRecord(draft.server); - const smtp = asRecord(server.smtp); const delivery = asRecord(draft.delivery); const imapAppend = asRecord(delivery.imap_append_sent); return ( @@ -106,8 +104,7 @@ export function SenderStep({ draft, patch }: WizardStepProps) { onChange={(addresses: MailboxAddress[]) => patch(["recipients", "reply_to"], addresses.slice(0, 1))} /> - patch(["server", "smtp", "host"], event.target.value)} /> - patch(["server", "smtp", "port"], Number(event.target.value || 0))} /> +

i18n:govoplan-campaign.select_the_delivery_profile_on_the_mail_settings.a89cbb5f

patch(["delivery", "imap_append_sent", "enabled"], checked)} /> ); @@ -232,4 +229,4 @@ function JsonEditor({ value, onValid }: {value: unknown;onValid: (value: unknown {Array.isArray(value) && value.length > 0 &&

i18n:govoplan-campaign.preview.4bf30626 {stringifyPreview(asArray(value)[0], 140)}

} ); -} \ No newline at end of file +} diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts index b3894fa..6bec95e 100644 --- a/webui/src/i18n/generatedTranslations.ts +++ b/webui/src/i18n/generatedTranslations.ts @@ -4,6 +4,26 @@ export const generatedTranslations: PlatformTranslations = { "en": { "i18n:govoplan-campaign.1_campaign.ccd70074": "1 campaign", "i18n:govoplan-campaign.2_campaigns.35b84804": "2 campaigns", + "i18n:govoplan-campaign.campaign_scoped_mail_profile.9cbf3505": "Campaign-scoped Mail profile", + "i18n:govoplan-campaign.campaign_stores_only_this_stable_profile_referen.de554809": "Campaign stores only this stable profile reference. The Mail module owns, encrypts, authorizes, tests, and resolves all SMTP/IMAP settings and credentials.", + "i18n:govoplan-campaign.configured.668c5fff": "Configured", + "i18n:govoplan-campaign.discard.36fff63c": "Discard", + "i18n:govoplan-campaign.imap_append_is_disabled_for_this_campaign.7757f7f1": "IMAP append is disabled for this campaign.", + "i18n:govoplan-campaign.imap_append_is_enabled_but_no_mail_profile_is_se.cf50419e": "IMAP append is enabled, but no Mail profile is selected.", + "i18n:govoplan-campaign.imap_append_requested_for_value0_validation_chec.51527a20": "IMAP append requested for {value0}; validation checks that the selected profile supports it.", + "i18n:govoplan-campaign.install_and_enable_the_mail_module_to_select_a_d.01c75fc4": "Install and enable the Mail module to select a delivery profile. Campaign does not store inline SMTP/IMAP settings.", + "i18n:govoplan-campaign.mail_owned_delivery_profile_selected.4f44778e": "Mail-owned delivery profile selected.", + "i18n:govoplan-campaign.mail_policy_limits_which_mail_owned_profiles_thi.824b1b2e": "Mail policy limits which Mail-owned profiles this campaign may reference.", + "i18n:govoplan-campaign.not_configured.811931bb": "Not configured", + "i18n:govoplan-campaign.select_a_mail_profile.76480af0": "Select a Mail profile", + "i18n:govoplan-campaign.select_an_authorized_mail_profile_before_live_de.45c80a42": "Select an authorized Mail profile before live delivery.", + "i18n:govoplan-campaign.select_the_delivery_profile_on_the_mail_settings.a89cbb5f": "Select the delivery profile on the Mail settings page. Campaign stores only the Mail profile reference; SMTP/IMAP settings and credentials remain in Mail.", + "i18n:govoplan-campaign.system.bc0792d8": "System", + "i18n:govoplan-campaign.testing_imap.13d255cf": "Testing IMAP…", + "i18n:govoplan-campaign.testing_smtp.8e9f8247": "Testing SMTP…", + "i18n:govoplan-campaign.the_editable_copy_will_preserve_this_audit_recor.aac956f1": "The editable copy will preserve this audit record while removing legacy inline mail transport data; select a Mail profile before validation.", + "i18n:govoplan-campaign.the_referenced_mail_profile_is_inactive_unavaila.abeebe26": "The referenced Mail profile is inactive, unavailable, or no longer authorized for this campaign. Select another profile.", + "i18n:govoplan-campaign.this_version_contains_legacy_campaign_local_mail.44c7a6fd": "This version contains legacy campaign-local mail transport data. It is preserved in storage and blocked from delivery. Select a Mail profile and save here to record an audited migration; no inline credentials are returned to the browser.", "i18n:govoplan-campaign.a_dry_run_checks_the_frozen_queue_and_delivery_c.c42924bb": "A dry run checks the frozen queue and delivery configuration without contacting SMTP or IMAP.", "i18n:govoplan-campaign.a_human_readable_name_shown_in_lists_and_reports.afc23e7e": "A human-readable name shown in lists and reports.", "i18n:govoplan-campaign.a_mapping_wizard_can_be_added_later.6b2fe0f3": "A mapping wizard can be added later", @@ -1156,6 +1176,26 @@ export const generatedTranslations: PlatformTranslations = { "de": { "i18n:govoplan-campaign.1_campaign.ccd70074": "1 campaign", "i18n:govoplan-campaign.2_campaigns.35b84804": "2 campaigns", + "i18n:govoplan-campaign.campaign_scoped_mail_profile.9cbf3505": "Kampagnenspezifisches Mail-Profil", + "i18n:govoplan-campaign.campaign_stores_only_this_stable_profile_referen.de554809": "Campaign speichert nur diese stabile Profilreferenz. Das Mail-Modul verwaltet, verschlüsselt, autorisiert, testet und löst alle SMTP-/IMAP-Einstellungen und Zugangsdaten auf.", + "i18n:govoplan-campaign.configured.668c5fff": "Konfiguriert", + "i18n:govoplan-campaign.discard.36fff63c": "Verwerfen", + "i18n:govoplan-campaign.imap_append_is_disabled_for_this_campaign.7757f7f1": "Das IMAP-Ablegen ist für diese Kampagne deaktiviert.", + "i18n:govoplan-campaign.imap_append_is_enabled_but_no_mail_profile_is_se.cf50419e": "Das IMAP-Ablegen ist aktiviert, aber es ist kein Mail-Profil ausgewählt.", + "i18n:govoplan-campaign.imap_append_requested_for_value0_validation_chec.51527a20": "IMAP-Ablegen in {value0} angefordert; die Validierung prüft, ob das ausgewählte Profil dies unterstützt.", + "i18n:govoplan-campaign.install_and_enable_the_mail_module_to_select_a_d.01c75fc4": "Installieren und aktivieren Sie das Mail-Modul, um ein Versandprofil auszuwählen. Campaign speichert keine eingebetteten SMTP-/IMAP-Einstellungen.", + "i18n:govoplan-campaign.mail_owned_delivery_profile_selected.4f44778e": "Ein vom Mail-Modul verwaltetes Versandprofil ist ausgewählt.", + "i18n:govoplan-campaign.mail_policy_limits_which_mail_owned_profiles_thi.824b1b2e": "Die Mail-Richtlinie begrenzt, auf welche vom Mail-Modul verwalteten Profile diese Kampagne verweisen darf.", + "i18n:govoplan-campaign.not_configured.811931bb": "Nicht konfiguriert", + "i18n:govoplan-campaign.select_a_mail_profile.76480af0": "Mail-Profil auswählen", + "i18n:govoplan-campaign.select_an_authorized_mail_profile_before_live_de.45c80a42": "Wählen Sie vor dem Live-Versand ein autorisiertes Mail-Profil aus.", + "i18n:govoplan-campaign.select_the_delivery_profile_on_the_mail_settings.a89cbb5f": "Wählen Sie das Versandprofil auf der Seite Mail-Einstellungen aus. Campaign speichert nur die Mail-Profilreferenz; SMTP-/IMAP-Einstellungen und Zugangsdaten verbleiben im Mail-Modul.", + "i18n:govoplan-campaign.system.bc0792d8": "System", + "i18n:govoplan-campaign.testing_imap.13d255cf": "IMAP wird getestet…", + "i18n:govoplan-campaign.testing_smtp.8e9f8247": "SMTP wird getestet…", + "i18n:govoplan-campaign.the_editable_copy_will_preserve_this_audit_recor.aac956f1": "Die bearbeitbare Kopie bewahrt diesen Prüfdatensatz und entfernt zugleich alte eingebettete Mail-Transportdaten; wählen Sie vor der Validierung ein Mail-Profil aus.", + "i18n:govoplan-campaign.the_referenced_mail_profile_is_inactive_unavaila.abeebe26": "Das referenzierte Mail-Profil ist inaktiv, nicht verfügbar oder für diese Kampagne nicht mehr autorisiert. Wählen Sie ein anderes Profil aus.", + "i18n:govoplan-campaign.this_version_contains_legacy_campaign_local_mail.44c7a6fd": "Diese Version enthält alte kampagnenlokale Mail-Transportdaten. Sie bleiben gespeichert und der Versand ist gesperrt. Wählen Sie ein Mail-Profil und speichern Sie hier, um eine protokollierte Migration festzuhalten; eingebettete Zugangsdaten werden nicht an den Browser zurückgegeben.", "i18n:govoplan-campaign.a_dry_run_checks_the_frozen_queue_and_delivery_c.c42924bb": "A dry run checks the frozen queue and delivery configuration without contacting SMTP or IMAP.", "i18n:govoplan-campaign.a_human_readable_name_shown_in_lists_and_reports.afc23e7e": "A human-readable name shown in lists and reports.", "i18n:govoplan-campaign.a_mapping_wizard_can_be_added_later.6b2fe0f3": "A mapping wizard can be added later", diff --git a/webui/src/index.ts b/webui/src/index.ts index 0cb0c46..9a9ae6e 100644 --- a/webui/src/index.ts +++ b/webui/src/index.ts @@ -1,7 +1,7 @@ export { default } from "./module"; export * from "./module"; export * from "./api/campaigns"; -export * from "./features/campaigns/policyUi"; +export * from "./features/campaigns/utils/mailProfileReference"; export { default as CampaignListPage } from "./features/campaigns/CampaignListPage"; export { default as CampaignWorkspace } from "./features/campaigns/CampaignWorkspace"; export { default as OperatorQueuePage } from "./features/operator/OperatorQueuePage"; diff --git a/webui/tests/policy-ui.test.ts b/webui/tests/policy-ui.test.ts index c2f3130..df5073f 100644 --- a/webui/tests/policy-ui.test.ts +++ b/webui/tests/policy-ui.test.ts @@ -1,42 +1,26 @@ -import { campaignMailSettingsPolicyState } from "../src/features/campaigns/policyUi"; +import { campaignMailProfileReferenceOnly } from "../src/features/campaigns/utils/mailProfileReference"; function assert(condition: unknown, message: string): void { if (!condition) throw new Error(message); } -const blockedInline = campaignMailSettingsPolicyState({ - effectivePolicy: { allow_campaign_profiles: false }, - selectedProfileId: "" -}); +const source = { + campaign: { id: "campaign-1" }, + server: { + mail_profile_id: " profile-1 ", + smtp: { host: "smtp.example.test", password: "smtp-secret" }, + imap: { host: "imap.example.test", password: "imap-secret" }, + credentials: { smtp: { username: "sender", password: "legacy-secret" } }, + inherit_smtp_credentials: false + } +}; -assert(blockedInline.campaignProfilesAllowed === false, "Blocked policy must not allow campaign-local settings."); -assert(blockedInline.inlineMailSettingsBlocked === true, "Inline SMTP/IMAP must be blocked when no reusable profile is selected."); -assert(blockedInline.inlineOptionDisabled === true, "Inline option must be disabled under a blocking policy."); -assert(blockedInline.canSelectInlineSettings === false, "Blocked inline option must not be selectable."); +const normalized = campaignMailProfileReferenceOnly(source); +const server = normalized.server as Record; -const blockedWithReusableProfile = campaignMailSettingsPolicyState({ - effectivePolicy: { allow_campaign_profiles: false }, - selectedProfileId: "tenant-profile" -}); +assert(server.mail_profile_id === "profile-1", "The stable Mail profile reference must be retained and trimmed."); +assert(Object.keys(server).length === 1, "All campaign-local transport settings and credentials must be removed."); +assert((source.server.smtp as Record).password === "smtp-secret", "Normalization must not mutate the loaded audit representation."); -assert(blockedWithReusableProfile.inlineMailSettingsBlocked === false, "Reusable profiles must remain usable when campaign-local settings are blocked."); -assert(blockedWithReusableProfile.inlineOptionDisabled === true, "Inline option remains disabled even while a reusable profile is selected."); - -const allowedInline = campaignMailSettingsPolicyState({ - effectivePolicy: { allow_campaign_profiles: true }, - selectedProfileId: "" -}); - -assert(allowedInline.campaignProfilesAllowed === true, "Allowed policy must allow campaign-local settings."); -assert(allowedInline.inlineMailSettingsBlocked === false, "Inline SMTP/IMAP must be available when campaign-local settings are allowed."); -assert(allowedInline.inlineOptionDisabled === false, "Inline option must be enabled under an allowing policy."); -assert(allowedInline.canSelectInlineSettings === true, "Allowed inline option must be selectable."); - -const lockedInline = campaignMailSettingsPolicyState({ - effectivePolicy: { allow_campaign_profiles: true }, - selectedProfileId: "", - locked: true -}); - -assert(lockedInline.inlineOptionDisabled === false, "Policy does not disable inline settings when the policy allows them."); -assert(lockedInline.canSelectInlineSettings === false, "A locked version still must not allow selecting inline settings."); +const withoutProfile = campaignMailProfileReferenceOnly({ server: { smtp: { host: "legacy" } } }); +assert(Object.keys(withoutProfile.server as Record).length === 0, "Legacy settings without a profile must normalize to an empty reference object."); diff --git a/webui/tsconfig.policy-tests.json b/webui/tsconfig.policy-tests.json index 01ffac4..5495469 100644 --- a/webui/tsconfig.policy-tests.json +++ b/webui/tsconfig.policy-tests.json @@ -15,6 +15,6 @@ }, "include": [ "tests/policy-ui.test.ts", - "src/features/campaigns/policyUi.ts" + "src/features/campaigns/utils/mailProfileReference.ts" ] }