Zimbra Script To Remove Broken Shares

Zimbra Script To Remove Broken Shares

Sharing in Zimbra is an incredibly useful collaboration feature. We have customers with more than 40K shared email folders; users report sharing enhances productivity and streamlines workflow processes.

Broken shares can cause performance issues, slow web client loading and sometimes even prevent a user with broken shares from logging in to the web client.  (If User A shares a folder tree with User B, and then User A leaves the company and User A’s account is deleted, User B’s account now has a string of “broken” shares.)

So, it’s important to remove “broken” shares periodically.  If users have only a handful of shares, users can keep things clean on their own.  But we host customers where a typical user can have more than 40K (forty thousand) folders as a result of sharing folder trees from multiple other users and where each folder tree can contain hundreds of subfolders.

In these cases, it is not practical to expect users to manage broken share cleanup on their own.

To help, we developed a script that leverages a technique from a Zimbra wiki page. Our script, run on any mailstore (ideally in a screen or tmux session!), will identify all of the broken shares in a Zimbra system (single- or multi-server), generate a report, and then create a separate script an administrator can run that will delete all of the broken shares automagically.

Our script, run CPU and IO “nicely” during the workday on a busy mailstore that normally runs at about 75% of CPU capacity, took about 30 minutes to parse through some 850 accounts where 32 users had 95 broken shares.  Of course, your mileage may vary.

Here’s the script:

#!/bin/bash

################################################################################
# Zimbra Broken Shares Checker (using zmsoap GetFolderRequest)
#
# Copyright 2025 Mission Critical Email LLC. All rights reserved.
#
# Based on: https://wiki.zimbra.com/wiki/Steps_to_find_broken_or_dead_share_mount_on_a_user%27s_mailbox_and_remove_using_zmmailbox_command
#
# DISCLAIMER:
# This script is provided "AS IS" without warranty of any kind, either express
# or implied, including but not limited to the implied warranties of
# merchantability and fitness for a particular purpose. Use at your own risk.
# In no event shall Mission Critical Email LLC be liable for any damages
# whatsoever arising out of the use of or inability to use this script.
#
# DESCRIPTION:
# Scans all Zimbra user accounts for broken share mount points and generates
# a report and removal script.
#
# USAGE:
# Run as zimbra user on a mailstore: time nice -n 19 ionice -c2 -n7 '/path/to/script.sh'
#
################################################################################

LOG_FILE="/tmp/zimbra_broken_shares_$(date +%Y%m%d_%H%M%S).log"
BROKEN_SHARES_FILE="/tmp/zimbra_broken_shares_$(date +%Y%m%d_%H%M%S).csv"
REMOVE_SCRIPT="/tmp/zimbra_remove_broken_shares_$(date +%Y%m%d_%H%M%S).sh"

# Configuration
DELAY_BETWEEN_USERS=0.2  # Seconds to wait between checking users (for nice IO)
ZMSOAP_TIMEOUT=300  # Timeout in seconds (5 minutes - increase if you have very large mailboxes)
SKIP_ON_TIMEOUT=true  # Set to false to halt script on timeout

echo "Zimbra Broken Shares Checker - Started at $(date)" | tee "$LOG_FILE"
echo "Configuration: Timeout=${ZMSOAP_TIMEOUT}s, Delay=${DELAY_BETWEEN_USERS}s" | tee -a "$LOG_FILE"
echo "" | tee -a "$LOG_FILE"

echo "UserEmail,FolderId,AbsolutePath,FolderName" > "$BROKEN_SHARES_FILE"
echo "#!/bin/bash" > "$REMOVE_SCRIPT"
echo "# Generated removal script - review before executing!" >> "$REMOVE_SCRIPT"
echo "# Run as zimbra user: su - zimbra -c 'bash $REMOVE_SCRIPT'" >> "$REMOVE_SCRIPT"
echo "" >> "$REMOVE_SCRIPT"

echo "Gathering all user accounts..." | tee -a "$LOG_FILE"
ALL_USERS=$(zmprov -l gaa)
TOTAL_USERS=$(echo "$ALL_USERS" | wc -l)
CURRENT_USER=0
TOTAL_BROKEN=0
USERS_WITH_BROKEN=0
TIMEOUT_COUNT=0

echo "Found $TOTAL_USERS users to check" | tee -a "$LOG_FILE"
echo "" | tee -a "$LOG_FILE"

