1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2025-05-30 17:30:53 +00:00
This commit is contained in:
Ahmad Kouider 2025-05-17 13:21:20 -07:00 committed by GitHub
commit 0e383189d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 672 additions and 0 deletions

150
tools/migrate.md Normal file
View File

@ -0,0 +1,150 @@
# Mail-in-a-Box Migration Script
**Author:** Ahmad Kouider
**Created:** April 3, 2025
## Overview
The `migrate-miab.sh` script provides a comprehensive solution for migrating Mail-in-a-Box installations from one server to another. It handles the secure transfer of data, configuration updates, and service management to ensure a smooth migration process.
## Features
- **Secure Data Transfer**: Uses rsync over SSH for efficient and secure file transfer
- **Configuration Management**: Automatically updates the Mail-in-a-Box configuration with the new IP address
- **Service Control**: Optional stopping and starting of Mail-in-a-Box services during migration
- **Error Handling**: Robust error detection and recovery with automatic service restoration
- **Partial Transfer Support**: Special handling for partial transfers (common with large installations)
- **Dry Run Mode**: Test the migration process without making any changes
## Prerequisites
- SSH access to both source and target servers
- rsync installed on the source server
- Sufficient disk space on the target server
- Mail-in-a-Box installed on the source server
## Usage
### Basic Usage
```bash
./migrate-miab.sh --username admin --target-host 192.168.1.100 --new-ip 203.0.113.10
```
### All Available Options
| Option | Description | Default |
|--------|-------------|---------|
| `--username USERNAME` | SSH username for target server | (required) |
| `--target-host HOST` | IP address or domain of target server | (required) |
| `--new-ip IP` | Public IP address of the new target machine | (required) |
| `--source-path PATH` | Path to Mail-in-a-Box files | /home/user-data |
| `--target-path PATH` | Destination path on target server | /home/user-data |
| `--config-path PATH` | Path to mailinabox.conf file | /etc/mailinabox.conf |
| `--exclude LIST` | Comma-separated list of files/folders to exclude | (none) |
| `--ssh-port PORT` | SSH port to use | 22 |
| `--stop-services` | Stop Mail-in-a-Box services during transfer | (default: keep running) |
| `--ignore-partial` | Continue migration even if some files fail to transfer | (default: prompt user) |
| `--dry-run` | Simulate the transfer without making changes | (default: false) |
| `--help` | Display help message | |
### Example Commands
**Standard Migration:**
```bash
./migrate-miab.sh --username admin --target-host mail2.example.com --new-ip 203.0.113.10
```
**Migration with Service Stopping:**
```bash
./migrate-miab.sh --username admin --target-host mail2.example.com --new-ip 203.0.113.10 --stop-services
```
**Excluding Certain Directories:**
```bash
./migrate-miab.sh --username admin --target-host mail2.example.com --new-ip 203.0.113.10 --exclude 'backup,logs,tmp'
```
**Dry Run Test:**
```bash
./migrate-miab.sh --username admin --target-host mail2.example.com --new-ip 203.0.113.10 --dry-run
```
## Migration Process
1. **Preparation**:
- Validates all input parameters
- Generates a temporary SSH key pair
- Prompts you to add the key to the target server
2. **Service Management** (if `--stop-services` is used):
- Stops all Mail-in-a-Box services on the source server
- This helps prevent file corruption during transfer
3. **Data Transfer**:
- Uses rsync to efficiently transfer all Mail-in-a-Box data
- Handles partial transfers with interactive prompting
4. **Configuration Update**:
- Updates the mailinabox.conf file with the new IP address
- Transfers the updated configuration to the target server
5. **Service Restoration**:
- Restarts any services that were stopped (if applicable)
- Ensures the source server returns to its original state
## Important Notes
### Partial Transfers
When transferring large Mail-in-a-Box installations, you may encounter "partial transfer" errors (rsync code 23). This typically happens because:
- Some files are actively being used (open files)
- Permission issues prevent access to certain files
- Special system files cannot be transferred normally
The script provides two ways to handle this:
1. **Interactive prompt**: Choose whether to continue or abort
2. **Automatic continuation**: Use the `--ignore-partial` flag
### Post-Migration Steps
After the migration completes successfully, you **MUST**:
1. **Reinstall Mail-in-a-Box** on the target server:
```bash
curl -s https://mailinabox.email/setup.sh | sudo bash
```
This ensures all configurations are properly updated while preserving your transferred data.
2. **Update DNS records** to point to the new server IP address
3. **Test mail functionality** on the new server
## Troubleshooting
### Common Issues
1. **SSH Connection Failures**:
- Ensure the public key was added to ~/.ssh/authorized_keys on the target server
- Verify the SSH port is correct (default: 22)
- Check for firewall rules blocking SSH connections
2. **File Transfer Errors**:
- Consider using the `--stop-services` option to prevent open file issues
- Use `--exclude` to skip problematic directories
- For partial transfers, use `--ignore-partial` if you're confident in the data integrity
3. **Configuration File Not Found**:
- If your Mail-in-a-Box has a non-standard configuration path, use the `--config-path` option
## Security Considerations
- The script generates a temporary SSH key pair for the migration
- Keys are automatically deleted after the migration completes
- No passwords are stored or transmitted
- All data is transferred over encrypted SSH connections
## License
This script is provided as-is with no warranty. Use at your own risk.

