secubox-openwrt/package/secubox/secubox-app-mailserver/files/usr/sbin/mailserverctl
CyberMind-FR d43855b3d1 fix(mailserver): Use uid/gid 5000 for vmail user in Dovecot config
Fixes Roundcube IMAP "Internal error occurred" caused by Dovecot
running mail processes as uid 102 (Alpine default) instead of the
actual vmail user uid 5000.

Changes:
- configure_postfix: virtual_uid_maps/gid_maps 102/105 → 5000/5000
- configure_dovecot: mail_uid/gid, first_valid_uid, userdb args
- cmd_add_user: passwd file entries and ownership

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 07:11:43 +01:00

597 lines
15 KiB
Bash

#!/bin/sh
# SecuBox Mailserver Controller
# LXC-based Postfix + Dovecot mail server
VERSION="2.0.0"
CONFIG="mailserver"
CONTAINER="mailserver"
LXC_PATH="/srv/lxc/mailserver"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[MAILSERVER]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
# ============================================================================
# Configuration Helpers
# ============================================================================
uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci -q set ${CONFIG}.$1="$2"; }
require_root() {
[ "$(id -u)" -eq 0 ] || { error "Root required"; exit 1; }
}
defaults() {
ip_address="$(uci_get main.ip_address)"
[ -z "$ip_address" ] && ip_address="192.168.255.30"
domain="$(uci_get main.domain)"
[ -z "$domain" ] && domain="secubox.in"
hostname="$(uci_get main.hostname)"
[ -z "$hostname" ] && hostname="mail.$domain"
}
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
# ============================================================================
# LXC Helpers
# ============================================================================
lxc_running() {
lxc-info -n "$CONTAINER" 2>/dev/null | grep -q "State:.*RUNNING"
}
lxc_exists() {
[ -d "$LXC_PATH/rootfs" ]
}
create_lxc_config() {
defaults
cat > "$LXC_PATH/config" << EOF
lxc.uts.name = mailserver
lxc.rootfs.path = dir:${LXC_PATH}/rootfs
lxc.net.0.type = veth
lxc.net.0.link = br-lan
lxc.net.0.flags = up
lxc.net.0.ipv4.address = ${ip_address}/24
lxc.net.0.ipv4.gateway = 192.168.255.1
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.cap.drop = sys_module mac_admin mac_override sys_time
lxc.seccomp.profile =
lxc.tty.max = 0
lxc.pty.max = 256
lxc.cgroup2.memory.max = 512000000
lxc.init.cmd = /opt/start-mail.sh
EOF
}
create_startup_script() {
cat > "$LXC_PATH/rootfs/opt/start-mail.sh" << 'EOF'
#!/bin/sh
# Mailserver startup script
# Ensure dovenull is in dovecot group (fixes login directory access)
addgroup dovenull dovecot 2>/dev/null || true
# Ensure dovecot run directory exists with correct permissions
# This fixes anvil-auth-penalty socket permission issues
mkdir -p /run/dovecot /run/dovecot/login /run/dovecot/token-login /run/dovecot/empty
chown -R dovecot:dovecot /run/dovecot
chown root:dovenull /run/dovecot/login /run/dovecot/token-login
chmod 755 /run/dovecot
chmod 750 /run/dovecot/login /run/dovecot/token-login
# Remove stale auth token (prevents "compromised token" errors on restart)
rm -f /run/dovecot/auth-token-secret.dat
# Ensure dovecot users file is readable (fixes LMTP lookup errors)
[ -f /etc/dovecot/users ] && chmod 644 /etc/dovecot/users && chown root:dovecot /etc/dovecot/users
# Start services
/usr/sbin/rsyslogd
sleep 1
/usr/sbin/postfix start
/usr/sbin/dovecot
# Give dovecot a moment to create sockets, then fix permissions
sleep 2
chown -R dovecot:dovecot /run/dovecot
chown root:dovenull /run/dovecot/login /run/dovecot/token-login
echo "Mail services started"
# Keep container running
exec tail -f /var/log/dovecot.log /var/log/messages 2>/dev/null || exec sleep infinity
EOF
chmod +x "$LXC_PATH/rootfs/opt/start-mail.sh"
}
# ============================================================================
# Alpine Bootstrap
# ============================================================================
bootstrap_alpine() {
require_root
defaults
log "Bootstrapping Alpine Linux rootfs..."
ensure_dir "$LXC_PATH"
# Download apk-tools-static
cd "$LXC_PATH"
if [ ! -f sbin/apk.static ]; then
log "Downloading apk-tools-static..."
curl -L -o apk-tools-static.apk \
"https://dl-cdn.alpinelinux.org/alpine/v3.21/main/aarch64/apk-tools-static-2.14.6-r3.apk"
tar -xzf apk-tools-static.apk sbin/apk.static
rm -f apk-tools-static.apk
fi
# Bootstrap rootfs
log "Installing base system..."
./sbin/apk.static -X https://dl-cdn.alpinelinux.org/alpine/v3.21/main \
-U --allow-untrusted --root rootfs --initdb add \
alpine-base alpine-baselayout busybox musl openrc
# Set up repositories
mkdir -p rootfs/etc/apk
cat > rootfs/etc/apk/repositories << 'REPOEOF'
https://dl-cdn.alpinelinux.org/alpine/v3.21/main
https://dl-cdn.alpinelinux.org/alpine/v3.21/community
REPOEOF
# Set up DNS
cat > rootfs/etc/resolv.conf << 'DNSEOF'
nameserver 8.8.8.8
nameserver 1.1.1.1
DNSEOF
# Set hostname
echo "mailserver" > rootfs/etc/hostname
log "Base system installed"
}
install_mail_packages() {
require_root
if ! lxc_running; then
log "Starting container for package installation..."
lxc-start -n "$CONTAINER" -d
sleep 3
fi
log "Installing mail packages..."
lxc-attach -n "$CONTAINER" -- apk update
lxc-attach -n "$CONTAINER" -- apk add --no-cache \
postfix \
dovecot \
dovecot-lmtpd \
ca-certificates \
openssl \
rsyslog
log "Packages installed"
}
# ============================================================================
# Mail Configuration
# ============================================================================
configure_postfix() {
defaults
local rootfs="$LXC_PATH/rootfs"
log "Configuring Postfix..."
cat > "$rootfs/etc/postfix/main.cf" << EOF
# Basic config
myhostname = $hostname
mydomain = $domain
myorigin = \$mydomain
mydestination = \$myhostname, localhost.\$mydomain, localhost
mynetworks = 127.0.0.0/8 [::1]/128 192.168.255.0/24
# Virtual mailbox
virtual_mailbox_domains = $domain
virtual_mailbox_base = /var/mail
virtual_mailbox_maps = lmdb:/etc/postfix/vmailbox
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000
virtual_transport = lmtp:unix:private/dovecot-lmtp
# SASL auth via Dovecot
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = \$mydomain
broken_sasl_auth_clients = yes
# TLS
smtpd_tls_cert_file = /etc/ssl/certs/mail.crt
smtpd_tls_key_file = /etc/ssl/private/mail.key
smtpd_tls_security_level = may
smtp_tls_security_level = may
# Restrictions
smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination
smtpd_sender_restrictions = permit_sasl_authenticated, permit_mynetworks
# Limits
mailbox_size_limit = 0
message_size_limit = 52428800
inet_interfaces = all
inet_protocols = ipv4
EOF
cat > "$rootfs/etc/postfix/master.cf" << 'EOF'
smtp inet n - n - - smtpd
submission inet n - n - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
smtps inet n - n - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
pickup unix n - n 60 1 pickup
cleanup unix n - n - 0 cleanup
qmgr unix n - n 300 1 qmgr
tlsmgr unix - - n 1000? 1 tlsmgr
rewrite unix - - n - - trivial-rewrite
bounce unix - - n - 0 bounce
defer unix - - n - 0 bounce
trace unix - - n - 0 bounce
verify unix - - n - 1 verify
flush unix n - n 1000? 0 flush
proxymap unix - - n - - proxymap
proxywrite unix - - n - 1 proxymap
smtp unix - - n - - smtp
relay unix - - n - - smtp
showq unix n - n - - showq
error unix - - n - - error
retry unix - - n - - error
discard unix - - n - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - n - - lmtp
anvil unix - - n - 1 anvil
scache unix - - n - 1 scache
EOF
log "Postfix configured"
}
configure_dovecot() {
defaults
local rootfs="$LXC_PATH/rootfs"
log "Configuring Dovecot..."
cat > "$rootfs/etc/dovecot/dovecot.conf" << 'EOF'
protocols = imap lmtp
listen = *
mail_location = maildir:/var/mail/%d/%n
mail_uid = 5000
mail_gid = 5000
first_valid_uid = 500
last_valid_uid = 65534
# Auth
auth_mechanisms = plain login
passdb {
driver = passwd-file
args = /etc/dovecot/users
}
userdb {
driver = static
args = uid=5000 gid=5000 home=/var/mail/%d/%n
}
# SSL
ssl = yes
ssl_cert = </etc/ssl/certs/mail.crt
ssl_key = </etc/ssl/private/mail.key
# Services
service imap-login {
inet_listener imap {
port = 143
}
inet_listener imaps {
port = 993
ssl = yes
}
}
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
namespace inbox {
inbox = yes
separator = /
}
log_path = /var/log/dovecot.log
info_log_path = /var/log/dovecot-info.log
EOF
log "Dovecot configured"
}
generate_ssl_cert() {
local rootfs="$LXC_PATH/rootfs"
defaults
log "Generating SSL certificate..."
mkdir -p "$rootfs/etc/ssl/private" "$rootfs/etc/ssl/certs"
openssl req -x509 -nodes -days 3650 \
-newkey rsa:2048 \
-keyout "$rootfs/etc/ssl/private/mail.key" \
-out "$rootfs/etc/ssl/certs/mail.crt" \
-subj "/CN=$hostname/O=SecuBox/C=FR" 2>/dev/null
chmod 600 "$rootfs/etc/ssl/private/mail.key"
log "SSL certificate generated"
}
# ============================================================================
# User Management
# ============================================================================
cmd_add_user() {
local email="$1"
local password="$2"
if [ -z "$email" ] || [ -z "$password" ]; then
echo "Usage: mailserverctl add-user <email> <password>"
return 1
fi
local user=$(echo "$email" | cut -d@ -f1)
local domain=$(echo "$email" | cut -d@ -f2)
local rootfs="$LXC_PATH/rootfs"
# Create mailbox entry
echo "$email ${domain}/${user}/" >> "$rootfs/etc/postfix/vmailbox"
# Generate password hash and add to users file
if lxc_running; then
local pass_hash=$(lxc-attach -n "$CONTAINER" -- doveadm pw -s SHA512-CRYPT -p "$password")
echo "${email}:${pass_hash}:5000:5000::/var/mail/${domain}/${user}::" >> "$rootfs/etc/dovecot/users"
# Fix permissions (dovecot needs read access)
chmod 644 "$rootfs/etc/dovecot/users"
chown root:5000 "$rootfs/etc/dovecot/users"
else
error "Container not running. Start it first."
return 1
fi
# Create mail directory
mkdir -p "$rootfs/var/mail/${domain}/${user}"
lxc-attach -n "$CONTAINER" -- chown -R vmail:vmail "/var/mail/${domain}"
# Rebuild postfix maps
lxc-attach -n "$CONTAINER" -- postmap /etc/postfix/vmailbox
log "User $email added"
}
# ============================================================================
# Service Control
# ============================================================================
cmd_install() {
require_root
log "Installing Mailserver LXC..."
defaults
if lxc_exists; then
log "Container already exists"
else
bootstrap_alpine
fi
create_lxc_config
create_startup_script
# Start for package installation
lxc-start -n "$CONTAINER" -d
sleep 3
if lxc_running; then
install_mail_packages
lxc-stop -n "$CONTAINER"
fi
configure_postfix
configure_dovecot
generate_ssl_cert
# Create vmail user directories
local rootfs="$LXC_PATH/rootfs"
mkdir -p "$rootfs/var/mail"
mkdir -p "$rootfs/var/spool/postfix/private"
# Create minimal interfaces file
cat > "$rootfs/etc/network/interfaces" << 'EOF'
auto lo
iface lo inet loopback
EOF
uci_set main.enabled '1'
uci commit ${CONFIG}
log "Mailserver installed!"
log "Add users with: mailserverctl add-user user@domain.com password"
log "Start with: mailserverctl start"
}
cmd_start() {
require_root
if lxc_running; then
log "Mailserver already running"
return 0
fi
if ! lxc_exists; then
error "Mailserver not installed. Run 'mailserverctl install' first"
return 1
fi
defaults
create_lxc_config
log "Starting Mailserver LXC..."
lxc-start -n "$CONTAINER" -d
sleep 5
if lxc_running; then
log "Mailserver started at $ip_address"
log "IMAP: ${ip_address}:993 (SSL)"
log "SMTP: ${ip_address}:465 (SSL)"
else
error "Failed to start Mailserver"
return 1
fi
}
cmd_stop() {
require_root
if ! lxc_running; then
log "Mailserver is not running"
return 0
fi
log "Stopping Mailserver..."
lxc-stop -n "$CONTAINER"
log "Mailserver stopped"
}
cmd_restart() {
cmd_stop
sleep 2
cmd_start
}
cmd_status() {
defaults
echo ""
echo "========================================"
echo " SecuBox Mailserver v$VERSION (LXC)"
echo "========================================"
echo ""
echo "Configuration:"
echo " Domain: $domain"
echo " Hostname: $hostname"
echo " IP Address: $ip_address"
echo ""
echo "Container:"
if lxc_running; then
echo -e " Status: ${GREEN}Running${NC}"
local pid=$(lxc-info -n "$CONTAINER" 2>/dev/null | grep PID | awk '{print $2}')
echo " PID: $pid"
# Test IMAP
local imap_test=$(echo "a LOGOUT" | openssl s_client -connect ${ip_address}:993 -quiet 2>/dev/null | head -1)
if echo "$imap_test" | grep -q "OK"; then
echo -e " IMAP: ${GREEN}OK${NC}"
else
echo -e " IMAP: ${YELLOW}Not responding${NC}"
fi
elif lxc_exists; then
echo -e " Status: ${YELLOW}Stopped${NC}"
else
echo -e " Status: ${RED}Not installed${NC}"
fi
echo ""
echo "Ports:"
echo " SMTP: 25, 465 (SSL), 587 (submission)"
echo " IMAP: 143, 993 (SSL)"
echo ""
}
cmd_shell() {
if ! lxc_running; then
error "Container not running"
return 1
fi
lxc-attach -n "$CONTAINER" -- /bin/sh
}
# ============================================================================
# Main
# ============================================================================
show_help() {
cat << EOF
SecuBox Mailserver Control v$VERSION (LXC)
Usage: mailserverctl <command> [options]
Commands:
install Install LXC mail server
start Start mail server
stop Stop mail server
restart Restart mail server
status Show status
add-user <email> <pass> Add mail user
shell Open shell in container
Examples:
mailserverctl install
mailserverctl add-user admin@example.com MyPassword123
mailserverctl start
EOF
}
case "${1:-}" in
install) shift; cmd_install "$@" ;;
start) shift; cmd_start "$@" ;;
stop) shift; cmd_stop "$@" ;;
restart) shift; cmd_restart "$@" ;;
status) shift; cmd_status "$@" ;;
add-user) shift; cmd_add_user "$@" ;;
shell) shift; cmd_shell "$@" ;;
service-run) cmd_start ;;
service-stop) cmd_stop ;;
help|--help|-h|'') show_help ;;
*) error "Unknown command: $1"; show_help >&2; exit 1 ;;
esac
exit 0