import { useEffect, useRef, useState } from "react"; import Button from "../Button"; import { CredentialFields } from "../CredentialPanel"; import DismissibleAlert from "../DismissibleAlert"; import FormField from "../FormField"; import SegmentedControl from "../SegmentedControl"; export type MailServerSecurity = "plain" | "tls" | "starttls" | string; export type MailServerCredentialSettings = { username?: string | null; password?: string | null; }; export type MailServerSmtpSettings = { host?: string | null; port?: string | number | null; security?: MailServerSecurity | null; timeout_seconds?: string | number | null; username?: string | null; password?: string | null; }; export type MailServerImapSettings = MailServerSmtpSettings & { sent_folder?: string | null; }; export type MailServerConnectionTestResult = { ok: boolean; protocol?: "smtp" | "imap"; host?: string | null; port?: number | null; security?: MailServerSecurity | null; message: string; details?: Record | null; }; export type MailServerFolderLookupResult = { ok: boolean; protocol?: "imap"; host?: string | null; port?: number | null; security?: MailServerSecurity | null; message: string; detected_sent_folder?: string | null; folders?: {name: string;flags?: string[];}[]; details?: Record | null; }; export type MailServerSettingsSection = "smtp" | "imap"; export type MailServerSettingsMode = "all" | "server" | "credentials"; export type MailServerSettingsPanelProps = { smtp: MailServerSmtpSettings; imap: MailServerImapSettings; onSmtpChange: (patch: Partial) => void; onImapChange: (patch: Partial) => void; smtpCredentials?: MailServerCredentialSettings; imapCredentials?: MailServerCredentialSettings; onSmtpCredentialsChange?: (patch: Partial) => void; onImapCredentialsChange?: (patch: Partial) => void; smtpDisabled?: boolean; smtpCredentialDisabled?: boolean; smtpPasswordSaved?: boolean; smtpSavedPasswordPlaceholder?: string; smtpActionDisabled?: boolean; imapServerDisabled?: boolean; imapCredentialDisabled?: boolean; imapPasswordSaved?: boolean; imapSavedPasswordPlaceholder?: string; imapActionDisabled?: boolean; smtpTestLabel?: string; imapTestLabel?: string; busyAction?: "smtp" | "imap" | "folders" | string | null; onTestSmtp?: () => void; onTestImap?: () => void; smtpTestResult?: MailServerConnectionTestResult | null; imapTestResult?: MailServerConnectionTestResult | null; disabled?: boolean; className?: string; floatingResults?: boolean; initialSection?: MailServerSettingsSection; visibleSections?: readonly MailServerSettingsSection[]; mode?: MailServerSettingsMode; }; export const mailServerSecurityOptions = ["plain", "tls", "starttls"] as const; export type MailServerSecurityOption = typeof mailServerSecurityOptions[number]; const securityOptions = mailServerSecurityOptions; export function defaultSmtpPort(security: MailServerSecurity | null | undefined): number { if (security === "tls") return 465; if (security === "plain") return 25; return 587; } export function defaultImapPort(security: MailServerSecurity | null | undefined): number { return security === "tls" ? 993 : 143; } export function mailTextOrNull(value: string | number | null | undefined, trim = true): string | null { const text = value === null || value === undefined ? "" : String(value); const normalized = trim ? text.trim() : text; return normalized ? normalized : null; } export function mailNumberOrNull(value: string | number | null | undefined): number | null { if (typeof value === "number") return Number.isFinite(value) ? value : null; const text = String(value ?? "").trim(); if (!text) return null; const parsed = Number(text); return Number.isFinite(parsed) ? parsed : null; } export function mailNumberOrDefault(value: string | number | null | undefined, fallback: number): number { return mailNumberOrNull(value) ?? fallback; } export function normalizeMailServerSecurity( value: string | null | undefined, options: {fallback: TSecurity;allowedSecurity?: readonly TSecurity[];}) : TSecurity { const allowed = options.allowedSecurity ?? mailServerSecurityOptions as unknown as readonly TSecurity[]; return allowed.includes(value as TSecurity) ? value as TSecurity : options.fallback; } export function mailTransportCredentialsPayload( username: string | number | null | undefined, password: string | number | null | undefined, preserveBlankPassword: boolean) : {username?: string | null;password?: string | null;} { const payload: {username?: string | null;password?: string | null;} = { username: mailTextOrNull(username) }; if (String(password ?? "") || !preserveBlankPassword) payload.password = mailTextOrNull(password, false); return payload; } function mailRecordText(record: Record, key: string, fallback = ""): string { const value = record[key]; if (value === null || value === undefined) return fallback; return String(value); } export function mailTransportCredentialsPayloadFromRecords( primary: Record, legacy: Record, preserveBlankPassword: boolean) : {username?: string | null;password?: string | null;} { return mailTransportCredentialsPayload( mailRecordText(primary, "username", mailRecordText(legacy, "username")), mailRecordText(primary, "password", mailRecordText(legacy, "password")), preserveBlankPassword ); } export function mailSmtpSettingsPayload( settings: MailServerSmtpSettings, options: {fallbackSecurity: TSecurity;allowedSecurity?: readonly TSecurity[];fallbackTimeoutSeconds?: number;}) : {host: string | null;port: number | null;security: TSecurity;timeout_seconds: number;} { return { host: mailTextOrNull(settings.host), port: mailNumberOrNull(settings.port), security: normalizeMailServerSecurity(settings.security ? String(settings.security) : null, { fallback: options.fallbackSecurity, allowedSecurity: options.allowedSecurity }), timeout_seconds: mailNumberOrDefault(settings.timeout_seconds, options.fallbackTimeoutSeconds ?? 30) }; } export function mailImapSettingsPayload( settings: MailServerImapSettings, options: {fallbackSecurity: TSecurity;allowedSecurity?: readonly TSecurity[];fallbackTimeoutSeconds?: number;}) : {host: string | null;port: number | null;security: TSecurity;sent_folder: string;timeout_seconds: number;} { return { host: mailTextOrNull(settings.host), port: mailNumberOrNull(settings.port), security: normalizeMailServerSecurity(settings.security ? String(settings.security) : null, { fallback: options.fallbackSecurity, allowedSecurity: options.allowedSecurity }), sent_folder: mailTextOrNull(settings.sent_folder) || "auto", timeout_seconds: mailNumberOrDefault(settings.timeout_seconds, options.fallbackTimeoutSeconds ?? 30) }; } export function hasMailImapSettings(values: Array): boolean { return values.some((value) => String(value ?? "").trim() !== ""); } export function resolveMailServerSettingsActiveSection( activeSection: MailServerSettingsSection, initialSection: MailServerSettingsSection, previousInitialSection: MailServerSettingsSection, visibleSectionIds: readonly MailServerSettingsSection[]) : MailServerSettingsSection { const fallbackSection = visibleSectionIds[0] ?? "smtp"; const initialSectionVisible = visibleSectionIds.includes(initialSection); if (previousInitialSection !== initialSection && initialSectionVisible) return initialSection; if (!visibleSectionIds.includes(activeSection)) return initialSectionVisible ? initialSection : fallbackSection; return activeSection; } export default function MailServerSettingsPanel({ smtp, imap, onSmtpChange, onImapChange, smtpCredentials, imapCredentials, onSmtpCredentialsChange, onImapCredentialsChange, smtpDisabled = false, smtpCredentialDisabled = smtpDisabled, smtpPasswordSaved = false, smtpSavedPasswordPlaceholder = "••••••••", smtpActionDisabled = smtpDisabled, imapServerDisabled = false, imapCredentialDisabled = imapServerDisabled, imapPasswordSaved = false, imapSavedPasswordPlaceholder = "••••••••", imapActionDisabled = imapServerDisabled, smtpTestLabel = "i18n:govoplan-core.test_smtp.e5697981", imapTestLabel = "i18n:govoplan-core.test_imap.ef1bd79c", busyAction = null, onTestSmtp, onTestImap, smtpTestResult = null, imapTestResult = null, disabled = false, className = "", floatingResults = false, initialSection = "smtp", visibleSections, mode = "all" }: MailServerSettingsPanelProps) { const smtpFieldsDisabled = disabled || smtpDisabled; const smtpCredentialFieldsDisabled = disabled || smtpCredentialDisabled; const smtpActionsDisabled = disabled || smtpActionDisabled; const imapFieldsDisabled = disabled || imapServerDisabled; const imapCredentialFieldsDisabled = disabled || imapCredentialDisabled; const imapActionsDisabled = disabled || imapActionDisabled; const smtpCredentialValues = smtpCredentials ?? { username: smtp.username, password: smtp.password }; const imapCredentialValues = imapCredentials ?? { username: imap.username, password: imap.password }; const smtpSecurity = stringValue(smtp.security, "starttls"); const imapSecurity = stringValue(imap.security, "tls"); const smtpPort = stringValue(smtp.port, String(defaultSmtpPort(smtpSecurity))); const imapPort = stringValue(imap.port, String(defaultImapPort(imapSecurity))); const allSections: {id: MailServerSettingsSection;label: string;}[] = [ { id: "smtp", label: "i18n:govoplan-core.smtp.efff9cca" }, { id: "imap", label: "i18n:govoplan-core.imap.271f9ef2" }]; const visibleSectionSet = new Set(visibleSections ?? allSections.map((section) => section.id)); const sections = allSections.filter((section) => visibleSectionSet.has(section.id)); const visibleSectionIds = sections.map((section) => section.id); const fallbackSection = visibleSectionIds[0] ?? "smtp"; const resolvedInitialSection = visibleSectionIds.includes(initialSection) ? initialSection : fallbackSection; const sectionKey = visibleSectionIds.join("|"); const [activeSection, setActiveSection] = useState(resolvedInitialSection); const previousInitialSectionRef = useRef(initialSection); const showSectionSwitcher = sections.length > 1; const showServerFields = mode !== "credentials"; const showCredentialFields = mode !== "server"; useEffect(() => { const nextSection = resolveMailServerSettingsActiveSection( activeSection, initialSection, previousInitialSectionRef.current, visibleSectionIds ); previousInitialSectionRef.current = initialSection; if (nextSection !== activeSection) setActiveSection(nextSection); }, [activeSection, initialSection, sectionKey]); function patchSmtpSecurity(security: MailServerSecurity) { const port = portForSecurityChange("smtp", smtp.port, smtpSecurity, security); onSmtpChange(port === undefined ? { security } : { security, port }); } function patchImapSecurity(security: MailServerSecurity) { const port = portForSecurityChange("imap", imap.port, imapSecurity, security); onImapChange(port === undefined ? { security } : { security, port }); } function patchSmtpCredentials(patch: Partial) { if (onSmtpCredentialsChange) onSmtpCredentialsChange(patch);else onSmtpChange(patch); } function patchImapCredentials(patch: Partial) { if (onImapCredentialsChange) onImapCredentialsChange(patch);else onImapChange(patch); } return (
{showSectionSwitcher && }
{activeSection === "smtp" &&
{showServerFields && <> onSmtpChange({ host: event.target.value })} /> onSmtpChange({ port: event.target.value })} /> onSmtpChange({ timeout_seconds: event.target.value })} /> } {showCredentialFields && }
{onTestSmtp &&
}
} {activeSection === "imap" &&
{showServerFields && <> onImapChange({ host: event.target.value })} /> onImapChange({ port: event.target.value })} /> onImapChange({ timeout_seconds: event.target.value })} /> } {showCredentialFields && }
{onTestImap &&
}
}
); } export function MailServerActionResult({ result, floating = false }: {result: MailServerConnectionTestResult | null | undefined;floating?: boolean;}) { if (!result) return null; const authenticated = result.details?.authenticated; return ( {result.message} {result.ok && typeof authenticated === "boolean" && i18n:govoplan-core.authentication.e665f157 {authenticated ? "i18n:govoplan-core.credentials_accepted.ae8e2629" : "i18n:govoplan-core.not_used.487f8bcd"}. } ); } export function MailServerFolderLookupResultView({ result, disabled = false, onUseDetected, compact = true, floatingFailures = false }: {result: MailServerFolderLookupResult | null | undefined;disabled?: boolean;onUseDetected?: () => void;compact?: boolean;floatingFailures?: boolean;}) { if (!result) return null; if (!result.ok) { return {result.message}; } const folders = result.folders ?? []; return (

{result.message}

i18n:govoplan-core.detected_sent_folder.cbf8ec8d {result.detected_sent_folder || "-"}

{result.detected_sent_folder && onUseDetected && } {folders.length > 0 &&
{folders.slice(0, 12).map((folder) => {folder.name} )} {folders.length > 12 && +{folders.length - 12} more}
}
); } function SecuritySelect({ value, disabled, onChange }: {value: string;disabled: boolean;onChange: (value: MailServerSecurity) => void;}) { return ; } function portForSecurityChange(protocol: "smtp" | "imap", currentPort: string | number | null | undefined, currentSecurity: MailServerSecurity, nextSecurity: MailServerSecurity): number | undefined { const current = numberValue(currentPort); const previousDefault = protocol === "smtp" ? defaultSmtpPort(currentSecurity) : defaultImapPort(currentSecurity); if (current !== null && current !== previousDefault) return undefined; return protocol === "smtp" ? defaultSmtpPort(nextSecurity) : defaultImapPort(nextSecurity); } function numberValue(value: string | number | null | undefined): number | null { if (typeof value === "number") return Number.isFinite(value) ? value : null; const trimmed = String(value ?? "").trim(); if (!trimmed) return null; const parsed = Number(trimmed); return Number.isFinite(parsed) ? parsed : null; } function stringValue(value: string | number | null | undefined, fallback = ""): string { if (value === null || value === undefined) return fallback; return String(value); }