[cPanel][Maildir] Duplicate emails between different mail boxes using Symbolic links

When some users switch to a new email naming policy, they may need to duplicate email content. This process can be very demanding in terms of inodes and memory space.

In my experience, one client had over a hundred addresses. They needed to retain the old email addresses while maintaining a complete history of previous emails and setting up forwarding.

The transitions were as follows:

We use Maildir, and to avoid content redundancy. I wrote a basic script with the understanding that mail operations won’t alter the original emails. As the user can only:

  • Remove the email message, which will result in the removal of the symlink.
  • Or move the email message from one directory to another (e.g., Inbox => Archive, or Trash); which will be interpreted as moving the symbolic link to the directories .archives/cur or .trash/. Since we use full paths to describe the links, this won’t cause any issues and won’t alter the original files.

I have also excluded some maildir system files from being linked, starting usually with dovecot* or maildir*.

The script usage is straightforward:

./mailinker.sh mycpuser [email protected] [email protected]

This script checks the number of arguments passed, validates the email format, and using cPanel sirectory structure, it checks if the directories and files exist, and starts recursively creating symbolic links. It also logs the process for future reference. The script is designed to handle files or folders with spaces in their names and excludes certain patterns from being linked.

#!/bin/bash# Check if correct number of arguments are passedif [ "$#" -ne 3 ]; then    echo "Usage: $0 <cPanel user> <source email> <destination email>"    exit 1ficpuser=$1 #cPanel usersrc_email=$2 # Source e-maildst_email=$3 # Destination e-mail# Validate email formatif ! [[ "$src_email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then    echo "Invalid source email format: $src_email"    exit 1fiif ! [[ "$dst_email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then    echo "Invalid destination email format: $dst_email"    exit 1fiIFS='@' read -r src_user src_domain <<< "$src_email"IFS='@' read -r dst_user dst_domain <<< "$dst_email"src_dir="/home/$cpuser/mail/$src_domain/$src_user" # Source directorydst_dir="/home/$cpuser/mail/$dst_domain/$dst_user" # Destination directory# Check if directories existif [ ! -d "$src_dir" ]; then    echo "Source directory does not exist: $src_dir"    exit 1fiif [ ! -d "$dst_dir" ]; then    echo "Destination directory does not exist: $dst_dir"    exit 1fi# Array of exclusion patterns like exclude_patterns=("dovecot*" "*.log" "maildirsize" "subscriptions")exclude_patterns=("dovecot*" "maildir*")# Log filetimestamp=$(date +%Y%m%d%H%M%S)log_file="mail_symlink_$timestamp.log"#echo "The process is logged in $log_file"echo "Source: $src_email | $src_dir" >> "$log_file"echo "Destination: $dst_email | $dst_dir" >> "$log_file"echo "=================================================" >> "$log_file"echo "Creating forward..." >> "$log_file"uapi Email add_forwarder domain=$src_domain email=$src_email fwdopt=fwd fwdemail=$dst_email >> "$log_file"echo "=================================================" >> "$log_file"echo "Starting symlinking..." >> "$log_file"# Function to check if a file matches any pattern in the arraymatches_pattern() {    local filename=$1    for pattern in "${exclude_patterns[@]}"; do        if [[ $filename == $pattern ]]; then            return 0        fi    done    return 1}# Function to create symbolic links recursivelycreate_symlinks() {    local src=$1    local dst=$2   # Set temporarily IFS to newline, As I am listing we ls to deal with files or folders having space in their names instead of using Glob.    local oldIFS=$IFS    IFS=$'\n'    # Iterate over all files and directories in source    for item in $(ls -A "$src"); do        local src_path="$src/$item"        local dst_path="$dst/$item"        # If item is a directory        if [ -d "$src_path" ]; then            # If directory doesn't exist in destination, create it            if [ ! -d "$dst_path" ]; then                mkdir -m 751 -p "$dst_path"                echo -e "\nCreated directory $dst_path\n" >> "$log_file"            else                echo -e "\n$dst_path Exists\n" >> "$log_file"            fi                        # Recursively create symbolic links in the directory            create_symlinks "$src_path" "$dst_path"        else            # If item is a file and doesn't exist in destination, create symbolic link            # Exclude files matching any pattern in the array            if ! matches_pattern "$item" && [ ! -e "$dst_path" ]; then                ln -s "$src_path" "$dst_path"                # touch "$dst_path" # Maybe required so that Dovecot recognize the new emails otherwise use: find $dst -exec touch {} \;                echo " - Created symbolic link from $src_path to $dst_path" >> "$log_file"            else                echo " * $dst_path Excluded" >> "$log_file"            fi        fi    done    # Reset IFS to its original value    IFS=$oldIFS}# Call the function to start creating symbolic linkscreate_symlinks "$src_dir" "$dst_dir"find "$dst_dir" -exec touch {} \; # Used so refresh Dovecot

P.S.: The directories in Maildir have a permission of 751, which deviates from the default 775. Consequently, I created them with a permission of 751. However, you can also execute the following command at the end:

/scripts/mailperm –verbose $cpuser

Other Scenarios:

1. Duplicating Files Initially:

If you prefer to duplicate the emails directly without symbolic links, you can simply replace the command ln -s “$src_path” “$dst_path” with cp “$src_path” “$dst_path”.

2. Switching from Symbolic Links to Concrete Files:

If you have already set up the symbolic links and wish to replace them with concrete files, modify the condition so that it checks if it’s a symbolic link -h, then removes it and creates a concrete file:

if ! matches_pattern "$item" && [ -h "$dst_path" ]; then    rm "$dst_path"    cp "$src_path" "$dst_path"

I would also recommend adding an inode counter to ensure that the file numbers remain consistent before and after the operation:

find "$dst_dir" -maxdepth 0 -type d | while read line; do echo "$(find $line | wc -l) $line"; done | sort -n

At the end, you can list any remaining symbolic links:

find $dst_path -type l -ls

Alternatively, instead of using the entire script, you can replace all the symbolic links recursively with their original source using the following command:

find . -type l -exec bash -c 'cp --remove-destination "$(readlink -f "{}")" {}' \;


1 comment

  1. To refresh the size of the email accounts on cPanel, you need to recalculate maildirsize:

    /scripts/remove_dovecot_index_files –user $cpuser
    /scripts/generate_maildirsize —verbose -confirm –allaccounts $cpuser

    If no change is visiable, you may need to remove the current metadata:
    mv /home/$cpuser/.cpanel/email_accounts.json /home/$cpuser/.cpanel/email_accounts.json.bk

Leave a Reply

Your email address will not be published. Required fields are marked *