import { useState } from "react"; import Button from "../Button"; import DismissibleAlert from "../DismissibleAlert"; import FormField from "../FormField"; import PasswordField from "../PasswordField"; import ToggleSwitch from "../ToggleSwitch"; 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 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; folderLookupLabel?: string; busyAction?: "smtp" | "imap" | "folders" | string | null; onTestSmtp?: () => void; onTestImap?: () => void; onLookupFolders?: () => void; smtpTestResult?: MailServerConnectionTestResult | null; imapTestResult?: MailServerConnectionTestResult | null; folderLookupResult?: MailServerFolderLookupResult | null; onUseDetectedFolder?: () => void; useDetectedFolderDisabled?: boolean; mockToggle?: { label?: string; checked: boolean; disabled?: boolean; onChange: (checked: boolean) => void; }; append?: { enabled: boolean; folder: string; disabled?: boolean; folderDisabled?: boolean; onEnabledChange: (enabled: boolean) => void; onFolderChange: (folder: string) => void; }; disabled?: boolean; className?: string; floatingResults?: boolean; initialSection?: MailServerSettingsSection; }; export const mailServerSecurityOptions = ["plain", "tls", "starttls"] as const; export type MailServerSecurityOption = typeof mailServerSecurityOptions[number]; const securityOptions = mailServerSecurityOptions; type MailServerSettingsSection = "smtp" | "imap" | "advanced"; 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 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 = "Test SMTP", imapTestLabel = "Test IMAP", folderLookupLabel = "Folders...", busyAction = null, onTestSmtp, onTestImap, onLookupFolders, smtpTestResult = null, imapTestResult = null, folderLookupResult = null, onUseDetectedFolder, useDetectedFolderDisabled = false, mockToggle, append, disabled = false, className = "", floatingResults = false, initialSection = "smtp" }: 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 appendTargetFolder = append ? append.folder : stringValue(imap.sent_folder, "auto"); const appendTargetDisabled = append ? disabled || append.folderDisabled : imapFieldsDisabled; const canLookupAppendFolders = Boolean(onLookupFolders) && !appendTargetDisabled; const appendTargetHelp = append ? "Folder for sent-message copies. Leave as auto unless this campaign needs a different target." : "Folder used when this IMAP account is used for sent-message copies. Leave as auto to use the server default."; const [activeSection, setActiveSection] = useState(initialSection); const sections: { id: MailServerSettingsSection; label: string }[] = [ { id: "smtp", label: "SMTP" }, { id: "imap", label: "IMAP" }, { id: "advanced", label: "Advanced" } ]; 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 (
{sections.map((section) => ( ))}
{activeSection === "smtp" && (
Server
onSmtpChange({ host: event.target.value })} /> onSmtpChange({ port: event.target.value })} />
Credentials
patchSmtpCredentials({ username: event.target.value })} /> patchSmtpCredentials({ password })} />
{onTestSmtp && (
)}
)} {activeSection === "imap" && (
Server
onImapChange({ host: event.target.value })} /> onImapChange({ port: event.target.value })} />
Credentials
patchImapCredentials({ username: event.target.value })} /> patchImapCredentials({ password })} />
{onTestImap && (
)}
)} {activeSection === "advanced" && (
{mockToggle && (
)} onSmtpChange({ timeout_seconds: event.target.value })} /> onImapChange({ timeout_seconds: event.target.value })} /> {append && (
)}
append ? append.onFolderChange(event.target.value) : onImapChange({ sent_folder: event.target.value })} placeholder="auto" /> {canLookupAppendFolders && }
{canLookupAppendFolders && }
)}
); } 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" && ( Authentication: {authenticated ? "credentials accepted" : "not used"}. )} ); } export function MailServerFolderLookupResultView({ result, disabled = false, onUseDetected }: { result: MailServerFolderLookupResult | null | undefined; disabled?: boolean; onUseDetected?: () => void; }) { if (!result) return null; if (!result.ok) { return {result.message}; } const folders = result.folders ?? []; return (

{result.message}

Detected Sent folder: {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); }