refactor(webui): select Mail-owned profiles for campaigns

This commit is contained in:
2026-07-21 17:13:22 +02:00
parent 2c5519908a
commit 701c0fe184
13 changed files with 210 additions and 561 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/campaign-webui", "name": "@govoplan/campaign-webui",
"version": "0.1.8", "version": "0.1.9",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",

View File

@@ -81,6 +81,7 @@ export type CampaignVersionListItem = {
export type CampaignVersionDetail = CampaignVersionListItem & { export type CampaignVersionDetail = CampaignVersionListItem & {
raw_json: Record<string, unknown>; raw_json: Record<string, unknown>;
campaign_json?: Record<string, unknown>; campaign_json?: Record<string, unknown>;
mail_profile_migration_required?: boolean;
}; };
export type CampaignWorkspaceResponse = { export type CampaignWorkspaceResponse = {
@@ -209,6 +210,7 @@ export type CampaignVersionUpdatePayload = {
editor_state?: Record<string, unknown> | null; editor_state?: Record<string, unknown> | null;
source_filename?: string | null; source_filename?: string | null;
source_base_path?: string | null; source_base_path?: string | null;
migrate_legacy_mail_settings?: boolean;
}; };
export type CampaignPartialValidationPayload = { export type CampaignPartialValidationPayload = {
@@ -894,7 +896,7 @@ export async function resolveCampaignJobOutcome(
settings: ApiSettings, settings: ApiSettings,
campaignId: string, campaignId: string,
jobId: string, jobId: string,
decision: "smtp_accepted" | "not_sent", decision: "smtp_accepted" | "not_sent" | "imap_appended" | "imap_not_appended",
note?: string) note?: string)
: Promise<Record<string, unknown>> { : Promise<Record<string, unknown>> {
return apiFetch<Record<string, unknown>>(settings, `/api/v1/campaigns/${campaignId}/jobs/${jobId}/resolve-outcome`, { return apiFetch<Record<string, unknown>>(settings, `/api/v1/campaigns/${campaignId}/jobs/${jobId}/resolve-outcome`, {

View File

@@ -2,17 +2,10 @@ import type {
ApiSettings, ApiSettings,
MailConnectionTestResponse, MailConnectionTestResponse,
MailImapFolderListResponse, MailImapFolderListResponse,
MailImapTestPayload,
MailProfilePolicy,
MailProfilePolicyResponse,
MailProfileScope,
MailSecurity,
MailServerProfile, MailServerProfile,
MailServerProfilePayload,
MailSmtpTestPayload,
MockMailboxMessageResponse MockMailboxMessageResponse
} from "@govoplan/core-webui"; } from "@govoplan/core-webui";
import { apiFetch, apiGetList, apiPath, apiPost, apiPostJson } from "./client"; import { apiFetch, apiGetList, apiPost } from "./client";
const profileActionEndpoints = { const profileActionEndpoints = {
smtp: "test-smtp", smtp: "test-smtp",
@@ -20,12 +13,6 @@ const profileActionEndpoints = {
folders: "list-imap-folders" folders: "list-imap-folders"
} as const; } 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<TResponse>( function runProfileAction<TResponse>(
settings: ApiSettings, settings: ApiSettings,
profileId: string, profileId: string,
@@ -37,14 +24,6 @@ function runProfileAction<TResponse>(
); );
} }
function runRawSettingsAction<TResponse, TPayload>(
settings: ApiSettings,
payload: TPayload,
action: keyof typeof rawSettingsEndpoints
): Promise<TResponse> {
return apiPostJson<TResponse>(settings, rawSettingsEndpoints[action], payload);
}
export async function listMailServerProfiles(settings: ApiSettings, includeInactive = false, campaignId?: string): Promise<MailServerProfile[]> { export async function listMailServerProfiles(settings: ApiSettings, includeInactive = false, campaignId?: string): Promise<MailServerProfile[]> {
return apiGetList<MailServerProfile, "profiles">(settings, "/api/v1/mail/profiles", "profiles", { return apiGetList<MailServerProfile, "profiles">(settings, "/api/v1/mail/profiles", "profiles", {
include_inactive: includeInactive ? true : undefined, 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<MailServerProfile> {
return apiFetch<MailServerProfile>(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<MailProfilePolicyResponse> {
return apiFetch<MailProfilePolicyResponse>(settings, apiPath(`/api/v1/mail/policies/${encodeURIComponent(scopeType)}`, {
scope_id: scopeId,
campaign_id: campaignId
}));
}
export async function testMailProfileSmtp(settings: ApiSettings, profileId: string): Promise<MailConnectionTestResponse> { export async function testMailProfileSmtp(settings: ApiSettings, profileId: string): Promise<MailConnectionTestResponse> {
return runProfileAction<MailConnectionTestResponse>(settings, profileId, "smtp"); return runProfileAction<MailConnectionTestResponse>(settings, profileId, "smtp");
} }
@@ -83,22 +43,8 @@ export async function listMailProfileImapFolders(settings: ApiSettings, profileI
return runProfileAction<MailImapFolderListResponse>(settings, profileId, "folders"); return runProfileAction<MailImapFolderListResponse>(settings, profileId, "folders");
} }
export async function testSmtpSettings(settings: ApiSettings, payload: MailSmtpTestPayload): Promise<MailConnectionTestResponse> {
return runRawSettingsAction<MailConnectionTestResponse, MailSmtpTestPayload>(settings, payload, "smtp");
}
export async function testImapSettings(settings: ApiSettings, payload: MailImapTestPayload): Promise<MailConnectionTestResponse> {
return runRawSettingsAction<MailConnectionTestResponse, MailImapTestPayload>(settings, payload, "imap");
}
export async function listImapFolders(settings: ApiSettings, payload: MailImapTestPayload): Promise<MailImapFolderListResponse> {
return runRawSettingsAction<MailImapFolderListResponse, MailImapTestPayload>(settings, payload, "folders");
}
export async function getMockMailboxMessage(settings: ApiSettings, id: string): Promise<MockMailboxMessageResponse> { export async function getMockMailboxMessage(settings: ApiSettings, id: string): Promise<MockMailboxMessageResponse> {
return apiFetch<MockMailboxMessageResponse>(settings, `/api/v1/dev/mailbox/messages/${encodeURIComponent(id)}`); return apiFetch<MockMailboxMessageResponse>(settings, `/api/v1/dev/mailbox/messages/${encodeURIComponent(id)}`);
} }
export { mailProfilePatternKeys, mailProfilePolicyLimitKeys } from "@govoplan/core-webui"; export type { MailConnectionTestResponse, MailImapFolderListResponse, MailImapFolderResponse, MailServerProfile, MailServerProfileListResponse, MockMailboxMessage, MockMailboxMessageResponse } 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";

View File

@@ -1,35 +1,35 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, 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 {
import type { ApiSettings } from "../../types"; Button,
import { Button } from "@govoplan/core-webui"; Card,
import { Card } from "@govoplan/core-webui"; DismissibleAlert,
import { FormField } from "@govoplan/core-webui"; FormField,
import { PageTitle } from "@govoplan/core-webui"; LoadingFrame,
import { LoadingFrame } from "@govoplan/core-webui"; MailServerFolderLookupResultView,
import LockedVersionNotice from "./components/LockedVersionNotice"; MetricCard,
import VersionLine from "./components/VersionLine"; PageTitle,
import { DismissibleAlert, i18nMessage } from "@govoplan/core-webui"; ToggleSwitch,
usePlatformModuleInstalled,
usePlatformUiCapability,
type MailProfilesUiCapability,
type MailServerConnectionTestResult,
type MailServerFolderLookupResult
} from "@govoplan/core-webui";
import type { ApiSettings } from "../../types";
import { import {
createMailServerProfile,
getMailProfilePolicy,
listImapFolders,
listMailProfileImapFolders, listMailProfileImapFolders,
listMailServerProfiles, listMailServerProfiles,
testImapSettings,
testMailProfileImap, testMailProfileImap,
testMailProfileSmtp, testMailProfileSmtp,
testSmtpSettings, type MailServerProfile
type MailProfilePolicy, } from "../../api/mail";
type MailSecurity,
type MailServerProfile } from
"../../api/mail";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor"; import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView"; import LockedVersionNotice from "./components/LockedVersionNotice";
import { cloneJson, getBool, getNumber, getText } from "./utils/draftEditor"; import VersionLine from "./components/VersionLine";
import { campaignMailSettingsPolicyState } from "./policyUi"; import { asRecord, isAuditLockedVersion } from "./utils/campaignView";
import { getBool, getText } from "./utils/draftEditor";
const securityOptions = mailServerSecurityOptions as readonly MailSecurity[]; import { campaignMailProfileReferenceOnly } from "./utils/mailProfileReference";
type MailSettingsView = "settings" | "policy"; type MailSettingsView = "settings" | "policy";
@@ -41,24 +41,22 @@ type MailSettingsPageProps = {
export default function MailSettingsPage({ settings, campaignId, view = "settings" }: MailSettingsPageProps) { export default function MailSettingsPage({ settings, campaignId, view = "settings" }: MailSettingsPageProps) {
const mailModuleInstalled = usePlatformModuleInstalled("mail"); const mailModuleInstalled = usePlatformModuleInstalled("mail");
const isPolicyView = view === "policy";
const mailProfilesUi = usePlatformUiCapability<MailProfilesUiCapability>("mail.profiles"); const mailProfilesUi = usePlatformUiCapability<MailProfilesUiCapability>("mail.profiles");
const MailProfilePolicyEditor = mailProfilesUi?.MailProfilePolicyEditor ?? null; const MailProfilePolicyEditor = mailProfilesUi?.MailProfilePolicyEditor ?? null;
const isPolicyView = view === "policy";
const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId); const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId);
const [mailProfiles, setMailProfiles] = useState<MailServerProfile[]>([]);
const [policyProfiles, setPolicyProfiles] = useState<MailServerProfile[]>([]);
const [profilesLoading, setProfilesLoading] = useState(false);
const [profileError, setProfileError] = useState("");
const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | "folders" | null>(null);
const [smtpTestResult, setSmtpTestResult] = useState<MailServerConnectionTestResult | null>(null); const [smtpTestResult, setSmtpTestResult] = useState<MailServerConnectionTestResult | null>(null);
const [imapTestResult, setImapTestResult] = useState<MailServerConnectionTestResult | null>(null); const [imapTestResult, setImapTestResult] = useState<MailServerConnectionTestResult | null>(null);
const [folderResult, setFolderResult] = useState<MailServerFolderLookupResult | null>(null); const [folderResult, setFolderResult] = useState<MailServerFolderLookupResult | null>(null);
const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | "folders" | null>(null);
const [mailProfiles, setMailProfiles] = useState<MailServerProfile[]>([]);
const [policyProfiles, setPolicyProfiles] = useState<MailServerProfile[]>([]);
const [effectiveMailPolicy, setEffectiveMailPolicy] = useState<MailProfilePolicy | null>(null);
const [profilesLoading, setProfilesLoading] = useState(false);
const [profileName, setProfileName] = useState("");
const [profileMessage, setProfileMessage] = useState("");
const [profileError, setProfileError] = useState("");
const version = data.currentVersion; const version = data.currentVersion;
const locked = isAuditLockedVersion(version, data.campaign?.current_version_id); 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({ const { draft, displayDraft, dirty, saveState, localError, setLocalError, patch, discardDraft, saveDraft } = useCampaignDraftEditor({
settings, settings,
campaignId, campaignId,
@@ -68,76 +66,29 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
setError, setError,
currentStep: isPolicyView ? "mail-policy" : "mail-settings", currentStep: isPolicyView ? "mail-policy" : "mail-settings",
unsavedTitle: isPolicyView ? "i18n:govoplan-campaign.unsaved_mail_policy_changes.c9327491" : "i18n:govoplan-campaign.unsaved_mail_settings.38e1536b", unsavedTitle: isPolicyView ? "i18n:govoplan-campaign.unsaved_mail_policy_changes.c9327491" : "i18n:govoplan-campaign.unsaved_mail_settings.38e1536b",
unsavedMessage: isPolicyView ? unsavedMessage: isPolicyView
"i18n:govoplan-campaign.mail_policy_changes_have_unsaved_draft_changes_s.5aee7d4e" : ? "i18n:govoplan-campaign.mail_policy_changes_have_unsaved_draft_changes_s.5aee7d4e"
"i18n:govoplan-campaign.mail_settings_have_unsaved_changes_save_them_bef.52644559", : "i18n:govoplan-campaign.mail_settings_have_unsaved_changes_save_them_bef.52644559",
transformDraftBeforeSave: normalizeMailSettingsBeforeSave transformDraftBeforeSave: campaignMailProfileReferenceOnly,
extraPayload: () => ({
migrate_legacy_mail_settings: migrationRequired && Boolean(selectedProfileId)
})
}); });
const server = asRecord(displayDraft.server); const server = asRecord(displayDraft.server);
const smtp = asRecord(server.smtp); const selectedProfileId = getText(server, "mail_profile_id");
const imap = asRecord(server.imap); const selectedProfile = mailProfiles.find((profile) => profile.id === selectedProfileId) ?? null;
const credentials = asRecord(server.credentials);
const smtpCredentials = asRecord(credentials.smtp);
const imapCredentials = asRecord(credentials.imap);
const delivery = asRecord(displayDraft.delivery); const delivery = asRecord(displayDraft.delivery);
const imapAppend = asRecord(delivery.imap_append_sent); 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 imapAppendEnabled = getBool(imapAppend, "enabled");
const imapAppendFolder = getText(imapAppend, "folder", getText(imap, "sent_folder", "auto")); const selectedProfileHasImap = Boolean(selectedProfile?.imap);
const appendTargetFolderDisabled = imapDisabled || !imapAppendEnabled; const selectedProfileUnavailable = Boolean(selectedProfileId && !profilesLoading && !selectedProfile);
const inlinePolicyMessages = useMemo(() => { const canSave = dirty && !locked && Boolean(draft) && (!migrationRequired || Boolean(selectedProfileId));
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(() => { useEffect(() => {
if (!mailModuleInstalled) { if (!mailModuleInstalled) {
setMailProfiles([]); setMailProfiles([]);
setPolicyProfiles([]); setPolicyProfiles([]);
setEffectiveMailPolicy({ allow_campaign_profiles: true });
setProfileError("");
setProfilesLoading(false); setProfilesLoading(false);
return; return;
} }
@@ -149,230 +100,55 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
setProfilesLoading(true); setProfilesLoading(true);
setProfileError(""); setProfileError("");
try { try {
const [allowedProfiles, visibleProfiles, policyResponse] = await Promise.all([ const [allowedProfiles, visibleProfiles] = await Promise.all([
listMailServerProfiles(settings, false, campaignId), listMailServerProfiles(settings, false, campaignId),
listMailServerProfiles(settings, true), listMailServerProfiles(settings, true)
getMailProfilePolicy(settings, "campaign", campaignId, campaignId)] ]);
);
setMailProfiles(allowedProfiles); setMailProfiles(allowedProfiles);
setPolicyProfiles(visibleProfiles); setPolicyProfiles(visibleProfiles);
setEffectiveMailPolicy(policyResponse.effective_policy ?? null);
} catch (err) { } catch (err) {
setMailProfiles([]); setMailProfiles([]);
setPolicyProfiles([]); setPolicyProfiles([]);
setEffectiveMailPolicy(null);
setProfileError(err instanceof Error ? err.message : String(err)); setProfileError(err instanceof Error ? err.message : String(err));
} finally { } finally {
setProfilesLoading(false); setProfilesLoading(false);
} }
} }
function normalizeMailSettingsBeforeSave(value: Record<string, unknown>): Record<string, unknown> {
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<string, unknown>,
credentialsValue: Record<string, unknown>,
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) { function selectMailProfile(profileId: string) {
if (!mailModuleInstalled || locked) return; if (!mailModuleInstalled || locked) return;
if (!profileId && !campaignProfilesAllowed) { patch(["server"], profileId ? { mail_profile_id: profileId } : {});
setProfileError(mailPolicyState.inlineBlockedMessage); setSmtpTestResult(null);
return; setImapTestResult(null);
} setFolderResult(null);
patch(["server", "mail_profile_id"], profileId);
setProfileMessage("");
setProfileError(""); setProfileError("");
if (profileId) {
patch(["server", "smtp"], {});
patch(["server", "imap"], {});
patch(["server", "credentials"], {});
setSmtpTestResult(null);
setImapTestResult(null);
setFolderResult(null);
}
} }
async function saveCurrentSettingsAsProfile() { async function runProfileTest(protocol: "smtp" | "imap") {
if (!mailModuleInstalled || locked || usingMailProfile || !campaignProfilesAllowed) return; if (!selectedProfileId || locked) return;
setProfilesLoading(true); setMailActionState(protocol);
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<MailServerSmtpSettings>) {
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<MailServerImapSettings>) {
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<MailServerCredentialSettings>) {
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<MailServerCredentialSettings>) {
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<MailSecurity>(
{ 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<MailSecurity>(
{ 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(""); setLocalError("");
try { try {
setSmtpTestResult(selectedProfileId && smtpCredentialsInherited ? if (protocol === "smtp") {
await testMailProfileSmtp(settings, selectedProfileId) : setSmtpTestResult(await testMailProfileSmtp(settings, selectedProfileId));
await testSmtpSettings(settings, rawSmtpPayload())); } else {
setImapTestResult(await testMailProfileImap(settings, selectedProfileId));
}
} catch (err) { } catch (err) {
setSmtpTestResult({ ok: false, protocol: "smtp", message: err instanceof Error ? err.message : String(err), details: {} }); const result = { ok: false, protocol, message: err instanceof Error ? err.message : String(err), details: {} };
} finally { if (protocol === "smtp") setSmtpTestResult(result);
setMailActionState(null); else setImapTestResult(result);
}
}
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 { } finally {
setMailActionState(null); setMailActionState(null);
} }
} }
async function runFolderLookup() { async function runFolderLookup() {
if (!mailModuleInstalled) { if (!selectedProfileId || locked || !selectedProfileHasImap) return;
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"); setMailActionState("folders");
setLocalError(""); setLocalError("");
try { try {
setFolderResult(selectedProfileId && imapCredentialsInherited ? setFolderResult(await listMailProfileImapFolders(settings, selectedProfileId));
await listMailProfileImapFolders(settings, selectedProfileId) :
await listImapFolders(settings, rawImapPayload()));
} catch (err) { } catch (err) {
setFolderResult({ ok: false, protocol: "imap", message: err instanceof Error ? err.message : String(err), folders: [], details: {} }); setFolderResult({ ok: false, protocol: "imap", message: err instanceof Error ? err.message : String(err), folders: [], details: {} });
} finally { } finally {
@@ -382,8 +158,7 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
function useDetectedSentFolder() { function useDetectedSentFolder() {
const folder = folderResult?.detected_sent_folder; const folder = folderResult?.detected_sent_folder;
if (!folder || appendTargetFolderDisabled) return; if (folder && !locked) patch(["delivery", "imap_append_sent", "folder"], folder);
patch(["delivery", "imap_append_sent", "folder"], folder);
} }
return ( return (
@@ -394,8 +169,8 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
<VersionLine version={version} versions={data.versions} status={saveState} /> <VersionLine version={version} versions={data.versions} status={saveState} />
</div> </div>
<div className="button-row compact-actions"> <div className="button-row compact-actions">
<Button onClick={() => void discardDraft()} disabled={loading}>Discard</Button> {!isPolicyView && <Button variant="primary" onClick={() => void saveDraft("manual")} disabled={!canSave}>{dirty ? "i18n:govoplan-campaign.save_now.3989b7c0" : "i18n:govoplan-campaign.saved.c0ae8f6e"}</Button>}
{!isPolicyView && <Button variant="primary" onClick={() => saveDraft("manual")} disabled={!dirty || locked || !draft || inlineMailSettingsBlocked}>{dirty ? "i18n:govoplan-campaign.save_now.3989b7c0" : "i18n:govoplan-campaign.saved.c0ae8f6e"}</Button>} <Button onClick={() => void discardDraft()} disabled={loading}>i18n:govoplan-campaign.discard.36fff63c</Button>
</div> </div>
</div> </div>
@@ -405,14 +180,13 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
<LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50"> <LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
<> <>
{!mailModuleInstalled && {!mailModuleInstalled && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-campaign.install_and_enable_the_mail_module_to_select_a_d.01c75fc4</DismissibleAlert>}
<DismissibleAlert tone="info" dismissible={false}>
i18n:govoplan-campaign.the_mail_module_is_not_installed_inline_smtp_and.8e0f802e
</DismissibleAlert>
}
{isPolicyView && mailModuleInstalled && MailProfilePolicyEditor && {migrationRequired && <DismissibleAlert tone="warning" dismissible={false}>
<MailProfilePolicyEditor i18n:govoplan-campaign.this_version_contains_legacy_campaign_local_mail.44c7a6fd
</DismissibleAlert>}
{isPolicyView && mailModuleInstalled && MailProfilePolicyEditor && <MailProfilePolicyEditor
settings={settings} settings={settings}
scopeType="campaign" scopeType="campaign"
scopeId={campaignId} scopeId={campaignId}
@@ -423,144 +197,72 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
canWrite={!locked} canWrite={!locked}
locked={locked} locked={locked}
title="i18n:govoplan-campaign.campaign_local_mail_policy.94f59da0" title="i18n:govoplan-campaign.campaign_local_mail_policy.94f59da0"
description="i18n:govoplan-campaign.campaign_local_mail_limits_applied_after_system_.e7f9bb31" description="i18n:govoplan-campaign.mail_policy_limits_which_mail_owned_profiles_thi.824b1b2e"
onSaved={refreshMailProfiles} /> onSaved={refreshMailProfiles} />}
} {isPolicyView && mailModuleInstalled && !MailProfilePolicyEditor && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-campaign.the_mail_module_did_not_expose_profile_managemen.2cdb57e1</DismissibleAlert>}
{isPolicyView && mailModuleInstalled && !MailProfilePolicyEditor && {!isPolicyView && mailModuleInstalled && <Card
<DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-campaign.the_mail_module_did_not_expose_profile_managemen.2cdb57e1</DismissibleAlert>
}
{!isPolicyView && mailModuleInstalled &&
<Card
title="i18n:govoplan-campaign.reusable_mail_profile.f9c9aab1" title="i18n:govoplan-campaign.reusable_mail_profile.f9c9aab1"
actions={ actions={<Button onClick={() => void refreshMailProfiles()} disabled={profilesLoading}>{profilesLoading ? "i18n:govoplan-campaign.loading.33ce4174" : "i18n:govoplan-campaign.reload_profiles.0fe100d1"}</Button>}>
<div className="button-row compact-actions"> <p className="muted small-note">i18n:govoplan-campaign.campaign_stores_only_this_stable_profile_referen.de554809</p>
<Button onClick={() => void refreshMailProfiles()} disabled={profilesLoading}>{profilesLoading ? "i18n:govoplan-campaign.loading.33ce4174" : "i18n:govoplan-campaign.reload_profiles.0fe100d1"}</Button>
<Button variant="primary" onClick={() => void saveCurrentSettingsAsProfile()} disabled={locked || usingMailProfile || profilesLoading || !campaignProfilesAllowed}>{profilesLoading ? "i18n:govoplan-campaign.saving.56a2285c" : "i18n:govoplan-campaign.save_current_settings_as_profile.7578a50b"}</Button>
</div>
}>
<div className="form-grid compact responsive-form-grid"> <div className="form-grid compact responsive-form-grid">
<FormField label="i18n:govoplan-campaign.profile.ff4fc027"> <FormField label="i18n:govoplan-campaign.profile.ff4fc027">
<select value={selectedProfileId} disabled={locked || profilesLoading} onChange={(event) => selectMailProfile(event.target.value)}> <select value={selectedProfileId} disabled={locked || profilesLoading} onChange={(event) => selectMailProfile(event.target.value)}>
<option value="" disabled={mailPolicyState.inlineOptionDisabled}>i18n:govoplan-campaign.inline_smtp_imap_settings.cf16c421</option> <option value="">i18n:govoplan-campaign.select_a_mail_profile.76480af0</option>
{mailProfiles.map((profile) => <option key={profile.id} value={profile.id}>{profile.name} ({profileScopeLabel(profile)})</option>)} {mailProfiles.map((profile) => <option key={profile.id} value={profile.id}>{profile.name} ({profileScopeLabel(profile)})</option>)}
</select> </select>
</FormField> </FormField>
<FormField label="i18n:govoplan-campaign.new_profile_name.393313b6">
<input value={profileName} disabled={locked || usingMailProfile || profilesLoading || !campaignProfilesAllowed} onChange={(event) => 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"} />
</FormField>
</div> </div>
{!campaignProfilesAllowed && <p className="muted small-note">i18n:govoplan-campaign.campaign_local_mail_settings_are_blocked_by_the_.0b2510aa</p>} {selectedProfileUnavailable && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-campaign.the_referenced_mail_profile_is_inactive_unavaila.abeebe26</DismissibleAlert>}
{selectedProfile && {selectedProfile && <div className="metric-grid inside">
<p className="muted small-note">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"}.</p> <MetricCard label="i18n:govoplan-campaign.profile.ff4fc027" value={selectedProfile.name} />
} <MetricCard label="i18n:govoplan-campaign.scope.4651a34e" value={profileScopeLabel(selectedProfile)} />
{selectedProfileNeedsLocalCredentials && <MetricCard label="i18n:govoplan-campaign.smtp.efff9cca" value="i18n:govoplan-campaign.configured.668c5fff" tone="good" />
<p className="muted small-note">i18n:govoplan-campaign.the_selected_profile_supplies_the_server_setting.bdc830db</p> <MetricCard label="i18n:govoplan-campaign.imap.271f9ef2" value={selectedProfile.imap ? "i18n:govoplan-campaign.configured.668c5fff" : "i18n:govoplan-campaign.not_configured.811931bb"} tone={selectedProfile.imap ? "good" : "neutral"} />
} </div>}
{profileMessage && <DismissibleAlert tone="success" resetKey={profileMessage} floating>{profileMessage}</DismissibleAlert>} <div className="button-row compact-actions">
{profileError && <DismissibleAlert tone="warning" resetKey={profileError} dismissStorageKey={`campaign:${campaignId}:mail-settings:profile-error`} floating>{profileError}</DismissibleAlert>} <Button onClick={() => void runProfileTest("smtp")} disabled={!selectedProfile || locked || Boolean(mailActionState)}>{mailActionState === "smtp" ? "i18n:govoplan-campaign.testing_smtp.8e9f8247" : "i18n:govoplan-campaign.test_profile_smtp.884a0e66"}</Button>
</Card> <Button onClick={() => void runProfileTest("imap")} disabled={!selectedProfileHasImap || locked || Boolean(mailActionState)}>{mailActionState === "imap" ? "i18n:govoplan-campaign.testing_imap.13d255cf" : "i18n:govoplan-campaign.test_profile_imap.e1cec0e0"}</Button>
} </div>
{smtpTestResult && <DismissibleAlert tone={smtpTestResult.ok ? "success" : "danger"} resetKey={`${smtpTestResult.protocol}:${smtpTestResult.message}`} floating>{smtpTestResult.message}</DismissibleAlert>}
{imapTestResult && <DismissibleAlert tone={imapTestResult.ok ? "success" : "danger"} resetKey={`${imapTestResult.protocol}:${imapTestResult.message}`} floating>{imapTestResult.message}</DismissibleAlert>}
{profileError && <DismissibleAlert tone="warning" resetKey={profileError} floating>{profileError}</DismissibleAlert>}
</Card>}
{!isPolicyView && {!isPolicyView && <Card title="i18n:govoplan-campaign.imap_append.8c0d9e96" collapsible>
<Card title="i18n:govoplan-campaign.imap_append.8c0d9e96" collapsible>
<div className="form-grid compact responsive-form-grid mail-server-form-grid"> <div className="form-grid compact responsive-form-grid mail-server-form-grid">
<div className="mail-server-field-span mail-server-toggle-row mail-server-plain-toggle-row"> <div className="mail-server-field-span mail-server-toggle-row mail-server-plain-toggle-row">
<ToggleSwitch <ToggleSwitch
label="i18n:govoplan-campaign.append_successful_messages_to_sent_via_imap.dbd1b1d8" label="i18n:govoplan-campaign.append_successful_messages_to_sent_via_imap.dbd1b1d8"
checked={imapAppendEnabled} checked={imapAppendEnabled}
disabled={imapDisabled} disabled={locked || !selectedProfileHasImap}
onChange={(checked) => patch(["delivery", "imap_append_sent", "enabled"], checked)} /> onChange={(checked) => patch(["delivery", "imap_append_sent", "enabled"], checked)} />
</div> </div>
<FormField label="i18n:govoplan-core.append_target_folder.0aaacc0c" help="i18n:govoplan-core.folder_for_sent_message_copies_leave_as_auto_unl.a62586e9"> <FormField label="i18n:govoplan-core.append_target_folder.0aaacc0c" help="i18n:govoplan-core.folder_for_sent_message_copies_leave_as_auto_unl.a62586e9">
<div className="field-with-action mail-server-folder-field"> <div className="field-with-action mail-server-folder-field">
<input <input
value={imapAppendFolder} value={getText(imapAppend, "folder", "auto")}
disabled={appendTargetFolderDisabled} disabled={locked || !imapAppendEnabled || !selectedProfileHasImap}
onChange={(event) => patch(["delivery", "imap_append_sent", "folder"], event.target.value)} onChange={(event) => patch(["delivery", "imap_append_sent", "folder"], event.target.value)} />
placeholder="i18n:govoplan-core.auto.0d612c12" /> <Button type="button" variant="primary" onClick={() => void runFolderLookup()} disabled={locked || !imapAppendEnabled || !selectedProfileHasImap || Boolean(mailActionState)}>
<Button type="button" variant="primary" onClick={() => void runFolderLookup()} disabled={appendTargetFolderDisabled || mailActionState === "folders"}>
{mailActionState === "folders" ? "i18n:govoplan-core.looking_up.5fc6d2a2" : "i18n:govoplan-core.folders.c603ab65"} {mailActionState === "folders" ? "i18n:govoplan-core.looking_up.5fc6d2a2" : "i18n:govoplan-core.folders.c603ab65"}
</Button> </Button>
</div> </div>
</FormField> </FormField>
<MailServerFolderLookupResultView result={folderResult} disabled={appendTargetFolderDisabled} onUseDetected={useDetectedSentFolder} floatingFailures /> <MailServerFolderLookupResultView result={folderResult} disabled={locked || !imapAppendEnabled} onUseDetected={useDetectedSentFolder} floatingFailures />
</div> </div>
</Card> </Card>}
}
{!isPolicyView &&
<Card title="i18n:govoplan-campaign.mail_server_settings.6db620b0" collapsible>
{inlinePolicyMessages.length > 0 &&
<DismissibleAlert tone="warning" resetKey={inlinePolicyMessages.map((item) => `${item.key}:${item.value}`).join("|")} dismissible={false}>
<strong>i18n:govoplan-campaign.effective_mail_policy_blocks_the_current_inline_.99c34c6b</strong>
<ul>{inlinePolicyMessages.map((item) => <li key={`${item.key}:${item.value}`}>{item.message}</li>)}</ul>
</DismissibleAlert>
}
<MailServerSettingsPanel
smtp={displayedSmtp}
imap={displayedImap}
onSmtpChange={patchSmtpSettings}
onImapChange={patchImapSettings}
smtpCredentials={{ username: displayedSmtp.username, password: displayedSmtp.password }}
imapCredentials={{ username: displayedImap.username, password: displayedImap.password }}
onSmtpCredentialsChange={patchSmtpCredentials}
onImapCredentialsChange={patchImapCredentials}
smtpDisabled={smtpDisabled}
smtpCredentialDisabled={smtpCredentialDisabled}
smtpPasswordSaved={Boolean(usingMailProfile && smtpCredentialsInherited && selectedProfile?.smtp_password_configured)}
smtpActionDisabled={locked || inlineMailSettingsBlocked || !mailModuleInstalled}
imapServerDisabled={imapServerDisabled}
imapCredentialDisabled={imapCredentialDisabled}
imapPasswordSaved={Boolean(usingMailProfile && imapCredentialsInherited && selectedProfile?.imap_password_configured)}
imapActionDisabled={imapDisabled || !mailModuleInstalled}
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}
smtpTestResult={smtpTestResult}
imapTestResult={imapTestResult}
floatingResults />
</Card>
}
</> </>
</LoadingFrame> </LoadingFrame>
</div>); </div>
);
} }
function profileScopeLabel(profile: MailServerProfile): string {
function stringOrEmpty(value: unknown): string { if (profile.scope_type === "system") return "i18n:govoplan-campaign.system.bc0792d8";
return value === null || value === undefined ? "" : String(value); 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";
function firstAddressEmail(value: unknown): string { return "i18n:govoplan-campaign.campaign_scoped_mail_profile.9cbf3505";
return addressesFromValue(value)[0]?.email ?? "";
}
function collectRecipientDomains(draft: Record<string, unknown>): string[] {
const domains = new Set<string>();
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<string>, value: unknown) {
for (const address of addressesFromValue(value)) {
const domain = address.email.split("@").pop()?.trim().toLowerCase();
if (domain) domains.add(domain);
}
} }

View File

@@ -136,12 +136,7 @@ export default function ReviewSendPage({ settings, campaignId }: {settings: ApiS
const version = data.currentVersion; const version = data.currentVersion;
const campaignJson = useMemo(() => getCampaignJson(version), [version]); const campaignJson = useMemo(() => getCampaignJson(version), [version]);
const server = asRecord(campaignJson.server); const server = asRecord(campaignJson.server);
const smtpServer = asRecord(server.smtp); const selectedMailProfileId = getText(server, "mail_profile_id");
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 inlineEntries = useMemo( const inlineEntries = useMemo(
() => asArray(asRecord(campaignJson.entries).inline).map(asRecord), () => asArray(asRecord(campaignJson.entries).inline).map(asRecord),
[campaignJson] [campaignJson]
@@ -398,7 +393,9 @@ export default function ReviewSendPage({ settings, campaignId }: {settings: ApiS
const imapAppendResultRows = asArray(imapAppendResult?.results).map(asRecord); const imapAppendResultRows = asArray(imapAppendResult?.results).map(asRecord);
const imapDiagnosticRows = imapDiagnostics.jobs.map(asRecord); const imapDiagnosticRows = imapDiagnostics.jobs.map(asRecord);
const imapDiagnosticsPending = imapDiagnosticRows.filter((job) => String(job.imap_status ?? "").toLowerCase() === "pending").length; 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 imapPendingForDisplay = Math.max(imapPending, imapDiagnosticsPending);
const imapFailedForDisplay = Math.max(imapFailed, imapDiagnosticsFailed); const imapFailedForDisplay = Math.max(imapFailed, imapDiagnosticsFailed);
const canAppendPendingImap = Boolean(imapAppend.enabled) && imapPendingForDisplay > 0 && !historicalVersion && !userLockedVersion; 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 ambiguousAttachments = numberFrom(attachmentSummary, ["ambiguous_configs"]);
const messagesPerMinute = numberFrom(rateLimit, ["messages_per_minute"]); const messagesPerMinute = numberFrom(rateLimit, ["messages_per_minute"]);
const estimatedMinutes = messagesPerMinute > 0 && jobsTotal > 0 ? Math.ceil(jobsTotal / messagesPerMinute) : null; const estimatedMinutes = messagesPerMinute > 0 && jobsTotal > 0 ? Math.ceil(jobsTotal / messagesPerMinute) : null;
const smtpConfigured = Boolean(selectedMailProfileId || getText(smtpServer, "host") || getText(smtpCredentials, "username")); const mailProfileSelected = Boolean(selectedMailProfileId);
const imapConfigured = Boolean(selectedMailProfileId || getText(imapServer, "host") || getText(imapCredentials, "username"));
const deliverabilityPreflightItems: DeliverabilityPreflightItem[] = [ const deliverabilityPreflightItems: DeliverabilityPreflightItem[] = [
{ {
label: "Transport", 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.", detail: mailProfileSelected ? "i18n:govoplan-campaign.mail_owned_delivery_profile_selected.4f44778e" : "i18n:govoplan-campaign.select_an_authorized_mail_profile_before_live_de.45c80a42",
state: smtpConfigured ? "ready" : "blocked" state: mailProfileSelected ? "ready" : "blocked"
}, },
{ {
label: "Policy", label: "Policy",
@@ -1062,8 +1058,8 @@ export default function ReviewSendPage({ settings, campaignId }: {settings: ApiS
}, },
{ {
label: "Sent copy", 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.", 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) ? imapConfigured ? "ready" : "blocked" : "info" state: Boolean(imapAppend.enabled) ? mailProfileSelected ? "ready" : "blocked" : "info"
}]; }];
const canCompleteInspection = blockingReviewCount === 0 && const canCompleteInspection = blockingReviewCount === 0 &&
reviewRequiredCount > 0 && reviewRequiredCount > 0 &&

View File

@@ -43,6 +43,9 @@ export default function LockedVersionNotice({ settings, campaignId, version, cur
const permanentUserLock = isPermanentUserLockedVersion(version); const permanentUserLock = isPermanentUserLockedVersion(version);
const finalLock = isFinalLockedVersion(version); const finalLock = isFinalLockedVersion(version);
const canCreateEditableCopy = !historicalVersion && (permanentUserLock || finalLock); const canCreateEditableCopy = !historicalVersion && (permanentUserLock || finalLock);
const mailProfileMigrationRequired = Boolean(
version && "mail_profile_migration_required" in version && version.mail_profile_migration_required
);
const presentation = lockPresentation(version, { const presentation = lockPresentation(version, {
historicalVersion, historicalVersion,
validationLock, validationLock,
@@ -83,7 +86,8 @@ export default function LockedVersionNotice({ settings, campaignId, version, cur
try { try {
const result = await forkCampaignVersion(settings, campaignId, version.id, { const result = await forkCampaignVersion(settings, campaignId, version.id, {
current_flow: "manual", 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}.`); setLocalMessage(`Created editable version #${result.version.version_number}.`);
await reload(); await reload();
@@ -100,6 +104,7 @@ export default function LockedVersionNotice({ settings, campaignId, version, cur
<strong>{presentation.title}</strong>{" "} <strong>{presentation.title}</strong>{" "}
<span>{presentation.description}</span> <span>{presentation.description}</span>
{message && <span className="locked-version-context"> {message}</span>} {message && <span className="locked-version-context"> {message}</span>}
{mailProfileMigrationRequired && <span className="locked-version-context"> i18n:govoplan-campaign.the_editable_copy_will_preserve_this_audit_recor.aac956f1</span>}
{presentation.info && <span className="locked-version-reason"> {presentation.info}</span>} {presentation.info && <span className="locked-version-reason"> {presentation.info}</span>}
{localMessage && <span className="locked-version-feedback"> {localMessage}</span>} {localMessage && <span className="locked-version-feedback"> {localMessage}</span>}
{localError && <span className="locked-version-error"> {localError}</span>} {localError && <span className="locked-version-error"> {localError}</span>}

View File

@@ -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
};
}

View File

@@ -0,0 +1,13 @@
export function campaignMailProfileReferenceOnly(value: Record<string, unknown>): Record<string, unknown> {
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<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

View File

@@ -49,8 +49,6 @@ export function SenderStep({ draft, patch }: WizardStepProps) {
const globalCc = addressesFromValue(recipients.cc); const globalCc = addressesFromValue(recipients.cc);
const globalBcc = addressesFromValue(recipients.bcc); const globalBcc = addressesFromValue(recipients.bcc);
const globalReplyTo = addressesFromValue(recipients.reply_to); const globalReplyTo = addressesFromValue(recipients.reply_to);
const server = asRecord(draft.server);
const smtp = asRecord(server.smtp);
const delivery = asRecord(draft.delivery); const delivery = asRecord(draft.delivery);
const imapAppend = asRecord(delivery.imap_append_sent); const imapAppend = asRecord(delivery.imap_append_sent);
return ( return (
@@ -106,8 +104,7 @@ export function SenderStep({ draft, patch }: WizardStepProps) {
onChange={(addresses: MailboxAddress[]) => patch(["recipients", "reply_to"], addresses.slice(0, 1))} /> onChange={(addresses: MailboxAddress[]) => patch(["recipients", "reply_to"], addresses.slice(0, 1))} />
</FormField> </FormField>
<FormField label="i18n:govoplan-campaign.smtp_host.2d4a434b"><input value={getText(smtp, "host")} onChange={(event) => patch(["server", "smtp", "host"], event.target.value)} /></FormField> <p className="muted small-note">i18n:govoplan-campaign.select_the_delivery_profile_on_the_mail_settings.a89cbb5f</p>
<FormField label="i18n:govoplan-campaign.smtp_port.65b5a108"><input type="number" value={getNumber(smtp, "port", 587)} onChange={(event) => patch(["server", "smtp", "port"], Number(event.target.value || 0))} /></FormField>
<ToggleSwitch label="i18n:govoplan-campaign.append_successful_messages_to_sent_via_imap.dbd1b1d8" checked={getBool(imapAppend, "enabled")} onChange={(checked) => patch(["delivery", "imap_append_sent", "enabled"], checked)} /> <ToggleSwitch label="i18n:govoplan-campaign.append_successful_messages_to_sent_via_imap.dbd1b1d8" checked={getBool(imapAppend, "enabled")} onChange={(checked) => patch(["delivery", "imap_append_sent", "enabled"], checked)} />
</div>); </div>);

View File

@@ -4,6 +4,26 @@ export const generatedTranslations: PlatformTranslations = {
"en": { "en": {
"i18n:govoplan-campaign.1_campaign.ccd70074": "1 campaign", "i18n:govoplan-campaign.1_campaign.ccd70074": "1 campaign",
"i18n:govoplan-campaign.2_campaigns.35b84804": "2 campaigns", "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_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_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", "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": { "de": {
"i18n:govoplan-campaign.1_campaign.ccd70074": "1 campaign", "i18n:govoplan-campaign.1_campaign.ccd70074": "1 campaign",
"i18n:govoplan-campaign.2_campaigns.35b84804": "2 campaigns", "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_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_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", "i18n:govoplan-campaign.a_mapping_wizard_can_be_added_later.6b2fe0f3": "A mapping wizard can be added later",

View File

@@ -1,7 +1,7 @@
export { default } from "./module"; export { default } from "./module";
export * from "./module"; export * from "./module";
export * from "./api/campaigns"; 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 CampaignListPage } from "./features/campaigns/CampaignListPage";
export { default as CampaignWorkspace } from "./features/campaigns/CampaignWorkspace"; export { default as CampaignWorkspace } from "./features/campaigns/CampaignWorkspace";
export { default as OperatorQueuePage } from "./features/operator/OperatorQueuePage"; export { default as OperatorQueuePage } from "./features/operator/OperatorQueuePage";

View File

@@ -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 { function assert(condition: unknown, message: string): void {
if (!condition) throw new Error(message); if (!condition) throw new Error(message);
} }
const blockedInline = campaignMailSettingsPolicyState({ const source = {
effectivePolicy: { allow_campaign_profiles: false }, campaign: { id: "campaign-1" },
selectedProfileId: "" 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."); const normalized = campaignMailProfileReferenceOnly(source);
assert(blockedInline.inlineMailSettingsBlocked === true, "Inline SMTP/IMAP must be blocked when no reusable profile is selected."); const server = normalized.server as Record<string, unknown>;
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 blockedWithReusableProfile = campaignMailSettingsPolicyState({ assert(server.mail_profile_id === "profile-1", "The stable Mail profile reference must be retained and trimmed.");
effectivePolicy: { allow_campaign_profiles: false }, assert(Object.keys(server).length === 1, "All campaign-local transport settings and credentials must be removed.");
selectedProfileId: "tenant-profile" assert((source.server.smtp as Record<string, unknown>).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."); const withoutProfile = campaignMailProfileReferenceOnly({ server: { smtp: { host: "legacy" } } });
assert(blockedWithReusableProfile.inlineOptionDisabled === true, "Inline option remains disabled even while a reusable profile is selected."); assert(Object.keys(withoutProfile.server as Record<string, unknown>).length === 0, "Legacy settings without a profile must normalize to an empty reference object.");
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.");

View File

@@ -15,6 +15,6 @@
}, },
"include": [ "include": [
"tests/policy-ui.test.ts", "tests/policy-ui.test.ts",
"src/features/campaigns/policyUi.ts" "src/features/campaigns/utils/mailProfileReference.ts"
] ]
} }