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]

Reply via email to