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:
- [email protected] => [email protected]
- [email protected] => [email protected]
- [email protected] => [email protected], [email protected]
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 DovecotP.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 $cpuserOther 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 -nAt the end, you can list any remaining symbolic links:
find $dst_path -type l -lsAlternatively, 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 "{}")" {}' \;
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