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]