522
tools/migrate.sh Normal file
View File

@ -0,0 +1,522 @@
#!/bin/bash
# migrate-miab.sh - Mail-in-a-Box Migration Script
# Author: Ahmad Kouider
# Created: April 3, 2025
# Description: Transfers Mail-in-a-Box data from one server to another
#
# Usage: ./migrate-miab.sh --username USER --target-host HOST --new-ip IP [OPTIONS]
#
# Required:
# --username USER SSH username for target server
# --target-host HOST IP address or domain of target server
# --new-ip IP Public IP address of the new target machine
#
# Options:
# --source-path PATH Path to Mail-in-a-Box files (default: /home/user-data)
# --target-path PATH Destination path on target server (default: /home/user-data)
# --config-path PATH Path to mailinabox.conf file (default: /etc/mailinabox.conf)
# --exclude LIST Comma-separated list of files/folders to exclude
# --ssh-port PORT SSH port to use (default: 22)
# --stop-services Stop Mail-in-a-Box services during transfer (default: keep running)
# --ignore-partial Continue migration even if some files fail to transfer (rsync code 23)
# --dry-run Simulate the transfer without making changes
# --help Display help message
#
# Example: ./migrate-miab.sh --username admin --target-host 192.168.1.100 --new-ip 203.0.113.10
#
# Note: After migration, you MUST reinstall Mail-in-a-Box on the target server:
# curl -s https://mailinabox.email/setup.sh | sudo bash
# Default values
SOURCE_PATH="/home/user-data"
TARGET_PATH="/home/user-data"
EXCLUDE=""
DRY_RUN=false
STOP_SERVICES=false
SERVICES_STOPPED=false
IGNORE_PARTIAL=false
SSH_KEY_PATH="/tmp/miab_migration_key"
SSH_PORT=22
CONFIG_PATH="/etc/mailinabox.conf"
# Mail-in-a-Box services
MIAB_SERVICES=(
"nginx"
"dovecot"
"postfix"
"opendkim"
"spamassassin"
"postgrey"
"clamav-daemon"
"clamav-freshclam"
"fail2ban"
"nsd"
"php8.1-fpm"
"redis-server"
"memcached"
"rspamd"
"unattended-upgrades"
"mailinabox-daemon"
)
# Text formatting
BOLD="\033[1m"
GREEN="\033[0;32m"
YELLOW="\033[0;33m"
RED="\033[0;31m"
NC="\033[0m" # No Color
# Function to display usage information
function show_usage {
echo -e "${BOLD}Mail-in-a-Box Migration Script${NC}"
echo -e "Transfers Mail-in-a-Box data from one server to another"
echo ""
echo -e "${BOLD}Usage:${NC}"
echo " $0 [options]"
echo ""
echo -e "${BOLD}Options:${NC}"
echo " --username USERNAME SSH username for target server (required)"
echo " --target-host HOST IP address or domain of target server (required)"
echo " --source-path PATH Path to Mail-in-a-Box files (default: /home/user-data)"
echo " --target-path PATH Destination path on target server (default: /home/user-data)"
echo " --config-path PATH Path to mailinabox.conf file (default: /etc/mailinabox.conf)"
echo " --exclude LIST Comma-separated list of files/folders to exclude"
echo " --new-ip IP Public IP address of the new target machine (required)"
echo " --ssh-port PORT SSH port to use (default: 22)"
echo " --stop-services Stop Mail-in-a-Box services during transfer (default: keep running)"
echo " --ignore-partial Continue migration even if some files fail to transfer (rsync code 23)"
echo " --dry-run Simulate the transfer without making changes"
echo " --help Display this help message"
echo ""
echo -e "${BOLD}Example:${NC}"
echo " $0 --username admin --target-host 192.168.1.100 --new-ip 203.0.113.10 --exclude 'backup,logs'"
exit 1
}
# Function to check if a command exists
function command_exists {
command -v "$1" >/dev/null 2>&1
}
# Function to check required dependencies
function check_dependencies {
local missing_deps=false
if ! command_exists rsync; then
echo -e "${RED}Error: rsync is not installed. Please install it and try again.${NC}"
missing_deps=true
fi
if ! command_exists ssh; then
echo -e "${RED}Error: ssh is not installed. Please install it and try again.${NC}"
missing_deps=true
fi
if ! command_exists ssh-keygen; then
echo -e "${RED}Error: ssh-keygen is not installed. Please install it and try again.${NC}"
missing_deps=true
fi
if [ "$missing_deps" = true ]; then
exit 1
fi
}
# Function to validate IP address
function validate_ip {
local ip=$1
local stat=1
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
IFS='.' read -r -a ip_segments <<< "$ip"
[[ ${ip_segments[0]} -le 255 && ${ip_segments[1]} -le 255 && \
${ip_segments[2]} -le 255 && ${ip_segments[3]} -le 255 ]]
stat=$?
fi
return $stat
}
# Function to generate SSH key pair
function generate_ssh_key {
echo -e "\n${YELLOW}Generating temporary SSH key pair for migration...${NC}"
# Remove old keys if they exist
rm -f "${SSH_KEY_PATH}" "${SSH_KEY_PATH}.pub"
# Generate new key pair without passphrase
ssh-keygen -t ed25519 -f "${SSH_KEY_PATH}" -N "" -C "miab_migration_$(date +%Y%m%d)" >/dev/null 2>&1
echo -e "\n${GREEN}SSH key pair generated successfully.${NC}"
echo -e "\n${BOLD}Please add the following public key to ~/.ssh/authorized_keys on the target server:${NC}"
echo -e "\n${YELLOW}$(cat "${SSH_KEY_PATH}.pub")${NC}\n"
}
# Function to test SSH connection
function test_ssh_connection {
echo -e "${YELLOW}Testing SSH connection to $USERNAME@$TARGET_HOST...${NC}"
if ssh -o BatchMode=yes -o ConnectTimeout=5 -p "$SSH_PORT" -i "${SSH_KEY_PATH}" "$USERNAME@$TARGET_HOST" exit >/dev/null 2>&1; then
echo -e "${GREEN}SSH connection successful.${NC}"
return 0
else
echo -e "${RED}SSH connection failed. Please ensure the public key is added to authorized_keys on the target server.${NC}"
return 1
fi
}
# Function to sync files using rsync
function sync_files {
local rsync_options="-avz --progress"
local exclude_options=""
# Add trailing slash to source path if not present
[[ "$SOURCE_PATH" != */ ]] && SOURCE_PATH="${SOURCE_PATH}/"
# Process exclude list
if [ -n "$EXCLUDE" ]; then
IFS=',' read -ra EXCLUDE_ARRAY <<< "$EXCLUDE"
for item in "${EXCLUDE_ARRAY[@]}"; do
exclude_options="$exclude_options --exclude='$item'"
done
fi
# Add dry-run option if specified
if [ "$DRY_RUN" = true ]; then
rsync_options="$rsync_options --dry-run"
echo -e "\n${YELLOW}Running in DRY RUN mode. No files will be transferred.${NC}"
fi
echo -e "\n${YELLOW}Starting file synchronization...${NC}"
echo -e "From: $SOURCE_PATH"
echo -e "To: $USERNAME@$TARGET_HOST:$TARGET_PATH"
# Execute rsync command
eval rsync $rsync_options $exclude_options -e "ssh -p $SSH_PORT -i ${SSH_KEY_PATH}" "$SOURCE_PATH" "$USERNAME@$TARGET_HOST:$TARGET_PATH"
local rsync_result=$?
if [ $rsync_result -eq 0 ]; then
echo -e "\n${GREEN}File synchronization completed successfully.${NC}"
return 0
elif [ $rsync_result -eq 23 ]; then
# Error code 23 means partial transfer - some files were not transferred
# but the overall transfer was mostly successful
echo -e "\n${YELLOW}Warning: Partial file synchronization (code 23).${NC}"
echo -e "${YELLOW}Some files or attributes were not transferred, but most files were synchronized successfully.${NC}"
echo -e "${YELLOW}This is often due to permission issues, open files, or special files that cannot be transferred.${NC}"
echo -e "${YELLOW}Would you like to continue with the migration? (y/n)${NC}"
read -p "Continue? (y/n): " -r
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo -e "${GREEN}Continuing with migration despite partial transfer...${NC}"
return 0
else
echo -e "${RED}Migration aborted due to partial transfer.${NC}"
return 1
fi
else
echo -e "\n${RED}File synchronization failed with exit code $rsync_result.${NC}"
return 1
fi
}
# Function to update mailinabox.conf file
function update_mailinabox_conf {
local temp_conf="/tmp/mailinabox.conf.tmp"
local source_conf="${CONFIG_PATH}"
echo -e "\n${YELLOW}Updating mailinabox.conf with new IP address...${NC}"
# Check if mailinabox.conf exists
if [ ! -f "$source_conf" ]; then
echo -e "${RED}Error: mailinabox.conf not found at $source_conf${NC}"
echo -e "${YELLOW}You can specify the correct path using --config-path option${NC}"
return 1
fi
# Create a temporary copy of the config file
if ! cp "$source_conf" "$temp_conf"; then
echo -e "${RED}Error: Failed to create temporary copy of mailinabox.conf${NC}"
return 1
fi
# Update the PUBLIC_IP value
if ! sed -i "s/^PUBLIC_IP=.*/PUBLIC_IP=$NEW_IP/" "$temp_conf"; then
echo -e "${RED}Error: Failed to update PUBLIC_IP in mailinabox.conf${NC}"
rm -f "$temp_conf"
return 1
fi
echo -e "${GREEN}mailinabox.conf updated locally.${NC}"
# Transfer the updated config file to the target server
if [ "$DRY_RUN" = false ]; then
if ! scp -P "$SSH_PORT" -i "${SSH_KEY_PATH}" "$temp_conf" "$USERNAME@$TARGET_HOST:$CONFIG_PATH"; then
echo -e "${RED}Failed to transfer updated mailinabox.conf to target server.${NC}"
rm -f "$temp_conf"
return 1
fi
echo -e "${GREEN}Updated mailinabox.conf transferred to target server.${NC}"
else
echo -e "${YELLOW}DRY RUN: Would transfer updated mailinabox.conf to target server.${NC}"
fi
# Clean up temporary file
rm -f "$temp_conf"
return 0
}
# Function to stop Mail-in-a-Box services
function stop_miab_services {
echo -e "\n${YELLOW}Stopping Mail-in-a-Box services on source server...${NC}"
local failed_services=""
for service in "${MIAB_SERVICES[@]}"; do
echo -e "Stopping $service..."
if ! systemctl stop "$service" 2>/dev/null; then
echo -e "${YELLOW}Warning: Failed to stop $service${NC}"
failed_services="$failed_services $service"
fi
done
if [ -z "$failed_services" ]; then
echo -e "${GREEN}All Mail-in-a-Box services stopped successfully.${NC}"
SERVICES_STOPPED=true
return 0
else
echo -e "${YELLOW}Warning: Failed to stop some services:${NC}$failed_services"
echo -e "${YELLOW}Continuing anyway...${NC}"
SERVICES_STOPPED=true
return 0
fi
}
# Function to start Mail-in-a-Box services
function start_miab_services {
echo -e "\n${YELLOW}Starting Mail-in-a-Box services on source server...${NC}"
local failed_services=""
for service in "${MIAB_SERVICES[@]}"; do
echo -e "Starting $service..."
if ! systemctl start "$service" 2>/dev/null; then
echo -e "${YELLOW}Warning: Failed to start $service${NC}"
failed_services="$failed_services $service"
fi
done
if [ -z "$failed_services" ]; then
echo -e "${GREEN}All Mail-in-a-Box services started successfully.${NC}"
SERVICES_STOPPED=false
return 0
else
echo -e "${YELLOW}Warning: Failed to start some services:${NC}$failed_services"
echo -e "${YELLOW}Some manual intervention may be required.${NC}"
SERVICES_STOPPED=false
return 1
fi
}
# Function to handle errors and cleanup
function handle_error {
local error_message=$1
echo -e "\n${RED}ERROR: $error_message${NC}"
# Restart services if they were stopped
if [ "$STOP_SERVICES" = true ] && [ "$SERVICES_STOPPED" = true ]; then
echo -e "\n${YELLOW}Attempting to restart Mail-in-a-Box services due to error...${NC}"
start_miab_services || echo -e "${RED}Failed to restart some services. Manual intervention may be required.${NC}"
fi
# Clean up temporary files
cleanup
exit 1
}
# Function to clean up temporary files
function cleanup {
echo -e "\n${YELLOW}Cleaning up temporary files...${NC}"
rm -f "${SSH_KEY_PATH}" "${SSH_KEY_PATH}.pub"
echo -e "${GREEN}Temporary SSH keys removed.${NC}"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
--username)
USERNAME="$2"
shift 2
;;
--target-host)
TARGET_HOST="$2"
shift 2
;;
--source-path)
SOURCE_PATH="$2"
shift 2
;;
--target-path)
TARGET_PATH="$2"
shift 2
;;
--config-path)
CONFIG_PATH="$2"
shift 2
;;
--exclude)
EXCLUDE="$2"
shift 2
;;
--new-ip)
NEW_IP="$2"
shift 2
;;
--ssh-port)
SSH_PORT="$2"
shift 2
;;
--stop-services)
STOP_SERVICES=true
shift
;;
--ignore-partial)
IGNORE_PARTIAL=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--help)
show_usage
;;
*)
echo -e "${RED}Error: Unknown option $1${NC}"
show_usage
;;
esac
done
# Check required parameters
if [ -z "$USERNAME" ] || [ -z "$TARGET_HOST" ] || [ -z "$NEW_IP" ]; then
echo -e "${RED}Error: Missing required parameters.${NC}"
show_usage
fi
# Validate IP address
if ! validate_ip "$NEW_IP"; then
echo -e "${RED}Error: Invalid IP address format: $NEW_IP${NC}"
exit 1
fi
# Check dependencies
check_dependencies
# Display migration information
echo -e "\n${BOLD}Mail-in-a-Box Migration${NC}"
echo -e "Source path: $SOURCE_PATH"
echo -e "Target server: $USERNAME@$TARGET_HOST:$TARGET_PATH"
echo -e "Config file: $CONFIG_PATH"
echo -e "New IP address: $NEW_IP"
if [ -n "$EXCLUDE" ]; then
echo -e "Excluding: $EXCLUDE"
fi
if [ "$STOP_SERVICES" = true ]; then
echo -e "Services: ${YELLOW}Will be stopped during transfer${NC}"
else
echo -e "Services: ${GREEN}Will remain running during transfer${NC}"
fi
if [ "$IGNORE_PARTIAL" = true ]; then
echo -e "Partial transfers: ${YELLOW}Will be ignored${NC}"
fi
if [ "$DRY_RUN" = true ]; then
echo -e "Mode: ${YELLOW}DRY RUN${NC}"
fi
# Generate SSH key pair
if ! generate_ssh_key; then
handle_error "Failed to generate SSH key pair"
fi
# Wait for user to add the SSH key to the target server
echo -e "\n${BOLD}Please add the above public key to the authorized_keys file on the target server.${NC}"
echo -e "You can use the following command on the target server:"
echo -e "${YELLOW}echo '$(cat "${SSH_KEY_PATH}.pub")' >> ~/.ssh/authorized_keys${NC}"
read -p "Press Enter once you have added the key to continue... " -r
# Test SSH connection
while ! test_ssh_connection; do
read -p "Would you like to try again? (y/n): " -r
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo -e "${RED}Migration aborted.${NC}"
cleanup
exit 1
fi
done
# Stop Mail-in-a-Box services on source server if requested
if [ "$STOP_SERVICES" = true ]; then
if [ "$DRY_RUN" = false ]; then
echo -e "\n${BOLD}The script will now stop all Mail-in-a-Box services on the source server.${NC}"
read -p "Press Enter to continue or Ctrl+C to abort... " -r
if ! stop_miab_services; then
handle_error "Failed to stop Mail-in-a-Box services"
fi
else
echo -e "\n${YELLOW}DRY RUN: Would stop all Mail-in-a-Box services on the source server.${NC}"
fi
else
echo -e "\n${YELLOW}Note: Mail-in-a-Box services will remain running during transfer.${NC}"
echo -e "${YELLOW}If you experience issues, consider using the --stop-services option.${NC}"
fi
# Sync files
if ! sync_files; then
if [ "$IGNORE_PARTIAL" = true ]; then
echo -e "${YELLOW}Continuing despite file sync issues (--ignore-partial flag is set)${NC}"
else
handle_error "Failed to sync files"
fi
fi
# Update mailinabox.conf
if ! update_mailinabox_conf; then
handle_error "Failed to update mailinabox.conf"
fi
# Start Mail-in-a-Box services on source server if they were stopped
if [ "$STOP_SERVICES" = true ] && [ "$SERVICES_STOPPED" = true ]; then
if [ "$DRY_RUN" = false ]; then
echo -e "\n${BOLD}The script will now restart all Mail-in-a-Box services on the source server.${NC}"
read -p "Press Enter to continue or Ctrl+C to abort... " -r
if ! start_miab_services; then
echo -e "${YELLOW}Warning: Some services failed to start. Manual intervention may be required.${NC}"
# Continue execution despite service start failures
fi
else
echo -e "\n${YELLOW}DRY RUN: Would restart all Mail-in-a-Box services on the source server.${NC}"
fi
fi
# Final message
if [ "$DRY_RUN" = false ]; then
echo -e "\n${GREEN}${BOLD}Mail-in-a-Box migration completed successfully!${NC}"
echo -e "\n${YELLOW}IMPORTANT: ${RED}You MUST reinstall Mail-in-a-Box on the target server${YELLOW} to ensure proper configuration.${NC}"
echo -e "The data has been transferred, but Mail-in-a-Box needs to be reinstalled to update all configurations."
echo -e "\n${YELLOW}Next steps:${NC}"
echo -e "1. SSH into the target server: ssh $USERNAME@$TARGET_HOST"
echo -e "2. Reinstall Mail-in-a-Box on the target server: curl -s https://mailinabox.email/setup.sh | sudo bash"
echo -e "3. Update DNS records to point to the new server IP: $NEW_IP"
echo -e "4. Test mail functionality on the new server"
echo -e "\n${YELLOW}Note:${NC} During reinstallation, Mail-in-a-Box will detect existing data and preserve it."
else
echo -e "\n${YELLOW}${BOLD}Mail-in-a-Box dry run completed. No changes were made.${NC}"
echo -e "Run the command without --dry-run to perform the actual migration."
fi
# Clean up
cleanup
exit 0