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) => (
))}