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) <noreply@anthropic.com>
This commit is contained in:
2026-07-28 11:50:55 +07:00
co-authored by Claude Opus 5
parent bb3635e517
commit 76ada57dd7
2 changed files with 78 additions and 17 deletions
+13 -1
View File
@@ -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;
+54 -5
View File
@@ -20,6 +20,21 @@ function defaultDst(src: string, dstFolders: string[], initial: Record<string, s
return src
}
// Exchange/Kerio special folders and their mailcow counterparts. Keyed by the
// lowercased source name so casing differences between servers don't matter.
const DEFAULT_TARGETS: Record<string, string> = {
'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<string, string> = {}
@@ -70,6 +109,15 @@ 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 <code>Спам</code> <code>Spam</code> to avoid duplicates).
</p>
<div className="map-toolbar">
<button
type="button"
className="btn btn-ghost"
onClick={applyDefaults}
title="Map Deleted Items → Trash, Junk E-mail → Junk, Sent Items → Sent and skip Public Folders"
>
By default
</button>
<label className="map-all">
<input
type="checkbox"
@@ -81,6 +129,7 @@ export function FolderMappingModal({
/>
sync all folders
</label>
</div>
<div className="map-grid">
{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) => (
<option key={f} value={f}>
{f}
{f === src && !dstFolders.includes(src) ? ' (create)' : ''}
{dstFolders.includes(f) ? '' : ' (create)'}
</option>
))}
</select>