feat(mail): map standard IMAP folders per profile
This commit is contained in:
@@ -22,11 +22,17 @@ assert.match(profiles, /disabledReason: credentialMutationBlocker/);
|
||||
assert.match(profiles, /disabledReason=\{editorSaveBlocker\}/);
|
||||
assert.match(profiles, /disabledReason=\{policySaveBlocker\}/);
|
||||
assert.match(profiles, /smtpActionDisabledReason=\{smtpTestBlocker\}/);
|
||||
assert.match(profiles, /folder_mappings: draft\.imapFolderMappings/);
|
||||
assert.match(profiles, /listMailProfileImapFolders[\s\S]*listImapFolders/);
|
||||
assert.match(profiles, /onLookupImapFolders=\{\(\) => void runImapFolderLookup\(\)\}/);
|
||||
assert.match(profiles, /imapFolderLookupResult=\{imapFolderResult\}/);
|
||||
|
||||
assert.match(mailbox, /ActionBlockerHint/);
|
||||
assert.match(mailbox, /DocumentationHelpLink/);
|
||||
assert.match(mailbox, /topicId: "mail\.workflow\.read-mailbox"/);
|
||||
assert.match(mailbox, /disabledReason=\{folderReloadBlocker\}/);
|
||||
assert.match(mailbox, /folder_mappings\?\.inbox/);
|
||||
assert.match(mailbox, /detected_folder_mappings\?\.inbox/);
|
||||
assert.match(mailbox, /onKeyDown=\{\(event\) => \{[\s\S]*event\.key === "Enter" \|\| event\.key === " "/);
|
||||
|
||||
assert.match(bounces, /DocumentationHelpLink/);
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,6 +177,10 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
|
||||
|
||||
async function loadMailboxBootstrap(profileId = selectedProfileId, refresh = false) {
|
||||
if (!profileId) return;
|
||||
const profileInbox = profiles.find((profile) => profile.id === profileId)?.imap?.folder_mappings?.inbox || "";
|
||||
const requestedFolder = foldersLoadedForProfile === profileId && selectedFolder
|
||||
? selectedFolder
|
||||
: profileInbox || "INBOX";
|
||||
const folderRequestId = ++folderRequestRef.current;
|
||||
const messageRequestId = ++messageListRequestRef.current;
|
||||
messageDetailRequestRef.current += 1;
|
||||
@@ -194,7 +198,7 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
|
||||
setDetailError("");
|
||||
setError("");
|
||||
try {
|
||||
const response = await bootstrapMailbox(settings, profileId, selectedFolder || "INBOX", messagePageSize, 0, refresh);
|
||||
const response = await bootstrapMailbox(settings, profileId, requestedFolder, messagePageSize, 0, refresh);
|
||||
if (folderRequestId !== folderRequestRef.current || messageRequestId !== messageListRequestRef.current) return;
|
||||
if (!response.folders.ok) throw new Error(response.folders.message || "i18n:govoplan-mail.mailbox_folders_could_not_be_loaded.c3e3880e");
|
||||
const loadedFolders = response.folders.folders?.length ? response.folders.folders : [{ name: "INBOX", flags: [] }];
|
||||
@@ -202,7 +206,10 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
|
||||
const total = response.messages.total_count ?? loadedMessages.length;
|
||||
let nextFolder = response.folder || response.messages.folder || selectedFolder || "INBOX";
|
||||
if (!loadedFolders.some((folder) => folder.name === nextFolder)) {
|
||||
nextFolder = loadedFolders.some((folder) => folder.name === "INBOX") ? "INBOX" : response.folders.detected_sent_folder || loadedFolders[0]?.name || "INBOX";
|
||||
const detectedInbox = response.folders.detected_folder_mappings?.inbox;
|
||||
nextFolder = detectedInbox && loadedFolders.some((folder) => folder.name === detectedInbox)
|
||||
? detectedInbox
|
||||
: loadedFolders.some((folder) => folder.name === "INBOX") ? "INBOX" : response.folders.detected_sent_folder || loadedFolders[0]?.name || "INBOX";
|
||||
}
|
||||
const foldersWithCounts = loadedFolders.map((folder) => folder.name === nextFolder ? { ...folder, message_count: total } : folder);
|
||||
const cursorKey = mailboxCursorKey(profileId, nextFolder, messagePageSize);
|
||||
|
||||
Reference in New Issue
Block a user