# Check each user for broken shares
for USER_EMAIL in $ALL_USERS; do
    CURRENT_USER=$((CURRENT_USER + 1))

    # Show progress every 10 users
    if [ $((CURRENT_USER % 10)) -eq 0 ]; then
        echo "[$CURRENT_USER/$TOTAL_USERS] Progress: $USERS_WITH_BROKEN users with broken shares, $TIMEOUT_COUNT timeouts" | tee -a "$LOG_FILE"
    fi

    # Use zmsoap GetFolderRequest to get folder tree with broken share indicators
    # The @tr=1 parameter requests the full folder tree
    FOLDER_DATA=$(timeout "$ZMSOAP_TIMEOUT" zmsoap -z -m "$USER_EMAIL" GetFolderRequest @tr=1 2>&1)
    EXIT_CODE=$?

    if [ $EXIT_CODE -eq 124 ]; then
        TIMEOUT_COUNT=$((TIMEOUT_COUNT + 1))
        echo "[$CURRENT_USER/$TOTAL_USERS] [TIMEOUT] $USER_EMAIL - took longer than ${ZMSOAP_TIMEOUT}s (large mailbox)" | tee -a "$LOG_FILE"

        if [ "$SKIP_ON_TIMEOUT" = false ]; then
            echo "ERROR: Timeout occurred and SKIP_ON_TIMEOUT is false. Exiting." | tee -a "$LOG_FILE"
            echo "Consider increasing ZMSOAP_TIMEOUT or setting SKIP_ON_TIMEOUT=true" | tee -a "$LOG_FILE"
            exit 1
        fi

        sleep "$DELAY_BETWEEN_USERS"
        continue
    elif [ $EXIT_CODE -ne 0 ]; then
        if echo "$FOLDER_DATA" | grep -qi "no such account\|does not exist"; then
            echo "  [WARNING] Account does not exist: $USER_EMAIL" >> "$LOG_FILE"
        else
            echo "  [WARNING] Could not retrieve folders for $USER_EMAIL (exit: $EXIT_CODE)" >> "$LOG_FILE"
        fi
        sleep "$DELAY_BETWEEN_USERS"
        continue
    fi

    # Check if response is empty
    if [ -z "$FOLDER_DATA" ]; then
        echo "  [WARNING] Empty response for $USER_EMAIL" >> "$LOG_FILE"
        sleep "$DELAY_BETWEEN_USERS"
        continue
    fi

    # Check for broken shares - look for 'broken="1"' in the XML response
    BROKEN_CHECK=$(echo "$FOLDER_DATA" | grep 'broken="1"')

    if [ -n "$BROKEN_CHECK" ]; then
        USERS_WITH_BROKEN=$((USERS_WITH_BROKEN + 1))
        echo "[$CURRENT_USER/$TOTAL_USERS] Found broken shares for: $USER_EMAIL" | tee -a "$LOG_FILE"

        # Extract broken share details from XML
        # Look for folder/link elements with broken="1"
        # Use process substitution to avoid subshell issue with counter
        while read -r LINE; do
            # Extract folder ID
            FOLDER_ID=$(echo "$LINE" | sed -n 's/.*id="\([^"]*\)".*/\1/p')

            # Extract absolute path
            ABS_PATH=$(echo "$LINE" | sed -n 's/.*absFolderPath="\([^"]*\)".*/\1/p')

            # Extract folder name
            FOLDER_NAME=$(echo "$LINE" | sed -n 's/.*name="\([^"]*\)".*/\1/p')

            # Sometimes the attributes are in different order, try alternative extraction
            if [ -z "$FOLDER_ID" ]; then
                FOLDER_ID=$(echo "$LINE" | grep -oP 'id="\K[^"]+')
            fi
            if [ -z "$ABS_PATH" ]; then
                ABS_PATH=$(echo "$LINE" | grep -oP 'absFolderPath="\K[^"]+')
            fi
            if [ -z "$FOLDER_NAME" ]; then
                FOLDER_NAME=$(echo "$LINE" | grep -oP 'name="\K[^"]+')
            fi

            if [ -n "$FOLDER_ID" ] && [ -n "$ABS_PATH" ]; then
                echo "  [BROKEN] ID: $FOLDER_ID | Path: $ABS_PATH | Name: $FOLDER_NAME" | tee -a "$LOG_FILE"

                # Add to CSV (escape quotes in fields)
                ESCAPED_PATH=$(echo "$ABS_PATH" | sed 's/"/""/g')
                ESCAPED_NAME=$(echo "$FOLDER_NAME" | sed 's/"/""/g')
                printf '"%s","%s","%s","%s"\n' "$USER_EMAIL" "$FOLDER_ID" "$ESCAPED_PATH" "$ESCAPED_NAME" >> "$BROKEN_SHARES_FILE"

                # Add removal command to script
                echo "# User: $USER_EMAIL - Remove broken share: $ABS_PATH" >> "$REMOVE_SCRIPT"
                echo "echo \"[$USER_EMAIL] Removing broken share ID $FOLDER_ID: $ABS_PATH\"" >> "$REMOVE_SCRIPT"
                echo "if ! zmmailbox -z -m \"$USER_EMAIL\" df \"$FOLDER_ID\" 2>&1; then echo \"  Error removing folder $FOLDER_ID\"; fi" >> "$REMOVE_SCRIPT"
                echo "" >> "$REMOVE_SCRIPT"

                TOTAL_BROKEN=$((TOTAL_BROKEN + 1))
            else
                echo "  [WARNING] Could not parse broken share details from: $LINE" >> "$LOG_FILE"
            fi
        done < <(echo "$FOLDER_DATA" | grep 'broken="1"')
    fi

    # Small delay to be nice to the system
    sleep "$DELAY_BETWEEN_USERS"
