secubox-openwrt/package/secubox/secubox-app-mailinabox/files/usr/sbin/mailinaboxctl
CyberMind-FR b671843132 feat(mailinabox): Major enhancement to mail server package v2.0.0
Complete rewrite of mailinaboxctl with comprehensive features:

Container Management:
- install, check, update, status, logs, shell commands
- Better prerequisite checking and Docker integration

Email Account Management:
- user-add/del/list/passwd for email accounts
- alias-add/del/list for email aliases
- Uses docker-mailserver setup command

Domain & SSL:
- domain-add/list for virtual domains
- ssl-status/renew for certificate management
- Let's Encrypt integration

Backup & Restore:
- Full backup with automatic container stop
- Restore with confirmation prompt

Diagnostics:
- health: comprehensive health check
- dns-check: verify MX, SPF, DMARC records
- ports: check listening ports
- config: show current configuration
- test-email: send test message

Updated configuration with:
- Separate hostname and domain options
- Feature flags for ClamAV, SpamAssassin, Fail2ban, POP3
- SSL type selection (letsencrypt, manual, self-signed)
- Complete port mapping options

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 06:12:23 +01:00

726 lines
19 KiB
Bash
Executable File

#!/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 <command>
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 <email> [password] Add email account
user-del <email> Remove email account
user-list List all email accounts
user-passwd <email> Change user password
alias-add <alias> <target> Add email alias
alias-del <alias> Remove email alias
alias-list List all aliases
Domain & SSL:
domain-add <domain> 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 <backup-file> 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 <to> 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 <email> [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 <email>"; 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 <email>"; 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 <alias> <target>"; return 1; }
docker_setup alias add "$alias" "$target"
}
cmd_alias_del() {
local alias="$1"
[ -z "$alias" ] && { log_error "Usage: mailinaboxctl alias-del <alias>"; 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 <domain>"; 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 <backup-file>"
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 <to-address>"; 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