maildir++ style .foo.bar folders are totally still accepted by Dovecot 2.4, since we have tests that rely on this behaviour. So I am still puzzled why you need to do any of this.
Aki
On 10/12/2025 12:12 EET byteme--- via dovecot <dovecot@dovecot.org> wrote:
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 0fi
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" donedone done
echo "Done."
This is working as desired now. I hope this helps everyone who is facing a similar problem.
ByteMe
dovecot mailing list -- dovecot@dovecot.org To unsubscribe send an email to dovecot-leave@dovecot.org