#!/bin/sh # SecuBox Mail-in-a-Box manager - Docker Mailserver Edition # Copyright (C) 2024 CyberMind.fr # # Based on docker-mailserver for lightweight email hosting CONFIG="mailinabox" CONTAINER_NAME="secbx-mailserver" OPKG_UPDATED=0 # Paths DATA_BASE="/srv/mailserver" usage() { cat <<'EOF' Usage: mailinaboxctl Container Management: install Install prerequisites, prepare directories, pull image check Run prerequisite checks (ports, DNS, storage) update Pull new image and restart status Show container and service status logs Show container logs (use -f to follow) shell Open shell in container service-run Internal: run container via procd service-stop Stop container Email Account Management: user-add [password] Add email account user-del Remove email account user-list List all email accounts user-passwd Change user password alias-add Add email alias alias-del Remove email alias alias-list List all aliases Domain & SSL: domain-add Add email domain domain-list List configured domains ssl-status Show SSL certificate status ssl-renew Force SSL certificate renewal Backup & Restore: backup [path] Backup mail data and config restore Restore from backup Diagnostics: health Run health checks dns-check [domain] Verify DNS records for domain ports Check required ports config Show current configuration test-email Send test email Post-Installation: 1. Configure hostname and domain in /etc/config/mailinabox 2. Set proper DNS records (A, MX, SPF, DKIM, DMARC) 3. Start with: /etc/init.d/mailinabox start 4. Add users with: mailinaboxctl user-add admin@yourdomain.com Required DNS Records: A mail.domain.com -> your-public-ip MX domain.com -> mail.domain.com (priority 10) TXT domain.com -> "v=spf1 mx -all" TXT _dmarc.domain.com -> "v=DMARC1; p=quarantine" TXT mail._domainkey.domain.com -> (DKIM key from container) EOF } require_root() { [ "$(id -u)" -eq 0 ] || { echo "Root required" >&2; exit 1; }; } log_info() { echo "[INFO] $*"; } log_warn() { echo "[WARN] $*" >&2; } log_error() { echo "[ERROR] $*" >&2; } uci_get() { uci -q get ${CONFIG}.main.$1; } uci_set() { uci set ${CONFIG}.main.$1="$2" && uci commit ${CONFIG}; } # Load configuration with defaults load_config() { enabled="$(uci_get enabled || echo 0)" image="$(uci_get image || echo ghcr.io/docker-mailserver/docker-mailserver:latest)" data_path="$(uci_get data_path || echo /srv/mailserver)" hostname="$(uci_get hostname || echo mail.example.com)" domain="$(uci_get domain || echo example.com)" timezone="$(uci_get timezone || cat /etc/TZ 2>/dev/null || echo UTC)" # Ports smtp_port="$(uci_get smtp_port || echo 25)" submission_port="$(uci_get submission_port || echo 587)" submissions_port="$(uci_get submissions_port || echo 465)" imap_port="$(uci_get imap_port || echo 143)" imaps_port="$(uci_get imaps_port || echo 993)" pop3_port="$(uci_get pop3_port || echo 110)" pop3s_port="$(uci_get pop3s_port || echo 995)" # Features enable_pop3="$(uci_get enable_pop3 || echo 0)" enable_clamav="$(uci_get enable_clamav || echo 0)" enable_spamassassin="$(uci_get enable_spamassassin || echo 1)" enable_fail2ban="$(uci_get enable_fail2ban || echo 1)" ssl_type="$(uci_get ssl_type || echo letsencrypt)" letsencrypt_email="$(uci_get letsencrypt_email)" } ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } # ============================================================================= # Docker Functions # ============================================================================= ensure_packages() { for pkg in "$@"; do if ! opkg list-installed 2>/dev/null | grep -q "^$pkg "; then if [ "$OPKG_UPDATED" -eq 0 ]; then opkg update || return 1 OPKG_UPDATED=1 fi opkg install "$pkg" || return 1 fi done } docker_ready() { command -v docker >/dev/null 2>&1 && [ -S /var/run/docker.sock ] } check_prereqs() { load_config log_info "Checking prerequisites..." # Check hostname configuration if [ "$hostname" = "mail.example.com" ] || [ "$domain" = "example.com" ]; then log_warn "Please configure hostname and domain in /etc/config/mailinabox" log_warn "docker-mailserver requires a valid domain name" fi # Check cgroups [ -d /sys/fs/cgroup ] || { log_error "/sys/fs/cgroup missing"; return 1; } # Install Docker ensure_packages dockerd docker containerd || return 1 # Enable and start Docker /etc/init.d/dockerd enable >/dev/null 2>&1 if ! /etc/init.d/dockerd status >/dev/null 2>&1; then /etc/init.d/dockerd start || return 1 sleep 3 fi # Wait for Docker socket local retry=0 while [ ! -S /var/run/docker.sock ] && [ $retry -lt 30 ]; do sleep 1 retry=$((retry + 1)) done [ -S /var/run/docker.sock ] || { log_error "Docker socket not available"; return 1; } # Create data directories ensure_dir "$data_path" ensure_dir "$data_path/mail-data" ensure_dir "$data_path/mail-state" ensure_dir "$data_path/mail-logs" ensure_dir "$data_path/config" log_info "Docker ready, directories created" return 0 } pull_image() { load_config log_info "Pulling Docker image: $image" docker pull "$image" } stop_container() { docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true docker rm "$CONTAINER_NAME" >/dev/null 2>&1 || true } container_running() { docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$" } # Execute setup.sh in container docker_setup() { if ! container_running; then log_error "Container not running. Start with: /etc/init.d/mailinabox start" return 1 fi docker exec -it "$CONTAINER_NAME" setup "$@" } # ============================================================================= # User Management Commands # ============================================================================= cmd_user_add() { local email="$1" local password="$2" [ -z "$email" ] && { log_error "Usage: mailinaboxctl user-add [password]"; return 1; } if [ -n "$password" ]; then docker_setup email add "$email" "$password" else docker_setup email add "$email" fi } cmd_user_del() { local email="$1" [ -z "$email" ] && { log_error "Usage: mailinaboxctl user-del "; return 1; } docker_setup email del "$email" } cmd_user_list() { docker_setup email list } cmd_user_passwd() { local email="$1" [ -z "$email" ] && { log_error "Usage: mailinaboxctl user-passwd "; return 1; } docker_setup email update "$email" } cmd_alias_add() { local alias="$1" local target="$2" [ -z "$alias" ] || [ -z "$target" ] && { log_error "Usage: mailinaboxctl alias-add "; return 1; } docker_setup alias add "$alias" "$target" } cmd_alias_del() { local alias="$1" [ -z "$alias" ] && { log_error "Usage: mailinaboxctl alias-del "; return 1; } docker_setup alias del "$alias" } cmd_alias_list() { docker_setup alias list } # ============================================================================= # Domain & SSL Commands # ============================================================================= cmd_domain_add() { local domain="$1" [ -z "$domain" ] && { log_error "Usage: mailinaboxctl domain-add "; return 1; } # Create virtual domain entry load_config local vhost_file="$data_path/config/postfix-virtual.cf" if ! grep -q "^$domain" "$vhost_file" 2>/dev/null; then echo "$domain" >> "$vhost_file" log_info "Domain $domain added. Restart service to apply." else log_warn "Domain $domain already exists" fi } cmd_domain_list() { load_config log_info "Configured domains:" if [ -f "$data_path/config/postfix-virtual.cf" ]; then cat "$data_path/config/postfix-virtual.cf" | grep -v "^#" | grep -v "^$" fi echo "" echo "Primary domain: $domain" echo "Mail hostname: $hostname" } cmd_ssl_status() { load_config log_info "SSL Configuration:" echo " Type: $ssl_type" if container_running; then docker_setup debug show-mail-logs | grep -i "ssl\|cert\|tls" | tail -20 fi # Check certificate files if [ -d "$data_path/config/ssl" ]; then echo "" echo "Certificate files:" ls -la "$data_path/config/ssl/" 2>/dev/null || echo " No certificates found" fi } cmd_ssl_renew() { load_config if [ "$ssl_type" = "letsencrypt" ]; then log_info "Triggering Let's Encrypt renewal..." if container_running; then docker exec "$CONTAINER_NAME" certbot renew else log_error "Container not running" fi else log_warn "SSL type is not letsencrypt" fi } # ============================================================================= # Backup & Restore Commands # ============================================================================= cmd_backup() { require_root load_config local backup_path="${1:-/tmp}" local timestamp=$(date +%Y%m%d_%H%M%S) local backup_file="$backup_path/mailserver_backup_$timestamp.tar.gz" log_info "Creating backup..." # Stop container for consistent backup local was_running=0 if container_running; then was_running=1 log_info "Stopping container for backup..." stop_container fi # Create backup tar czf "$backup_file" -C "$(dirname $data_path)" "$(basename $data_path)" 2>/dev/null || { log_error "Backup failed" [ $was_running -eq 1 ] && /etc/init.d/mailinabox start return 1 } # Restart if was running [ $was_running -eq 1 ] && /etc/init.d/mailinabox start log_info "Backup created: $backup_file" ls -lh "$backup_file" } cmd_restore() { require_root load_config local backup_file="$1" [ -z "$backup_file" ] || [ ! -f "$backup_file" ] && { log_error "Usage: mailinaboxctl restore " return 1 } log_warn "This will OVERWRITE existing mail data!" echo -n "Continue? [y/N] " read answer [ "$answer" != "y" ] && [ "$answer" != "Y" ] && { echo "Aborted"; return 1; } # Stop container if container_running; then log_info "Stopping container..." stop_container fi # Remove existing data log_info "Removing existing data..." rm -rf "$data_path" # Restore log_info "Restoring from backup..." tar xzf "$backup_file" -C "$(dirname $data_path)" || { log_error "Restore failed" return 1 } log_info "Restore complete. Start service with: /etc/init.d/mailinabox start" } # ============================================================================= # Diagnostic Commands # ============================================================================= cmd_health() { load_config echo "=== Mail Server Health Check ===" echo "" # Container status echo "Container Status:" if container_running; then echo " [OK] Container is running" local uptime=$(docker inspect --format='{{.State.StartedAt}}' "$CONTAINER_NAME" 2>/dev/null) echo " Started: $uptime" else echo " [FAIL] Container is not running" fi echo "" # Port checks echo "Port Status:" for port in $smtp_port $submission_port $imaps_port; do if netstat -tln 2>/dev/null | grep -q ":$port "; then echo " [OK] Port $port is listening" else echo " [WARN] Port $port is not listening" fi done echo "" # Service checks inside container if container_running; then echo "Services Status:" docker exec "$CONTAINER_NAME" supervisorctl status 2>/dev/null || echo " Unable to check services" fi echo "" # Disk usage echo "Disk Usage:" if [ -d "$data_path" ]; then du -sh "$data_path" 2>/dev/null du -sh "$data_path"/* 2>/dev/null | head -10 fi } cmd_dns_check() { load_config local check_domain="${1:-$domain}" echo "=== DNS Check for $check_domain ===" echo "" # A record echo "A Record (mail.$check_domain):" local a_record=$(nslookup "mail.$check_domain" 2>/dev/null | grep -A1 "Name:" | tail -1) if [ -n "$a_record" ]; then echo " [OK] $a_record" else echo " [FAIL] No A record found" fi echo "" # MX record echo "MX Record ($check_domain):" local mx_record=$(nslookup -type=mx "$check_domain" 2>/dev/null | grep "mail exchanger") if [ -n "$mx_record" ]; then echo " [OK] $mx_record" else echo " [FAIL] No MX record found" fi echo "" # SPF record echo "SPF Record ($check_domain):" local spf=$(nslookup -type=txt "$check_domain" 2>/dev/null | grep "v=spf1") if [ -n "$spf" ]; then echo " [OK] $spf" else echo " [WARN] No SPF record found" echo " Recommended: \"v=spf1 mx -all\"" fi echo "" # DMARC record echo "DMARC Record (_dmarc.$check_domain):" local dmarc=$(nslookup -type=txt "_dmarc.$check_domain" 2>/dev/null | grep "v=DMARC1") if [ -n "$dmarc" ]; then echo " [OK] $dmarc" else echo " [WARN] No DMARC record found" echo " Recommended: \"v=DMARC1; p=quarantine; rua=mailto:postmaster@$check_domain\"" fi } cmd_ports() { load_config echo "=== Port Status ===" echo "" echo "Required ports for mail server:" echo "" local ports="25:SMTP 587:Submission 465:SMTPS 143:IMAP 993:IMAPS" [ "$enable_pop3" = "1" ] && ports="$ports 110:POP3 995:POP3S" for entry in $ports; do local port=$(echo "$entry" | cut -d: -f1) local name=$(echo "$entry" | cut -d: -f2) printf " %-6s %-12s " "$port" "$name" if netstat -tln 2>/dev/null | grep -q ":$port "; then echo "[LISTENING]" else echo "[NOT LISTENING]" fi done echo "" echo "Note: Port 25 may be blocked by some ISPs" } cmd_config() { load_config echo "=== Mail Server Configuration ===" echo "" echo "General:" echo " Enabled: $enabled" echo " Image: $image" echo " Hostname: $hostname" echo " Domain: $domain" echo " Data path: $data_path" echo " Timezone: $timezone" echo "" echo "Features:" echo " SpamAssassin: $enable_spamassassin" echo " ClamAV: $enable_clamav" echo " Fail2ban: $enable_fail2ban" echo " POP3: $enable_pop3" echo " SSL Type: $ssl_type" echo "" echo "Ports:" echo " SMTP: $smtp_port" echo " Submission: $submission_port" echo " IMAPS: $imaps_port" } cmd_test_email() { local to="$1" [ -z "$to" ] && { log_error "Usage: mailinaboxctl test-email "; return 1; } load_config if ! container_running; then log_error "Container not running" return 1 fi log_info "Sending test email to $to..." docker exec "$CONTAINER_NAME" sh -c "echo 'Test email from SecuBox Mail Server' | mail -s 'Test from $hostname' $to" log_info "Test email sent (check spam folder if not received)" } # ============================================================================= # Main Container Commands # ============================================================================= cmd_install() { require_root check_prereqs || exit 1 pull_image || exit 1 uci_set enabled '1' uci commit ${CONFIG} /etc/init.d/mailinabox enable load_config echo "" log_info "Mail server installed successfully!" echo "" echo "NEXT STEPS:" echo " 1. Edit /etc/config/mailinabox and configure:" echo " - hostname (e.g., mail.yourdomain.com)" echo " - domain (e.g., yourdomain.com)" echo " 2. Set up DNS records (see 'mailinaboxctl dns-check')" echo " 3. Start: /etc/init.d/mailinabox start" echo " 4. Add first user: mailinaboxctl user-add admin@$domain" echo "" } cmd_check() { check_prereqs echo "" cmd_config echo "" cmd_ports } cmd_update() { require_root pull_image || exit 1 if container_running; then /etc/init.d/mailinabox restart else log_info "Image updated. Start manually when ready." fi } cmd_status() { echo "=== Container Status ===" docker ps -a --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" echo "" if container_running; then echo "=== Service Status ===" docker exec "$CONTAINER_NAME" supervisorctl status 2>/dev/null || true fi } cmd_logs() { docker logs "$@" "$CONTAINER_NAME" } cmd_shell() { if container_running; then docker exec -it "$CONTAINER_NAME" /bin/bash else log_error "Container not running" fi } cmd_service_run() { require_root check_prereqs || exit 1 load_config stop_container log_info "Starting mail server container..." # Build docker run command local docker_args="--name $CONTAINER_NAME" # Hostname docker_args="$docker_args --hostname $hostname" docker_args="$docker_args --domainname $domain" # Ports docker_args="$docker_args -p $smtp_port:25" docker_args="$docker_args -p $submission_port:587" docker_args="$docker_args -p $submissions_port:465" docker_args="$docker_args -p $imap_port:143" docker_args="$docker_args -p $imaps_port:993" if [ "$enable_pop3" = "1" ]; then docker_args="$docker_args -p $pop3_port:110" docker_args="$docker_args -p $pop3s_port:995" fi # Volumes docker_args="$docker_args -v $data_path/mail-data:/var/mail" docker_args="$docker_args -v $data_path/mail-state:/var/mail-state" docker_args="$docker_args -v $data_path/mail-logs:/var/log/mail" docker_args="$docker_args -v $data_path/config:/tmp/docker-mailserver" # Let's Encrypt volume if using certbot if [ "$ssl_type" = "letsencrypt" ]; then ensure_dir "$data_path/letsencrypt" docker_args="$docker_args -v $data_path/letsencrypt:/etc/letsencrypt" fi # Environment variables docker_args="$docker_args -e TZ=$timezone" docker_args="$docker_args -e OVERRIDE_HOSTNAME=$hostname" docker_args="$docker_args -e ENABLE_SPAMASSASSIN=$enable_spamassassin" docker_args="$docker_args -e ENABLE_CLAMAV=$enable_clamav" docker_args="$docker_args -e ENABLE_FAIL2BAN=$enable_fail2ban" docker_args="$docker_args -e ENABLE_POP3=$enable_pop3" docker_args="$docker_args -e SSL_TYPE=$ssl_type" docker_args="$docker_args -e PERMIT_DOCKER=network" docker_args="$docker_args -e ONE_DIR=1" docker_args="$docker_args -e POSTMASTER_ADDRESS=postmaster@$domain" if [ -n "$letsencrypt_email" ]; then docker_args="$docker_args -e LETSENCRYPT_EMAIL=$letsencrypt_email" fi # Capabilities docker_args="$docker_args --cap-add=NET_ADMIN" docker_args="$docker_args --cap-add=SYS_PTRACE" # Restart policy docker_args="$docker_args --restart=unless-stopped" exec docker run --rm $docker_args "$image" } cmd_service_stop() { require_root stop_container } # ============================================================================= # Main Entry Point # ============================================================================= case "${1:-}" in # Container management install) shift; cmd_install "$@" ;; check) shift; cmd_check "$@" ;; update) shift; cmd_update "$@" ;; status) shift; cmd_status "$@" ;; logs) shift; cmd_logs "$@" ;; shell) shift; cmd_shell "$@" ;; service-run) shift; cmd_service_run "$@" ;; service-stop) shift; cmd_service_stop "$@" ;; # User management user-add) shift; cmd_user_add "$@" ;; user-del) shift; cmd_user_del "$@" ;; user-list) shift; cmd_user_list "$@" ;; user-passwd) shift; cmd_user_passwd "$@" ;; alias-add) shift; cmd_alias_add "$@" ;; alias-del) shift; cmd_alias_del "$@" ;; alias-list) shift; cmd_alias_list "$@" ;; # Domain & SSL domain-add) shift; cmd_domain_add "$@" ;; domain-list) shift; cmd_domain_list "$@" ;; ssl-status) shift; cmd_ssl_status "$@" ;; ssl-renew) shift; cmd_ssl_renew "$@" ;; # Backup & restore backup) shift; cmd_backup "$@" ;; restore) shift; cmd_restore "$@" ;; # Diagnostics health) shift; cmd_health "$@" ;; dns-check) shift; cmd_dns_check "$@" ;; ports) shift; cmd_ports "$@" ;; config) shift; cmd_config "$@" ;; test-email) shift; cmd_test_email "$@" ;; help|--help|-h|'') usage ;; *) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;; esac