feat(mail): map standard IMAP folders per profile

This commit is contained in:
2026-08-19 21:27:42 +02:00
parent 393331574a
commit e0e00d7000
13 changed files with 243 additions and 33 deletions
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState, type ReactNode } from "react";
import { ActionToolbar, FormGrid, ActionBlockerHint, AdminSelectionList, ConnectionTree, DocumentationHelpLink, FieldLabel, LoadingFrame, MailServerSettingsPanel, PolicyLockedHint, PolicyPathHelp, PolicyRow, PolicySourcePath, PolicyTable, StageRail, StatusBadge, TableActionGroup, ToggleSwitch, hasMailImapSettings, mailImapSettingsPayload, mailServerSecurityOptions, mailSmtpSettingsPayload, mailTextOrNull, mailTransportCredentialsPayload, mergeDeltaRows, normalizeMailServerSecurity, normalizePolicySourcePathItems, useDeltaWatermarks, type ConnectionTreeColumn, type MailServerConnectionTestResult, type MailServerCredentialSettings, type MailServerImapSettings, type MailServerSmtpSettings, type NormalizedPolicySourcePathItem, type PolicySourcePathItem } from "@govoplan/core-webui";
import { ActionToolbar, FormGrid, ActionBlockerHint, AdminSelectionList, ConnectionTree, DocumentationHelpLink, FieldLabel, LoadingFrame, MailServerSettingsPanel, PolicyLockedHint, PolicyPathHelp, PolicyRow, PolicySourcePath, PolicyTable, StageRail, StatusBadge, TableActionGroup, ToggleSwitch, hasMailImapSettings, mailImapSettingsPayload, mailServerSecurityOptions, mailSmtpSettingsPayload, mailTextOrNull, mailTransportCredentialsPayload, mergeDeltaRows, normalizeMailImapFolderMappings, normalizeMailServerSecurity, normalizePolicySourcePathItems, useDeltaWatermarks, type ConnectionTreeColumn, type MailImapFolderMappings, type MailImapFolderListResponse, type MailServerConnectionTestResult, type MailServerCredentialSettings, type MailServerImapSettings, type MailServerSmtpSettings, type NormalizedPolicySourcePathItem, type PolicySourcePathItem } from "@govoplan/core-webui";
import { ArrowLeft, ArrowRight, Inbox, KeyRound, Link2, Pencil, Plus, Send, Settings2, Trash2, Unlink } from "lucide-react";
import type { ApiSettings } from "../../types";
import {
@@ -11,6 +11,8 @@ import {
deactivateMailServerProfile,
fetchMailSettingsDelta,
getMailProfilePolicy,
listImapFolders,
listMailProfileImapFolders,
mailProfilePatternKeys,
mailProfilePolicyLimitKeys,
listAvailableMailCredentials,
@@ -102,6 +104,7 @@ type ProfileDraft = {
imapUsername: string;
imapPassword: string;
imapSentFolder: string;
imapFolderMappings: MailImapFolderMappings;
imapTimeout: string;
};
@@ -1163,7 +1166,8 @@ function ProfileForm({
const { translateText } = usePlatformLanguage();
const [smtpTestResult, setSmtpTestResult] = useState<MailServerConnectionTestResult | null>(null);
const [imapTestResult, setImapTestResult] = useState<MailServerConnectionTestResult | null>(null);
const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | null>(null);
const [imapFolderResult, setImapFolderResult] = useState<MailImapFolderListResponse | null>(null);
const [mailActionState, setMailActionState] = useState<"smtp" | "imap" | "folders" | null>(null);
const disabled = busy || !canWrite;
const credentialDisabled = disabled || !canManageCredentials;
@@ -1256,6 +1260,7 @@ function ProfileForm({
useEffect(() => {
setSmtpTestResult(null);
setImapTestResult(null);
setImapFolderResult(null);
setMailActionState(null);
}, [editing, editTarget]);
@@ -1276,6 +1281,7 @@ function ProfileForm({
imapPort: patch.port !== undefined ? String(patch.port ?? "") : draft.imapPort,
imapSecurity: patch.security !== undefined ? readSecurity(String(patch.security || "tls"), "tls") : draft.imapSecurity,
imapSentFolder: patch.sent_folder !== undefined ? String(patch.sent_folder ?? "") : draft.imapSentFolder,
imapFolderMappings: patch.folder_mappings !== undefined ? normalizeMailImapFolderMappings(patch.folder_mappings) : draft.imapFolderMappings,
imapTimeout: patch.timeout_seconds !== undefined ? String(patch.timeout_seconds ?? "") : draft.imapTimeout
});
}
@@ -1325,6 +1331,21 @@ function ProfileForm({
}
}
async function runImapFolderLookup() {
if (!draftHasImap) return;
setMailActionState("folders");
setImapFolderResult(null);
try {
setImapFolderResult(useSavedImapTest && existingProfile ?
await listMailProfileImapFolders(settings, existingProfile.id, selectedServerId, selectedCredentialId) :
await listImapFolders(settings, rawImapPayload(draft, false)));
} catch (err) {
setImapFolderResult({ ok: false, protocol: "imap", message: errorMessage(err), folders: [] });
} finally {
setMailActionState(null);
}
}
return (
<div className="mail-profile-form">
{!canManageCredentials && (
@@ -1472,7 +1493,7 @@ function ProfileForm({
{showSettingsPanel && settingsPanelMode && (!creatingCredential || creatingNewCredential) &&
<MailServerSettingsPanel
smtp={{ host: draft.smtpHost, port: draft.smtpPort, security: draft.smtpSecurity, timeout_seconds: draft.smtpTimeout }}
imap={{ host: draft.imapHost, port: draft.imapPort, security: draft.imapSecurity, sent_folder: draft.imapSentFolder, timeout_seconds: draft.imapTimeout }}
imap={{ host: draft.imapHost, port: draft.imapPort, security: draft.imapSecurity, sent_folder: draft.imapSentFolder, folder_mappings: draft.imapFolderMappings, timeout_seconds: draft.imapTimeout }}
smtpCredentials={{ username: draft.smtpUsername, password: draft.smtpPassword }}
imapCredentials={{ username: draft.imapUsername, password: draft.imapPassword }}
onSmtpChange={patchSmtpSettings}
@@ -1501,8 +1522,10 @@ function ProfileForm({
busyAction={mailActionState}
onTestSmtp={() => void runSmtpTest()}
onTestImap={() => void runImapTest()}
onLookupImapFolders={() => void runImapFolderLookup()}
smtpTestResult={smtpTestResult}
imapTestResult={imapTestResult}
imapFolderLookupResult={imapFolderResult}
initialSection={initialSection}
visibleSections={visibleSections}
mode={settingsPanelMode} />
@@ -1645,6 +1668,7 @@ function emptyProfileDraft(): ProfileDraft {
imapUsername: "",
imapPassword: "",
imapSentFolder: "auto",
imapFolderMappings: {},
imapTimeout: "30"
};
}
@@ -1696,7 +1720,11 @@ function profileToDraft(profile: MailServerProfile, target: MailProfileEditTarge
? targetUsername
: stringValue(profile.credentials?.imap?.username ?? profile.imap?.username),
imapPassword: "",
imapSentFolder: stringValue("sent_folder" in (imapConfig ?? {}) ? imapConfig?.sent_folder || "auto" : "auto"),
imapSentFolder: stringValue(imapConfig?.folder_mappings?.sent || ("sent_folder" in (imapConfig ?? {}) ? imapConfig?.sent_folder || "auto" : "auto")),
imapFolderMappings: normalizeMailImapFolderMappings({
...(imapConfig?.folder_mappings ?? {}),
...(!imapConfig?.folder_mappings?.sent && imapConfig?.sent_folder && imapConfig.sent_folder !== "auto" ? { sent: imapConfig.sent_folder } : {})
}),
imapTimeout: stringValue(imapConfig?.timeout_seconds ?? 30)
};
}
@@ -1873,7 +1901,7 @@ function smtpServerPayload(draft: ProfileDraft): MailSmtpTestPayload {
function imapServerPayload(draft: ProfileDraft): MailImapTestPayload {
return mailImapSettingsPayload<MailSecurity>(
{ host: draft.imapHost, port: draft.imapPort, security: draft.imapSecurity, sent_folder: draft.imapSentFolder, timeout_seconds: draft.imapTimeout },
{ host: draft.imapHost, port: draft.imapPort, security: draft.imapSecurity, sent_folder: draft.imapSentFolder, folder_mappings: draft.imapFolderMappings, timeout_seconds: draft.imapTimeout },
{ fallbackSecurity: "tls", allowedSecurity: securityOptions }
);
}