done

echo "" | tee -a "$LOG_FILE"
echo "========================================" | tee -a "$LOG_FILE"
echo "Scan completed at $(date)" | tee -a "$LOG_FILE"
echo "" | tee -a "$LOG_FILE"
echo "SUMMARY:" | tee -a "$LOG_FILE"
echo "  Total users checked: $TOTAL_USERS" | tee -a "$LOG_FILE"
echo "  Users with broken shares: $USERS_WITH_BROKEN" | tee -a "$LOG_FILE"
echo "  Total broken shares found: $TOTAL_BROKEN" | tee -a "$LOG_FILE"
echo "  Timeouts encountered: $TIMEOUT_COUNT" | tee -a "$LOG_FILE"
echo "" | tee -a "$LOG_FILE"
echo "OUTPUT FILES:" | tee -a "$LOG_FILE"
echo "  Log file: $LOG_FILE" | tee -a "$LOG_FILE"
echo "  CSV report: $BROKEN_SHARES_FILE" | tee -a "$LOG_FILE"
echo "  Removal script: $REMOVE_SCRIPT" | tee -a "$LOG_FILE"
echo "" | tee -a "$LOG_FILE"

if [ "$TIMEOUT_COUNT" -gt 0 ]; then
    echo "NOTE: $TIMEOUT_COUNT users timed out during scan." | tee -a "$LOG_FILE"
    echo "These users were skipped. Consider increasing ZMSOAP_TIMEOUT if needed." | tee -a "$LOG_FILE"
    echo "" | tee -a "$LOG_FILE"
fi

if [ "$TOTAL_BROKEN" -gt 0 ]; then
    echo "NEXT STEPS:" | tee -a "$LOG_FILE"
    echo "1. Review the broken shares in: $BROKEN_SHARES_FILE" | tee -a "$LOG_FILE"
    echo "2. Review the removal script: $REMOVE_SCRIPT" | tee -a "$LOG_FILE"
    echo "3. To remove ALL broken shares, run:" | tee -a "$LOG_FILE"
    echo "   su - zimbra -c 'bash $REMOVE_SCRIPT'" | tee -a "$LOG_FILE"
    echo "" | tee -a "$LOG_FILE"
    echo "   Or remove individual shares manually:" | tee -a "$LOG_FILE"
    echo "   zmmailbox -z -m  df \"\"" | tee -a "$LOG_FILE"

    # Make the removal script executable
    chmod +x "$REMOVE_SCRIPT"
else
    echo "No broken shares found! Your Zimbra system is clean." | tee -a "$LOG_FILE"
fi

echo "========================================" | tee -a "$LOG_FILE"

If you’d like help dealing with your broken shares, or any other Zimbra housekeeping tasks, please just fill out the form and we’ll be back in touch!

Go back

Your message has been sent

Warning
Warning
Warning
Warning

Warning.

 

Hope that helps,
L. Mark Stone
Mission Critical Email LLC
14 October 2025

The information provided in this blog is intended for informational and educational purposes only. The views expressed herein are those of Mr. Stone personally. The contents of this site are not intended as advice for any purpose and are subject to change without notice. Mission Critical Email makes no warranties of any kind regarding the accuracy or completeness of any information on this site, and we make no representations regarding whether such information is up-to-date or applicable to any particular situation. All copyrights are reserved by Mr. Stone. Any portion of the material on this site may be used for personal or educational purposes provided appropriate attribution is given to Mr. Stone and this blog.

Leave a Reply