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 <[email protected]> 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 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
> _______________________________________________
> dovecot mailing list -- [email protected]
> To unsubscribe send an email to [email protected]

_______________________________________________
dovecot mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to