refactor(webui): select Mail-owned profiles for campaigns
This commit is contained in:
@@ -81,6 +81,7 @@ export type CampaignVersionListItem = {
|
||||
export type CampaignVersionDetail = CampaignVersionListItem & {
|
||||
raw_json: Record<string, unknown>;
|
||||
campaign_json?: Record<string, unknown>;
|
||||
mail_profile_migration_required?: boolean;
|
||||
};
|
||||
|
||||
export type CampaignWorkspaceResponse = {
|
||||
@@ -209,6 +210,7 @@ export type CampaignVersionUpdatePayload = {
|
||||
editor_state?: Record<string, unknown> | 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<Record<string, unknown>> {
|
||||
return apiFetch<Record<string, unknown>>(settings, `/api/v1/campaigns/${campaignId}/jobs/${jobId}/resolve-outcome`, {
|
||||
|
||||
@@ -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<TResponse>(
|
||||
settings: ApiSettings,
|
||||
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[]> {
|
||||
return apiGetList<MailServerProfile, "profiles">(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<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> {
|
||||
return runProfileAction<MailConnectionTestResponse>(settings, profileId, "smtp");
|
||||
}
|
||||
@@ -83,22 +43,8 @@ export async function listMailProfileImapFolders(settings: ApiSettings, profileI
|
||||
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> {
|
||||
return apiFetch<MockMailboxMessageResponse>(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";
|
||||
|
||||
@@ -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<MailProfilesUiCapability>("mail.profiles");
|
||||
const MailProfilePolicyEditor = mailProfilesUi?.MailProfilePolicyEditor ?? null;
|
||||
const isPolicyView = view === "policy";
|
||||
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 [imapTestResult, setImapTestResult] = useState<MailServerConnectionTestResult | 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 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<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) {
|
||||
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<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");
|
||||
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
|
||||
<VersionLine version={version} versions={data.versions} status={saveState} />
|
||||
</div>
|
||||
<div className="button-row compact-actions">
|
||||
<Button onClick={() => void discardDraft()} disabled={loading}>Discard</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>}
|
||||
{!isPolicyView && <Button variant="primary" onClick={() => void saveDraft("manual")} disabled={!canSave}>{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>
|
||||
|
||||
@@ -405,14 +180,13 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
|
||||
|
||||
<LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
|
||||
<>
|
||||
{!mailModuleInstalled &&
|
||||
<DismissibleAlert tone="info" dismissible={false}>
|
||||
i18n:govoplan-campaign.the_mail_module_is_not_installed_inline_smtp_and.8e0f802e
|
||||
</DismissibleAlert>
|
||||
}
|
||||
{!mailModuleInstalled && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-campaign.install_and_enable_the_mail_module_to_select_a_d.01c75fc4</DismissibleAlert>}
|
||||
|
||||
{isPolicyView && mailModuleInstalled && MailProfilePolicyEditor &&
|
||||
<MailProfilePolicyEditor
|
||||
{migrationRequired && <DismissibleAlert tone="warning" dismissible={false}>
|
||||
i18n:govoplan-campaign.this_version_contains_legacy_campaign_local_mail.44c7a6fd
|
||||
</DismissibleAlert>}
|
||||
|
||||
{isPolicyView && mailModuleInstalled && MailProfilePolicyEditor && <MailProfilePolicyEditor
|
||||
settings={settings}
|
||||
scopeType="campaign"
|
||||
scopeId={campaignId}
|
||||
@@ -423,144 +197,72 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
|
||||
canWrite={!locked}
|
||||
locked={locked}
|
||||
title="i18n:govoplan-campaign.campaign_local_mail_policy.94f59da0"
|
||||
description="i18n:govoplan-campaign.campaign_local_mail_limits_applied_after_system_.e7f9bb31"
|
||||
onSaved={refreshMailProfiles} />
|
||||
description="i18n:govoplan-campaign.mail_policy_limits_which_mail_owned_profiles_thi.824b1b2e"
|
||||
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 &&
|
||||
<DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-campaign.the_mail_module_did_not_expose_profile_managemen.2cdb57e1</DismissibleAlert>
|
||||
}
|
||||
|
||||
{!isPolicyView && mailModuleInstalled &&
|
||||
<Card
|
||||
{!isPolicyView && mailModuleInstalled && <Card
|
||||
title="i18n:govoplan-campaign.reusable_mail_profile.f9c9aab1"
|
||||
actions={
|
||||
<div className="button-row compact-actions">
|
||||
<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>
|
||||
}>
|
||||
|
||||
actions={<Button onClick={() => void refreshMailProfiles()} disabled={profilesLoading}>{profilesLoading ? "i18n:govoplan-campaign.loading.33ce4174" : "i18n:govoplan-campaign.reload_profiles.0fe100d1"}</Button>}>
|
||||
<p className="muted small-note">i18n:govoplan-campaign.campaign_stores_only_this_stable_profile_referen.de554809</p>
|
||||
<div className="form-grid compact responsive-form-grid">
|
||||
<FormField label="i18n:govoplan-campaign.profile.ff4fc027">
|
||||
<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>)}
|
||||
</select>
|
||||
</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>
|
||||
{!campaignProfilesAllowed && <p className="muted small-note">i18n:govoplan-campaign.campaign_local_mail_settings_are_blocked_by_the_.0b2510aa</p>}
|
||||
{selectedProfile &&
|
||||
<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>
|
||||
}
|
||||
{selectedProfileNeedsLocalCredentials &&
|
||||
<p className="muted small-note">i18n:govoplan-campaign.the_selected_profile_supplies_the_server_setting.bdc830db</p>
|
||||
}
|
||||
{profileMessage && <DismissibleAlert tone="success" resetKey={profileMessage} floating>{profileMessage}</DismissibleAlert>}
|
||||
{profileError && <DismissibleAlert tone="warning" resetKey={profileError} dismissStorageKey={`campaign:${campaignId}:mail-settings:profile-error`} floating>{profileError}</DismissibleAlert>}
|
||||
</Card>
|
||||
}
|
||||
{selectedProfileUnavailable && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-campaign.the_referenced_mail_profile_is_inactive_unavaila.abeebe26</DismissibleAlert>}
|
||||
{selectedProfile && <div className="metric-grid inside">
|
||||
<MetricCard label="i18n:govoplan-campaign.profile.ff4fc027" value={selectedProfile.name} />
|
||||
<MetricCard label="i18n:govoplan-campaign.scope.4651a34e" value={profileScopeLabel(selectedProfile)} />
|
||||
<MetricCard label="i18n:govoplan-campaign.smtp.efff9cca" value="i18n:govoplan-campaign.configured.668c5fff" tone="good" />
|
||||
<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>}
|
||||
<div className="button-row compact-actions">
|
||||
<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>
|
||||
<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 &&
|
||||
<Card title="i18n:govoplan-campaign.imap_append.8c0d9e96" collapsible>
|
||||
{!isPolicyView && <Card title="i18n:govoplan-campaign.imap_append.8c0d9e96" collapsible>
|
||||
<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">
|
||||
<ToggleSwitch
|
||||
label="i18n:govoplan-campaign.append_successful_messages_to_sent_via_imap.dbd1b1d8"
|
||||
checked={imapAppendEnabled}
|
||||
disabled={imapDisabled}
|
||||
disabled={locked || !selectedProfileHasImap}
|
||||
onChange={(checked) => patch(["delivery", "imap_append_sent", "enabled"], checked)} />
|
||||
</div>
|
||||
<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">
|
||||
<input
|
||||
value={imapAppendFolder}
|
||||
disabled={appendTargetFolderDisabled}
|
||||
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={appendTargetFolderDisabled || mailActionState === "folders"}>
|
||||
value={getText(imapAppend, "folder", "auto")}
|
||||
disabled={locked || !imapAppendEnabled || !selectedProfileHasImap}
|
||||
onChange={(event) => patch(["delivery", "imap_append_sent", "folder"], event.target.value)} />
|
||||
<Button type="button" variant="primary" onClick={() => void runFolderLookup()} disabled={locked || !imapAppendEnabled || !selectedProfileHasImap || Boolean(mailActionState)}>
|
||||
{mailActionState === "folders" ? "i18n:govoplan-core.looking_up.5fc6d2a2" : "i18n:govoplan-core.folders.c603ab65"}
|
||||
</Button>
|
||||
</div>
|
||||
</FormField>
|
||||
<MailServerFolderLookupResultView result={folderResult} disabled={appendTargetFolderDisabled} onUseDetected={useDetectedSentFolder} floatingFailures />
|
||||
<MailServerFolderLookupResultView result={folderResult} disabled={locked || !imapAppendEnabled} onUseDetected={useDetectedSentFolder} floatingFailures />
|
||||
</div>
|
||||
</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>
|
||||
}
|
||||
|
||||
</Card>}
|
||||
</>
|
||||
</LoadingFrame>
|
||||
</div>);
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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, 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);
|
||||
}
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
<strong>{presentation.title}</strong>{" "}
|
||||
<span>{presentation.description}</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>}
|
||||
{localMessage && <span className="locked-version-feedback"> {localMessage}</span>}
|
||||
{localError && <span className="locked-version-error"> {localError}</span>}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
13
webui/src/features/campaigns/utils/mailProfileReference.ts
Normal file
13
webui/src/features/campaigns/utils/mailProfileReference.ts
Normal 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);
|
||||
}
|
||||
@@ -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))} />
|
||||
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-campaign.smtp_host.2d4a434b"><input value={getText(smtp, "host")} onChange={(event) => patch(["server", "smtp", "host"], event.target.value)} /></FormField>
|
||||
<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>
|
||||
<p className="muted small-note">i18n:govoplan-campaign.select_the_delivery_profile_on_the_mail_settings.a89cbb5f</p>
|
||||
<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>);
|
||||
|
||||
@@ -232,4 +229,4 @@ function JsonEditor({ value, onValid }: {value: unknown;onValid: (value: unknown
|
||||
{Array.isArray(value) && value.length > 0 && <p className="form-help">i18n:govoplan-campaign.preview.4bf30626 {stringifyPreview(asArray(value)[0], 140)}</p>}
|
||||
</div>);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user