The problem with dot-notation folders in Dovecot 2.3, which were no longer accepted in Dovecot 2.4, is SOLVED!
Solution:
systemctl stop dovecot
/usr/local/bin/migrate-dovecot-folders.sh
systemctl start dovecot
doveadm force-resync -u '*' '*'
Now there is another challenge: many individual user folders are not automatically displayed. This is automatically synchronized using the script "auto-subscribe-dovecot-folders.sh".
Afterward, all mailboxes will function as expected.
Here are the two scripts: #!/usr/bin/bash
migrate-dovecot-folders.sh
Usage:
./migrate-dovecot-folders.sh (dry-run)
./migrate-dovecot-folders.sh --apply (perform changes)
./migrate-dovecot-folders.sh --user user@domain (only that user)
set -euo pipefail
BASE="/var/vmail" DRY_RUN=1 ONLY_USER="" PRESERVE_CASE=1 # 1 = keep original case after .INBOX., 0 = TitleCase
if [[ "${1:-}" == "--apply" ]]; then DRY_RUN=0; shift; fi while [[ $# -gt 0 ]]; do case "$1" in --apply) DRY_RUN=0; shift ;; --user) ONLY_USER="$2"; shift 2 ;; --lower) PRESERVE_CASE=0; shift ;; *) echo "Unknown arg: $1"; exit 1 ;; esac done
log() { echo "$(date +'%F %T') $*" }
transform a source folder (basename) to target name
transform_name() { local src="$1"
if starts with .INBOX. => strip that prefix
if [[ "$src" =~ ^\.INBOX\.(.+) ]]; then local after="${BASH_REMATCH[1]}" if [[ $PRESERVE_CASE -eq 1 ]]; then echo "$after" else # Title case: replace separators and uppercase first letters echo "$after" | sed -E 's/[^A-Za-z0-9]+/ /g' | awk '{ for(i=1;i<=NF;i++){ $i = toupper(substr($i,1,1)) tolower(substr($i,2)) } ; print $0 }' | sed 's/ /_/g' fi return fi
otherwise strip leading dot
if [[ "$src" =~ ^\.(.+) ]]; then echo "${BASH_REMATCH[1]}" return fi
otherwise return unchanged
echo "$src" }
move/merge a mailbox subdir
move_folder() { local userdir="$1" # full path to user dir, e.g. /var/vmail/domain/user local srcname="$2" # basename e.g. .INBOX.Fail2ban local tgtname="$3" # basename e.g. Fail2ban
local src="$userdir/$srcname" local tgt="$userdir/$tgtname"
sanity checks
[[ -d "$src" ]] || { log "SKIP: src not dir: $src"; return 0; }
if [[ "$src" == "$tgt" ]]; then log "SKIP: source == target for $src" return 0 fi
if [[ $DRY_RUN -eq 1 ]]; then log "DRYRUN: would rename '$src' -> '$tgt'" return 0 fi
if target exists, merge contents
if [[ -d "$tgt" ]]; then log "Merging '$src' -> '$tgt'"
# move cur/new/tmp files (avoid clobbering same names)
for part in cur new tmp; do
if [[ -d "$src/$part" ]]; then
mkdir -p "$tgt/$part"
# move files, avoid overwrite: use mv -n if available; otherwise loop
if mv -n "$src/$part/"* "$tgt/$part/" 2>/dev/null; then
true
else
# fallback: move one-by-one with unique suffix
for f in "$src/$part/"*; do
[[ -e "$f" ]] || continue
base="$(basename "$f")"
if [[ -e "$tgt/$part/$base" ]]; then
# append PID timestamp
mv "$f" "$tgt/$part/${base}.$(date +%s).$$"
else
mv "$f" "$tgt/$part/"
fi
done
fi
fi
done
# move dovecot.* files (index/cache/uidlist etc.) - if target has file, we keep target file
for f in dovecot.* maildirsize subscriptions mailbox*; do
if [[ -e "$src/$f" && ! -e "$tgt/$f" ]]; then
mv "$src/$f" "$tgt/"
else
# if both exist, prefer keeping target and remove src's file
if [[ -e "$src/$f" ]]; then
rm -f "$src/$f"
fi
fi
done
# remove now-empty src directories if empty
find "$src" -mindepth 1 -maxdepth 1 | read -r || rmdir --ignore-fail-on-non-empty "$src" || true
chown -R vmail:vmail "$tgt"
log "Merged done: $src -> $tgt"
return 0
fi
Otherwise simple rename
log "Renaming '$src' -> '$tgt'" mv "$src" "$tgt" chown -R vmail:vmail "$tgt" log "Renamed done: $src -> $tgt" }
iterate all user dirs
find_users() { if [[ -n "$ONLY_USER" ]]; then # Only process single user: split domain/user # user dir may be under /var/vmail/<domain>/<localpart or fulluser?> # try to find the exact directory find "$BASE" -mindepth 2 -maxdepth 3 -type d -path "*/$ONLY_USER" 2>/dev/null return fi
list user dirs: /var/vmail/<domain>/<user>
find "$BASE" -mindepth 2 -maxdepth 2 -type d -printf '%h/%f\n' 2>/dev/null }
Main
log "Starting migration script (DRY_RUN=$DRY_RUN) base=$BASE only_user=$ONLY_USER"
while IFS= read -r userdir; do [[ -n "$userdir" ]] || continue
ensure it's a user directory (has cur/new/tmp or maildirfolder)
if [[ ! -d "$userdir" ]]; then continue; fi
scan for dot-folders in that user dir
while IFS= read -r src; do srcbase="$(basename "$src")" # skip standard maildir parts case "$srcbase" in cur|new|tmp|Maildir|dovecot.*|subscriptions|maildirsize) continue ;; esac # select only directories starting with dot OR name equal to "INBOX" variants if [[ "$srcbase" =~ ^\. ]] || [[ "$srcbase" =~ ^INBOX ]]; then tgtbase="$(transform_name "$srcbase")" # if transform yields empty -> skip [[ -n "$tgtbase" ]] || continue # avoid converting e.g. ".INBOX" -> "" (keep INBOX) if [[ "$tgtbase" == "" ]]; then tgtbase="INBOX"; fi move_folder "$userdir" "$srcbase" "$tgtbase" fi done < <(find "$userdir" -mindepth 1 -maxdepth 1 -type d -printf '%p\n' 2>/dev/null) done < <(find_users)
log "Done. If not --apply then this was a dry-run. Verify and then run with --apply."
after running with --apply, run (per-user) reindex/resync, e.g.:
doveadm force-resync -u user@domain '*'
or for many users:
for u in user1@d; do doveadm force-resync -u '*' '*'; done
And: #!/bin/bash
auto-subscribe-dovecot-folders.sh
Automatically subscribes to all existing Maildir folders for all users.
BASE="/var/vmail" SYSTEM_FOLDERS=("Drafts" "Sent" "Junk" "Trash" "Archive" "Quarantine" "INBOX" "new" "cur" "tmp" "sieve")
echo "Starting auto-subscribe of non-system folders..."
Loop through all domains
for DOMAIN in "$BASE"/*; do [ -d "$DOMAIN" ] || continue DOMAIN_NAME=$(basename "$DOMAIN")
Loop through all users
for USERDIR in "$DOMAIN"/*; do [ -d "$USERDIR" ] || continue USERNAME=$(basename "$USERDIR") USER_EMAIL="$USERNAME@$DOMAIN_NAME"
echo "Processing user $USER_EMAIL..."
# Loop through all folders in the user directory
for MAILBOX in "$USERDIR"/*; do
[ -d "$MAILBOX" ] || continue
FOLDER=$(basename "$MAILBOX")
# Check if it's a system folder
SKIP=0
for SYS in "${SYSTEM_FOLDERS[@]}"; do
if [[ "$FOLDER" == "$SYS" ]]; then
SKIP=1
break
fi
done
[ $SKIP -eq 1 ] && continue
# Subscribe to mailbox
echo "Subscribing $FOLDER for $USER_EMAIL..."
doveadm mailbox subscribe -u "$USER_EMAIL" "$FOLDER"
done
done done
echo "Done."
This is working as desired now. I hope this helps everyone who is facing a similar problem.
ByteMe