From 76ada57dd794a6a6c02c3f2e24f9638e408da84f Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Tue, 28 Jul 2026 11:50:55 +0700 Subject: [PATCH] Add a defaults button to the folder mapping dialog Mapping the Exchange/Kerio special folders onto mailcow's by hand is repetitive work that scales with the number of accounts. "By default" maps Deleted Items to Trash, Junk E-mail to Junk, Sent Items to Sent and unchecks Public Folders, which mailcow has no counterpart for. The destination select only offered the source folder as a name to create, so a target missing on the destination could not be selected at all. It now also offers the current selection, and marks any name absent from the destination as "(create)". Co-Authored-By: Claude Opus 5 (1M context) --- web/src/app.css | 14 +++- web/src/components/FolderMappingModal.tsx | 81 ++++++++++++++++++----- 2 files changed, 78 insertions(+), 17 deletions(-) diff --git a/web/src/app.css b/web/src/app.css index 6818ac4..e43e61f 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -488,11 +488,23 @@ cursor: pointer; } +.map-toolbar { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 12px; + flex-wrap: wrap; +} + +.map-toolbar .btn { + padding: 6px 12px; + font-size: 11px; +} + .map-all { display: flex; align-items: center; gap: 8px; - margin-bottom: 12px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; diff --git a/web/src/components/FolderMappingModal.tsx b/web/src/components/FolderMappingModal.tsx index ac2aba6..2dcbe64 100644 --- a/web/src/components/FolderMappingModal.tsx +++ b/web/src/components/FolderMappingModal.tsx @@ -20,6 +20,21 @@ function defaultDst(src: string, dstFolders: string[], initial: Record = { + 'deleted items': 'Trash', + 'deleted messages': 'Trash', + 'junk e-mail': 'Junk', + 'junk email': 'Junk', + spam: 'Junk', + 'sent items': 'Sent', + 'sent messages': 'Sent', +} + +// Source folders with no counterpart on mailcow — unchecked by the defaults. +const DEFAULT_EXCLUDED = new Set(['public folders']) + export function FolderMappingModal({ open, srcFolders, dstFolders, initialMapping, initialExcluded, accountLabel, onConfirm, onCancel, }: Props) { @@ -31,16 +46,40 @@ export function FolderMappingModal({ // Options per select: all destination folders, plus the source name itself // (marked "create") when it does not already exist on the destination. + const valueFor = (src: string) => choice[src] ?? defaultDst(src, dstFolders, initialMapping) + + // Options per select: all destination folders, plus any name not present there + // — the source folder itself and the current selection — marked "create". const options = useMemo(() => { const set = new Set(dstFolders) - return (src: string) => { + return (src: string, current: string) => { const opts = [...dstFolders] - if (!set.has(src)) opts.unshift(src) + if (!set.has(current)) opts.unshift(current) + if (!set.has(src) && src !== current) opts.unshift(src) return opts } }, [dstFolders]) - const valueFor = (src: string) => choice[src] ?? defaultDst(src, dstFolders, initialMapping) + // Collapse the Exchange/Kerio folder layout onto mailcow's in one click: the + // per-account mapping is otherwise repetitive work when importing many users. + function applyDefaults() { + const nextChoice = { ...choice } + const nextSynced = { ...synced } + for (const src of srcFolders) { + const key = src.trim().toLowerCase() + if (DEFAULT_EXCLUDED.has(key)) { + nextSynced[src] = false + continue + } + const target = DEFAULT_TARGETS[key] + if (target) { + nextChoice[src] = target + nextSynced[src] = true + } + } + setChoice(nextChoice) + setSynced(nextSynced) + } function confirm() { const mapping: Record = {} @@ -70,17 +109,27 @@ export function FolderMappingModal({ Route each source folder to an existing destination folder. Leaving a folder mapped to its own name creates it on the destination if missing (e.g. map СпамSpam to avoid duplicates).

- +
+ + +
{srcFolders.map((src) => { const on = synced[src] !== false @@ -107,10 +156,10 @@ export function FolderMappingModal({ disabled={!on} onChange={(e) => setChoice((c) => ({ ...c, [src]: e.target.value }))} > - {options(src).map((f) => ( + {options(src, val).map((f) => ( ))}