mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 15:28:24 +00:00
* test(mail): Phase 2 scaffolding — rspamd.sh + templates + routers + bats baseline (ref #153)
* feat(mail): rspamd.sh — install_rspamd + re-entry guard (ref #153)
* feat(mail): Rspamd config templates (9 .conf files) — DKIM/ARC/DMARC/greylist/ratelimit (ref #153)
* feat(mail): lib/mail/rspamd.sh — configure helpers + DKIM keygen + D9 purge gate (ref #153)
* feat(mail): Milestone C — rspamd_client + /rspamd/* router + legacy shims (ref #153)
- rspamd_client.py: httpx wrapper around the Rspamd HTTP controller
(read /stat /history /graph; write /reload /learnspam /learnham).
- routers/rspamd.py: 11 new endpoints under /api/v1/mail/rspamd/*
(status, history, scores, reload, learn-{spam,ham}, whitelist CRUD,
dkim/{domain}/{status,keygen}).
- routers/legacy.py: 14 deprecation shims for the Phase 1 /dkim/*
/spam/* /grey/* surface. Each emits X-Deprecated-Endpoint: rspamd
and forwards to the Rspamd equivalent. Removed in v3.0.
- main.py: drops the 14 inline legacy handlers and mounts the two
routers via app.include_router(). 62/62 Phase 1 endpoint pytest
still passes.
* feat(mail): Milestone D — mailctl cmd_rspamd dispatcher (ref #153)
Adds rspamd subcommand to mailctl: install / start / stop / restart /
reload / status / dkim-keygen / dns-records / learn-spam / learn-ham /
purge-legacy. The install path calls lib/mail/rspamd.sh helpers in
order: install_rspamd → configure_rspamd_milter → configure_rspamd_controller
→ configure_rspamd_dkim → configure_rspamd_postfix_milter.
The purge-legacy verb requires Rspamd to respond healthy on :11334
before removing SA + OpenDKIM (D9 safety gate).
* feat(mail): Milestone E — install adds rspamd; mail.toml [mail.rspamd]; postinst secrets (ref #153)
- install_mail_packages: now installs rspamd + redis-server + enables
postfix/dovecot/rspamd at LXC boot (Phase 1 follow-up). Drops Apache/
Roundcube — they live in the roundcube LXC after the rev. 3 split.
- mail.toml: [mail.rspamd] section (greylist, bayes_autolearn,
ratelimit_outbound=200/h/user, web_ui_host). horde_url added.
- debian/postinst: on upgrade <2.3, generate /etc/secubox/secrets/
rspamd-controller.pw (32-byte random) and mkdir /data/volumes/mail/
rspamd/{dkim,bayes,history,settings} chown 100110 (_rspamd in unpriv).
* feat(mail): Milestone F — rspamd-route-sync-patch deploy helper (ref #153)
Idempotent one-shot script run at deploy time:
1. Removes 10.100.0.{10,11,12} from sync-mitmproxy-routes.sh's
DEAD_CONTAINER_IPS list so the periodic timer does not reroute
the mail/horde/roundcube LXC routes to webui.
2. Adds rspamd.gk2.secubox.in → [10.100.0.10, 11334] to both the
host's /srv/mitmproxy/haproxy-routes.json and the mitmproxy LXC
copy, then restarts the mitmdump worker.
No source-side nginx vhost added: rspamd UI goes through the
existing HAProxy → mitmproxy_inspector → 10.100.0.10:11334 path.
* feat(mail): bump secubox-mail to 2.3.0 (Phase 2 Rspamd) (ref #153)
* test(mail): Phase 2 13-gate acceptance smoke (ref #153)
Run with: bash tests/scripts/test-mail-phase2-acceptance.sh root@admin.gk2.secubox.in
Gates 1-3 are source-side (parses, pytest, deb path coverage).
Gates 4-11 are board-side (Rspamd listening, Postfix milter wired,
DKIM key generated, modules loaded, web UI via WAF).
Gates 12-13 are post-cutover (legacy purged + Phase 1 regression
check — 5 secubox.in users intact, webmail still WAF-routed).
Every board call uses timeout — Phase 1 lesson, never raw pipes.
* fix(mail): debian/rules ships templates/rspamd/ (path-coverage fix, ref #153)
Phase 1 lesson redux: debian/rules silently misses files unless every
new source-tree subdir gets its explicit cp line. Adds the templates/
rspamd/ install line so the 9 local.d configs + the postfix milter
snippet land at /usr/lib/secubox/mail/templates/rspamd/.
* fix(mail): live-deploy fixes — _rspamd uid via lxc-attach + smoke gate 7/12 (ref #153)
Two issues surfaced during Phase 2 live deploy on admin.gk2.secubox.in:
1. configure_rspamd_controller hardcoded chown to host uid 100110, assuming
_rspamd is uid 110 inside the LXC. On this Debian 12 image _rspamd is
uid 107 — rspamd crash-looped with "Permission denied" on secrets.inc.
Fix: chown via `lxc-attach -- chown _rspamd:_rspamd` so the kernel's
idmap maps the right uid regardless of image package set. Mode 0640
instead of 0600 so the worker (uid _rspamd) can read it even if launched
from a slightly different group context.
2. Smoke gate 7 grepped for the literal string "rspamd" in the first 5
lines of `rspamc stat`, but rspamc only prints "Results for command:
stat" + counters. Fix: grep for an always-present stat marker
("Messages scanned" or "Pools allocated") with a wider head -10 window.
3. Smoke gate 12 ran `dpkg -l opendkim spamassassin` under `set -e` and
silently aborted the smoke when the named packages were unknown (the
success case after purge). Fix: append `|| true` so the grep below is
the actual gate.
All 13 gates green on admin.gk2.secubox.in after these fixes + the manual
deploy steps (DKIM keygen via lxc-attach, bind-mount entries, HAProxy +
mitmproxy route map for rspamd.gk2.secubox.in).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(mail): rspamd lib + sbin write through live LXC (ref #153)
Phase 2 deploy on admin.gk2.secubox.in surfaced three brittleness points
that the live-deploy fixes patched ad-hoc:
1. configure_rspamd_milter wrote to ${LXC_BASE}/<container>/rootfs/etc/
rspamd/local.d/ — but on this board the runtime LXC mounts a different
rootfs (/data/lxc/<container>/rootfs/ per lxc.rootfs.path). Files written
to the host-side guess landed in a stale shell the running LXC didn't
see, and the first install's local.d was empty.
2. configure_rspamd_controller had the same path-guess issue plus
hardcoded chown 100110:100110 (had to be patched in commit 637b2221
because _rspamd is uid 107 on this image, not 110).
3. rspamd_keygen called rspamadm on the host PATH — rspamadm only exists
inside the rspamd-installed LXC, so the function errored out on every
install. The Phase 2 deploy worked around it manually.
This refactor makes all three write THROUGH the live LXC via lxc-attach:
- Each local.d template is streamed in with `lxc-attach -- tee` (kernel
resolves the rootfs path; idmap applies to the resulting file uid).
- secrets.inc + worker-controller.inc likewise written via tee, then
chown'd inside the LXC to _rspamd:_rspamd (kernel maps to the correct
outside-LXC subuid regardless of image).
- rspamd_keygen takes a `container` arg, runs `rspamadm dkim_keygen`
inside the LXC, writes the keypair to /etc/rspamd-keys/<domain>/ (the
bind-mounted path; falls back to /var/lib/rspamd/dkim/ if the bind
mount isn't active yet), and mirrors the DNS TXT to the host data dir
for DNS-publishing tooling.
- mailctl's `dkim-keygen` subcommand now delegates to the lib function.
- rspamd-route-sync-patch.sh verifies each write (reads back + asserts
the entry equals expected) and fails loudly on mismatch — the Phase 2
deploy needed a manual second pass for the mitmproxy LXC copy.
Pre-conditions: configure_rspamd_milter and configure_rspamd_controller
now require the LXC to be RUNNING (added an explicit lxc-info guard).
This matches the cmd_rspamd install path which starts the LXC via
install_rspamd before configuring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
950 lines
29 KiB
Bash
Executable File
950 lines
29 KiB
Bash
Executable File
#!/bin/bash
|
|
# SecuBox Mail Server Controller
|
|
# Unified mail + webmail management for Debian
|
|
# Three-fold architecture: Components, Status, Access
|
|
|
|
VERSION="2.2.0"
|
|
CONFIG_FILE="/etc/secubox/mail.toml"
|
|
LIB_DIR="/usr/lib/secubox/mail/lib"
|
|
DATA_PATH="/data/volumes/mail"
|
|
LXC_PATH="/var/lib/lxc"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
log() { echo -e "${GREEN}[MAIL]${NC} $1"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
|
|
|
# Load libraries
|
|
[ -f "$LIB_DIR/container.sh" ] && . "$LIB_DIR/container.sh"
|
|
[ -f "$LIB_DIR/users.sh" ] && . "$LIB_DIR/users.sh"
|
|
|
|
# ============================================================================
|
|
# Configuration (TOML parsing)
|
|
# ============================================================================
|
|
|
|
config_get() {
|
|
local key="$1"
|
|
local default="$2"
|
|
if [ -f "$CONFIG_FILE" ]; then
|
|
local value=$(grep "^${key} *=" "$CONFIG_FILE" 2>/dev/null | head -1 | cut -d= -f2- | tr -d ' "')
|
|
[ -n "$value" ] && echo "$value" || echo "$default"
|
|
else
|
|
echo "$default"
|
|
fi
|
|
}
|
|
|
|
DOMAIN=$(config_get "domain" "secubox.local")
|
|
HOSTNAME=$(config_get "hostname" "mail")
|
|
CONTAINER=$(config_get "container" "mail")
|
|
WEBMAIL_CONTAINER="$CONTAINER" # legacy alias — webmail merged into single container
|
|
LXC_IP=$(config_get "lxc_ip" "10.100.0.10")
|
|
LXC_BRIDGE=$(config_get "lxc_bridge" "br-lxc")
|
|
LXC_GATEWAY=$(config_get "lxc_gateway" "10.100.0.1")
|
|
WEBMAIL_PORT=80 # Roundcube now on standard HTTP inside the LXC, proxied via host nginx :443
|
|
|
|
# ============================================================================
|
|
# LXC Helpers
|
|
# ============================================================================
|
|
|
|
lxc_running() {
|
|
local name="$1"
|
|
lxc-info -n "$name" 2>/dev/null | grep -q "State:.*RUNNING"
|
|
}
|
|
|
|
lxc_exists() {
|
|
local name="$1"
|
|
[ -d "$LXC_PATH/$name/rootfs" ]
|
|
}
|
|
|
|
lxc_attach() {
|
|
local name="$1"
|
|
shift
|
|
lxc-attach -n "$name" -- "$@"
|
|
}
|
|
|
|
# ============================================================================
|
|
# THREE-FOLD ARCHITECTURE: Components (What)
|
|
# ============================================================================
|
|
|
|
cmd_components() {
|
|
# Output JSON for API consumption
|
|
local mail_installed=false
|
|
local mail_running=false
|
|
local webmail_installed=false
|
|
local webmail_running=false
|
|
|
|
lxc_exists "$CONTAINER" && mail_installed=true
|
|
lxc_running "$CONTAINER" && mail_running=true
|
|
lxc_exists "$WEBMAIL_CONTAINER" && webmail_installed=true
|
|
lxc_running "$WEBMAIL_CONTAINER" && webmail_running=true
|
|
|
|
cat <<EOF
|
|
{
|
|
"components": [
|
|
{
|
|
"name": "Mail Server",
|
|
"type": "lxc",
|
|
"container": "$CONTAINER",
|
|
"description": "Postfix + Dovecot mail server",
|
|
"installed": $mail_installed,
|
|
"running": $mail_running,
|
|
"ports": [25, 587, 465, 143, 993, 110, 995],
|
|
"ip": "$LXC_IP"
|
|
},
|
|
{
|
|
"name": "Webmail",
|
|
"type": "lxc",
|
|
"container": "$WEBMAIL_CONTAINER",
|
|
"description": "Roundcube webmail interface",
|
|
"installed": $webmail_installed,
|
|
"running": $webmail_running,
|
|
"port": $WEBMAIL_PORT
|
|
},
|
|
{
|
|
"name": "User Database",
|
|
"type": "file",
|
|
"path": "$DATA_PATH/config/users",
|
|
"description": "Dovecot passwd-file users",
|
|
"installed": true
|
|
},
|
|
{
|
|
"name": "SSL Certificates",
|
|
"type": "file",
|
|
"path": "$DATA_PATH/ssl/fullchain.pem",
|
|
"description": "TLS certificates for mail server",
|
|
"installed": $([ -f "$DATA_PATH/ssl/fullchain.pem" ] && echo true || echo false)
|
|
}
|
|
]
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# ============================================================================
|
|
# THREE-FOLD ARCHITECTURE: Access (How to connect)
|
|
# ============================================================================
|
|
|
|
cmd_access() {
|
|
local fqdn="$HOSTNAME.$DOMAIN"
|
|
local ssl_enabled=false
|
|
[ -f "$DATA_PATH/ssl/fullchain.pem" ] && ssl_enabled=true
|
|
|
|
cat <<EOF
|
|
{
|
|
"domain": "$DOMAIN",
|
|
"mail_server": "$fqdn",
|
|
"connections": {
|
|
"imap": {
|
|
"host": "$fqdn",
|
|
"port": 993,
|
|
"ssl": true,
|
|
"starttls_port": 143
|
|
},
|
|
"smtp": {
|
|
"host": "$fqdn",
|
|
"port": 587,
|
|
"ssl": false,
|
|
"starttls": true,
|
|
"ssl_port": 465
|
|
},
|
|
"pop3": {
|
|
"host": "$fqdn",
|
|
"port": 995,
|
|
"ssl": true
|
|
}
|
|
},
|
|
"webmail": {
|
|
"url": "https://webmail.$DOMAIN",
|
|
"local_url": "http://localhost:$WEBMAIL_PORT"
|
|
},
|
|
"ssl_enabled": $ssl_enabled,
|
|
"client_configs": {
|
|
"thunderbird": "https://autoconfig.$DOMAIN/mail/config-v1.1.xml",
|
|
"outlook": "https://autodiscover.$DOMAIN/autodiscover/autodiscover.xml",
|
|
"apple": "https://$fqdn/$DOMAIN.mobileconfig"
|
|
}
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# ============================================================================
|
|
# DNS Setup
|
|
# ============================================================================
|
|
|
|
cmd_dns_setup() {
|
|
local fqdn="$HOSTNAME.$DOMAIN"
|
|
log "Generating DNS records for $DOMAIN..."
|
|
|
|
echo ""
|
|
echo "============================================"
|
|
echo " DNS Records for $DOMAIN"
|
|
echo "============================================"
|
|
echo ""
|
|
echo "MX Record:"
|
|
echo " $DOMAIN. IN MX 10 $fqdn."
|
|
echo ""
|
|
echo "A Records:"
|
|
echo " $HOSTNAME.$DOMAIN. IN A <YOUR_IP>"
|
|
echo " webmail.$DOMAIN. IN A <YOUR_IP>"
|
|
echo ""
|
|
echo "SPF Record:"
|
|
echo " $DOMAIN. IN TXT \"v=spf1 mx a:$fqdn -all\""
|
|
echo ""
|
|
echo "DKIM Record (generate with: mailctl dkim-setup):"
|
|
echo " default._domainkey.$DOMAIN. IN TXT \"v=DKIM1; k=rsa; p=...\""
|
|
echo ""
|
|
echo "DMARC Record:"
|
|
echo " _dmarc.$DOMAIN. IN TXT \"v=DMARC1; p=quarantine; rua=mailto:postmaster@$DOMAIN\""
|
|
echo ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# User Repair (mailbox resync)
|
|
# ============================================================================
|
|
|
|
cmd_user_repair() {
|
|
local email="$1"
|
|
[ -z "$email" ] && { echo "Usage: mailctl user-repair <email>"; return 1; }
|
|
|
|
if ! lxc_running "$CONTAINER"; then
|
|
error "Mail container not running"
|
|
return 1
|
|
fi
|
|
|
|
log "Repairing mailbox for $email..."
|
|
lxc_attach "$CONTAINER" doveadm force-resync -u "$email" '*'
|
|
log "Mailbox repair complete"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Fix Ports (container port checking)
|
|
# ============================================================================
|
|
|
|
cmd_fix_ports() {
|
|
if ! lxc_running "$CONTAINER"; then
|
|
error "Mail container not running"
|
|
return 1
|
|
fi
|
|
|
|
log "Checking mail ports..."
|
|
local ports=$(lxc_attach "$CONTAINER" netstat -tln 2>/dev/null)
|
|
local all_ok=true
|
|
|
|
for port in 25 587 465 143 993; do
|
|
if echo "$ports" | grep -q ":$port "; then
|
|
echo -e " Port $port: ${GREEN}OK${NC}"
|
|
else
|
|
echo -e " Port $port: ${RED}NOT LISTENING${NC}"
|
|
all_ok=false
|
|
fi
|
|
done
|
|
|
|
if [ "$all_ok" = "false" ]; then
|
|
warn "Some ports are not listening. Attempting restart..."
|
|
lxc_attach "$CONTAINER" /opt/start-mail.sh &
|
|
sleep 3
|
|
log "Services restarted"
|
|
else
|
|
log "All ports OK"
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Install
|
|
# ============================================================================
|
|
|
|
cmd_install() {
|
|
[ "$(id -u)" -eq 0 ] || { error "Root required"; return 1; }
|
|
log "Installing SecuBox Mail Server (single LXC '$CONTAINER')..."
|
|
|
|
: "${LIB_DIR:=/usr/lib/secubox/mail/lib}"
|
|
if [ ! -f "$LIB_DIR/install.sh" ]; then
|
|
local alt="$(dirname "$0")/../lib/mail"
|
|
[ -f "$alt/install.sh" ] && LIB_DIR="$alt"
|
|
fi
|
|
# shellcheck source=/dev/null
|
|
source "$LIB_DIR/lxc.sh"
|
|
# shellcheck source=/dev/null
|
|
source "$LIB_DIR/install.sh"
|
|
|
|
# Persistent data dirs (host-side, bind-mounted into the LXC)
|
|
mkdir -p "$DATA_PATH"/{vmail,config,ssl,backups}
|
|
mkdir -p "$LXC_PATH"
|
|
|
|
# Bootstrap rootfs + render LXC config
|
|
if ! lxc_exists "$CONTAINER"; then
|
|
LXC_BASE="$LXC_PATH" bootstrap_debian "$CONTAINER"
|
|
fi
|
|
LXC_BASE="$LXC_PATH" lxc_create_config "$CONTAINER" "$LXC_IP" "$LXC_BRIDGE" "$LXC_GATEWAY"
|
|
|
|
# Bring the container up so install steps can chroot/apt
|
|
LXC_BASE="$LXC_PATH" lxc_start_safely "$CONTAINER" || {
|
|
error "container failed to start"; return 1; }
|
|
|
|
LXC_BASE="$LXC_PATH" install_mail_packages "$CONTAINER"
|
|
LXC_BASE="$LXC_PATH" install_webmail_packages "$CONTAINER"
|
|
LXC_BASE="$LXC_PATH" DOMAIN="$DOMAIN" HOSTNAME="$HOSTNAME" \
|
|
configure_postfix "$CONTAINER"
|
|
LXC_BASE="$LXC_PATH" configure_dovecot "$CONTAINER"
|
|
LXC_BASE="$LXC_PATH" DOMAIN="$DOMAIN" \
|
|
configure_roundcube "$CONTAINER"
|
|
|
|
log "Installation complete!"
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo " 1. Configure: edit $CONFIG_FILE"
|
|
echo " 2. Add users: mailctl user add user@$DOMAIN"
|
|
echo " 3. Start: mailctl start"
|
|
}
|
|
|
|
cmd_uninstall() {
|
|
warn "This will remove mail containers. Data in $DATA_PATH will be preserved."
|
|
read -p "Continue? (yes/no): " confirm
|
|
[ "$confirm" != "yes" ] && { echo "Aborted"; return 1; }
|
|
|
|
log "Stopping services..."
|
|
systemctl stop secubox-mail 2>/dev/null
|
|
|
|
log "Removing containers..."
|
|
lxc-stop -n "$CONTAINER" 2>/dev/null
|
|
lxc-stop -n "$WEBMAIL_CONTAINER" 2>/dev/null
|
|
rm -rf "$LXC_PATH/$CONTAINER"
|
|
rm -rf "$LXC_PATH/$WEBMAIL_CONTAINER"
|
|
|
|
log "Mail server removed. Data preserved in $DATA_PATH"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Service Control
|
|
# ============================================================================
|
|
|
|
cmd_start() {
|
|
log "Starting mail LXC '$CONTAINER'..."
|
|
if lxc_running "$CONTAINER"; then
|
|
log "already running"
|
|
return 0
|
|
fi
|
|
if ! lxc_exists "$CONTAINER"; then
|
|
error "container '$CONTAINER' not present at $LXC_PATH/$CONTAINER"
|
|
return 1
|
|
fi
|
|
lxc-start -n "$CONTAINER" -d
|
|
# Wait up to 10s for RUNNING state.
|
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
|
lxc_running "$CONTAINER" && { log "mail LXC running"; return 0; }
|
|
sleep 1
|
|
done
|
|
error "mail LXC failed to start within 10s"
|
|
return 1
|
|
}
|
|
|
|
cmd_stop() {
|
|
log "Stopping mail LXC '$CONTAINER'..."
|
|
if ! lxc_running "$CONTAINER"; then
|
|
log "already stopped"
|
|
return 0
|
|
fi
|
|
lxc-stop -n "$CONTAINER" -t 30 || true
|
|
log "mail LXC stopped"
|
|
}
|
|
|
|
cmd_restart() {
|
|
cmd_stop
|
|
sleep 2
|
|
cmd_start
|
|
}
|
|
|
|
cmd_status() {
|
|
echo ""
|
|
echo "========================================"
|
|
echo " SecuBox Mail Server v$VERSION"
|
|
echo "========================================"
|
|
echo ""
|
|
|
|
echo "Configuration:"
|
|
echo " Domain: $DOMAIN"
|
|
echo " Hostname: $HOSTNAME.$DOMAIN"
|
|
echo " Mail IP: $LXC_IP"
|
|
echo ""
|
|
|
|
# Mail server status
|
|
echo "Mail Server ($CONTAINER):"
|
|
if lxc_running "$CONTAINER"; then
|
|
echo -e " Status: ${GREEN}Running${NC}"
|
|
# Check ports
|
|
local ports=$(lxc_attach "$CONTAINER" netstat -tln 2>/dev/null)
|
|
for port in 25 587 465 993 995; do
|
|
if echo "$ports" | grep -q ":$port "; then
|
|
echo -e " Port $port: ${GREEN}listening${NC}"
|
|
else
|
|
echo -e " Port $port: ${RED}closed${NC}"
|
|
fi
|
|
done
|
|
elif lxc_exists "$CONTAINER"; then
|
|
echo -e " Status: ${YELLOW}Stopped${NC}"
|
|
else
|
|
echo -e " Status: ${RED}Not installed${NC}"
|
|
fi
|
|
echo ""
|
|
|
|
# Webmail status
|
|
echo "Webmail ($WEBMAIL_CONTAINER):"
|
|
if lxc_running "$WEBMAIL_CONTAINER"; then
|
|
echo -e " Status: ${GREEN}Running${NC}"
|
|
echo " Port: $WEBMAIL_PORT"
|
|
elif lxc_exists "$WEBMAIL_CONTAINER"; then
|
|
echo -e " Status: ${YELLOW}Stopped${NC}"
|
|
else
|
|
echo -e " Status: ${RED}Not installed${NC}"
|
|
fi
|
|
echo ""
|
|
|
|
# User count
|
|
local users_file="$DATA_PATH/config/users"
|
|
if [ -f "$users_file" ]; then
|
|
local user_count=$(wc -l < "$users_file")
|
|
echo "Users: $user_count"
|
|
fi
|
|
|
|
# Storage
|
|
if [ -d "$DATA_PATH/mail" ]; then
|
|
local storage=$(du -sh "$DATA_PATH/mail" 2>/dev/null | cut -f1)
|
|
echo "Storage: ${storage:-0}"
|
|
fi
|
|
echo ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# User Management
|
|
# ============================================================================
|
|
|
|
cmd_sync() {
|
|
# Phase 1 rev. 2: persistent config is bind-mounted from
|
|
# $DATA_PATH/config into $LXC_PATH/$CONTAINER/rootfs/etc/mail-config,
|
|
# so there is no copy step. Rebuild Postfix LMDB indexes if the
|
|
# container is running; otherwise it's a no-op until next start.
|
|
if lxc_running "$CONTAINER"; then
|
|
for map in vmailbox virtual; do
|
|
lxc_attach "$CONTAINER" postmap "lmdb:/etc/mail-config/$map" 2>/dev/null || true
|
|
done
|
|
fi
|
|
}
|
|
|
|
cmd_user() {
|
|
local action="${1:-list}"
|
|
shift
|
|
|
|
case "$action" in
|
|
add)
|
|
local email="$1"
|
|
local password="$2"
|
|
[ -z "$email" ] && { echo "Usage: mailctl user add <email> [password]"; return 1; }
|
|
|
|
if [ -z "$password" ]; then
|
|
read -s -p "Password: " password
|
|
echo ""
|
|
fi
|
|
|
|
user_add "$email" "$password"
|
|
cmd_sync
|
|
;;
|
|
del|delete|remove)
|
|
local email="$1"
|
|
[ -z "$email" ] && { echo "Usage: mailctl user del <email>"; return 1; }
|
|
user_del "$email"
|
|
cmd_sync
|
|
;;
|
|
passwd|password)
|
|
local email="$1"
|
|
local password="$2"
|
|
[ -z "$email" ] && { echo "Usage: mailctl user passwd <email> [password]"; return 1; }
|
|
|
|
if [ -z "$password" ]; then
|
|
read -s -p "New password: " password
|
|
echo ""
|
|
fi
|
|
|
|
user_passwd "$email" "$password"
|
|
cmd_sync
|
|
;;
|
|
list)
|
|
user_list
|
|
;;
|
|
*)
|
|
echo "User commands:"
|
|
echo " user add <email> [pass] Add mail user"
|
|
echo " user del <email> Delete mail user"
|
|
echo " user list List all users"
|
|
echo " user passwd <email> Change password"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cmd_alias() {
|
|
local action="${1:-list}"
|
|
shift
|
|
|
|
case "$action" in
|
|
add)
|
|
alias_add "$@"
|
|
cmd_sync
|
|
;;
|
|
del|delete)
|
|
alias_del "$@"
|
|
cmd_sync
|
|
;;
|
|
list)
|
|
alias_list
|
|
;;
|
|
*)
|
|
echo "Alias commands:"
|
|
echo " alias add <alias> <target> Add email alias"
|
|
echo " alias del <alias> Delete email alias"
|
|
echo " alias list List aliases"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ============================================================================
|
|
# Migration from OpenWrt (192.168.255.1)
|
|
# ============================================================================
|
|
|
|
cmd_migrate() {
|
|
local source="${1:-192.168.255.1}"
|
|
|
|
log "Migrating mail data from $source..."
|
|
|
|
# Check SSH connectivity
|
|
if ! ssh -o ConnectTimeout=5 "root@$source" "echo ok" >/dev/null 2>&1; then
|
|
error "Cannot connect to $source via SSH"
|
|
echo "Ensure SSH key is configured: ssh-copy-id root@$source"
|
|
return 1
|
|
fi
|
|
|
|
# Create backup directory
|
|
local backup_dir="$DATA_PATH/migration_$(date +%Y%m%d_%H%M%S)"
|
|
mkdir -p "$backup_dir"
|
|
|
|
# Migrate mail data
|
|
log "Migrating mailboxes..."
|
|
rsync -avz --progress "root@$source:/srv/mailserver/mail/" "$DATA_PATH/mail/"
|
|
|
|
# Migrate config (users, aliases)
|
|
log "Migrating configuration..."
|
|
rsync -avz --progress "root@$source:/srv/mailserver/config/" "$DATA_PATH/config/"
|
|
|
|
# Migrate SSL certificates
|
|
log "Migrating SSL certificates..."
|
|
rsync -avz --progress "root@$source:/srv/mailserver/ssl/" "$DATA_PATH/ssl/"
|
|
|
|
# Import UCI config
|
|
log "Importing UCI configuration..."
|
|
ssh "root@$source" "uci show mailserver" > "$backup_dir/mailserver.uci" 2>/dev/null || true
|
|
|
|
# Convert UCI to TOML
|
|
if [ -f "$backup_dir/mailserver.uci" ]; then
|
|
local uci_domain=$(grep "main.domain=" "$backup_dir/mailserver.uci" | cut -d"'" -f2)
|
|
local uci_hostname=$(grep "main.hostname=" "$backup_dir/mailserver.uci" | cut -d"'" -f2)
|
|
|
|
if [ -n "$uci_domain" ]; then
|
|
log "Setting domain: $uci_domain"
|
|
sed -i "s/^domain = .*/domain = \"$uci_domain\"/" "$CONFIG_FILE" 2>/dev/null || \
|
|
echo "domain = \"$uci_domain\"" >> "$CONFIG_FILE"
|
|
fi
|
|
|
|
if [ -n "$uci_hostname" ]; then
|
|
log "Setting hostname: $uci_hostname"
|
|
sed -i "s/^hostname = .*/hostname = \"$uci_hostname\"/" "$CONFIG_FILE" 2>/dev/null || \
|
|
echo "hostname = \"$uci_hostname\"" >> "$CONFIG_FILE"
|
|
fi
|
|
fi
|
|
|
|
log "Migration complete!"
|
|
log "Data backed up to: $backup_dir"
|
|
log "Restart services: systemctl restart secubox-mail"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Backup / Restore
|
|
# ============================================================================
|
|
|
|
cmd_backup() {
|
|
local name="${1:-$(date +%Y%m%d_%H%M%S)}"
|
|
local backup_file="$DATA_PATH/backups/mail_$name.tar.gz"
|
|
|
|
log "Creating backup: $backup_file"
|
|
|
|
mkdir -p "$DATA_PATH/backups"
|
|
|
|
tar -czf "$backup_file" \
|
|
-C "$DATA_PATH" \
|
|
mail config ssl \
|
|
2>/dev/null
|
|
|
|
log "Backup created: $backup_file"
|
|
echo "$backup_file"
|
|
}
|
|
|
|
cmd_restore() {
|
|
local backup_file="$1"
|
|
|
|
if [ -z "$backup_file" ] || [ ! -f "$backup_file" ]; then
|
|
echo "Usage: mailctl restore <backup_file>"
|
|
echo ""
|
|
echo "Available backups:"
|
|
ls -la "$DATA_PATH/backups/"*.tar.gz 2>/dev/null || echo " No backups found"
|
|
return 1
|
|
fi
|
|
|
|
warn "This will overwrite current mail data!"
|
|
read -p "Continue? (yes/no): " confirm
|
|
[ "$confirm" != "yes" ] && { echo "Aborted"; return 1; }
|
|
|
|
log "Stopping services..."
|
|
cmd_stop
|
|
|
|
log "Restoring from: $backup_file"
|
|
tar -xzf "$backup_file" -C "$DATA_PATH"
|
|
|
|
log "Starting services..."
|
|
cmd_start
|
|
|
|
log "Restore complete!"
|
|
}
|
|
|
|
# ============================================================================
|
|
# SSL
|
|
# ============================================================================
|
|
|
|
cmd_ssl() {
|
|
local action="${1:-status}"
|
|
shift
|
|
|
|
case "$action" in
|
|
status)
|
|
if [ -f "$DATA_PATH/ssl/fullchain.pem" ]; then
|
|
echo "SSL Certificate:"
|
|
openssl x509 -in "$DATA_PATH/ssl/fullchain.pem" -noout -subject -dates 2>/dev/null | sed 's/^/ /'
|
|
else
|
|
echo "No SSL certificate installed"
|
|
echo "Use: mailctl ssl setup"
|
|
fi
|
|
;;
|
|
setup)
|
|
local fqdn="$HOSTNAME.$DOMAIN"
|
|
log "Setting up SSL for $fqdn..."
|
|
|
|
if command -v certbot >/dev/null 2>&1; then
|
|
certbot certonly --standalone -d "$fqdn" \
|
|
--cert-path "$DATA_PATH/ssl/fullchain.pem" \
|
|
--key-path "$DATA_PATH/ssl/privkey.pem"
|
|
elif command -v acme.sh >/dev/null 2>&1; then
|
|
acme.sh --issue -d "$fqdn" --standalone
|
|
acme.sh --install-cert -d "$fqdn" \
|
|
--fullchain-file "$DATA_PATH/ssl/fullchain.pem" \
|
|
--key-file "$DATA_PATH/ssl/privkey.pem"
|
|
else
|
|
error "No ACME client found. Install certbot or acme.sh"
|
|
return 1
|
|
fi
|
|
|
|
chmod 600 "$DATA_PATH/ssl/"*.pem
|
|
log "SSL certificate installed"
|
|
cmd_restart
|
|
;;
|
|
*)
|
|
echo "SSL commands:"
|
|
echo " ssl status Show certificate info"
|
|
echo " ssl setup Obtain Let's Encrypt certificate"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ============================================================================
|
|
# DKIM
|
|
# ============================================================================
|
|
|
|
cmd_dkim() {
|
|
local action="${1:-status}"
|
|
shift
|
|
|
|
case "$action" in
|
|
setup|generate)
|
|
# OpenDKIM setup is deferred to Phase 2 (Rspamd replaces it).
|
|
# Phase 1 stub: tell the user where to look.
|
|
warn "DKIM setup will move to Rspamd in Phase 2."
|
|
warn "Phase 1: configure DKIM manually inside the LXC if needed."
|
|
;;
|
|
status)
|
|
if [ -f "$DATA_PATH/dkim/default.txt" ]; then
|
|
log "DKIM key exists"
|
|
cat "$DATA_PATH/dkim/default.txt"
|
|
else
|
|
warn "DKIM not configured. Run: mailctl dkim setup"
|
|
fi
|
|
;;
|
|
*)
|
|
echo "DKIM commands:"
|
|
echo " dkim setup Generate DKIM keys"
|
|
echo " dkim status Show DKIM DNS record"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ============================================================================
|
|
# Logs
|
|
# ============================================================================
|
|
|
|
cmd_logs() {
|
|
local lines="${1:-50}"
|
|
|
|
if lxc_running "$CONTAINER"; then
|
|
lxc_attach "$CONTAINER" tail -n "$lines" /var/log/mail.log 2>/dev/null
|
|
else
|
|
error "Mail container not running"
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Help
|
|
# ============================================================================
|
|
|
|
show_help() {
|
|
cat << EOF
|
|
SecuBox Mail Server v$VERSION
|
|
Three-fold architecture: Components, Status, Access
|
|
|
|
Usage: mailctl <command> [options]
|
|
|
|
Information (Three-fold):
|
|
components List system components (JSON)
|
|
status Show service status
|
|
access Show connection info (JSON)
|
|
|
|
Setup:
|
|
install Install mail server + webmail
|
|
uninstall Remove mail server
|
|
migrate [host] Migrate data from OpenWrt (default: 192.168.255.1)
|
|
|
|
Service:
|
|
start Start mail services
|
|
stop Stop mail services
|
|
restart Restart mail services
|
|
|
|
Users:
|
|
user add <email> Add mail user
|
|
user del <email> Delete mail user
|
|
user list List all users
|
|
user passwd <email> Change password
|
|
user-repair <email> Repair user mailbox
|
|
|
|
Aliases:
|
|
alias add <a> <t> Add email alias
|
|
alias del <alias> Delete alias
|
|
alias list List aliases
|
|
|
|
Backup:
|
|
backup [name] Create backup
|
|
restore <file> Restore from backup
|
|
|
|
SSL & DNS:
|
|
ssl status Show certificate info
|
|
ssl setup Obtain Let's Encrypt certificate
|
|
dns-setup Show DNS records to configure
|
|
|
|
Diagnostics:
|
|
logs [lines] View mail logs
|
|
fix-ports Check and fix listening ports
|
|
|
|
Examples:
|
|
mailctl install
|
|
mailctl components # List what's installed
|
|
mailctl access # Show connection details
|
|
mailctl user add admin@example.com
|
|
mailctl migrate 192.168.255.1
|
|
mailctl dns-setup # Show DNS records
|
|
|
|
EOF
|
|
}
|
|
|
|
# ============================================================================
|
|
# Phase 1 config migration (rev. 2)
|
|
# ============================================================================
|
|
|
|
cmd_migrate_config() {
|
|
[ "$(id -u)" -eq 0 ] || { error "Root required"; return 1; }
|
|
: "${LIB_DIR:=/usr/lib/secubox/mail/lib}"
|
|
# Installed layout: /usr/lib/secubox/mail/lib/migrate.sh
|
|
# In-tree layout: packages/secubox-mail/lib/mail/migrate.sh
|
|
if [ ! -f "$LIB_DIR/migrate.sh" ]; then
|
|
local alt="$(dirname "$0")/../lib/mail"
|
|
[ -f "$alt/migrate.sh" ] && LIB_DIR="$alt"
|
|
fi
|
|
# shellcheck source=/dev/null
|
|
source "$LIB_DIR/migrate.sh"
|
|
|
|
local cfg="${CONFIG_FILE:-/etc/secubox/mail.toml}"
|
|
[ -f "$cfg" ] || { warn "no config to migrate at $cfg"; return 0; }
|
|
|
|
if grep -q "^container *=" "$cfg" 2>/dev/null; then
|
|
log "config already migrated (has 'container =' key)"
|
|
return 0
|
|
fi
|
|
|
|
if ! detect_legacy_toml_keys "$cfg" >/dev/null; then
|
|
log "no legacy keys to migrate"
|
|
return 0
|
|
fi
|
|
|
|
local backup="${cfg}.pre-phase1.$(date +%s).bak"
|
|
cp "$cfg" "$backup"
|
|
log "backup written to $backup"
|
|
|
|
python3 - "$cfg" <<'PY'
|
|
import sys, re
|
|
path = sys.argv[1]
|
|
src = open(path).read()
|
|
inject = (
|
|
'container = "mail"\n'
|
|
'lxc_ip = "10.100.0.10"\n'
|
|
'lxc_bridge = "br-lxc"\n'
|
|
'lxc_gateway = "10.100.0.1"\n'
|
|
'data_path = "/data/volumes/mail"\n'
|
|
'lxc_path = "/var/lib/lxc"\n'
|
|
)
|
|
# Comment out legacy keys FIRST so we don't end up with duplicates of
|
|
# data_path / lxc_path after injection.
|
|
for k in ("mail_container", "webmail_container", "mail_ip", "webmail_ip",
|
|
"webmail_port", "data_path", "lxc_path"):
|
|
src = re.sub(rf"^({k} *=.*)$", r"# DEPRECATED Phase 1: \1", src, flags=re.MULTILINE)
|
|
src = re.sub(r"(\[mail\]\n)", r"\1" + inject, src, count=1)
|
|
open(path, "w").write(src)
|
|
PY
|
|
log "config migrated to single-container schema"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Phase 2 — Rspamd subcommand
|
|
# ============================================================================
|
|
|
|
cmd_rspamd() {
|
|
[ "$(id -u)" -eq 0 ] || { error "Root required"; return 1; }
|
|
: "${LIB_DIR:=/usr/lib/secubox/mail/lib}"
|
|
[ -f "$LIB_DIR/rspamd.sh" ] || LIB_DIR="$(dirname "$0")/../lib/mail"
|
|
# shellcheck source=/dev/null
|
|
source "$LIB_DIR/rspamd.sh"
|
|
|
|
local sub="${1:-status}"
|
|
shift || true
|
|
case "$sub" in
|
|
install)
|
|
LXC_BASE="$LXC_PATH" install_rspamd "$CONTAINER"
|
|
LXC_BASE="$LXC_PATH" configure_rspamd_milter "$CONTAINER"
|
|
LXC_BASE="$LXC_PATH" configure_rspamd_controller "$CONTAINER"
|
|
LXC_BASE="$LXC_PATH" configure_rspamd_dkim "$CONTAINER" "$DOMAIN" "default"
|
|
LXC_BASE="$LXC_PATH" configure_rspamd_postfix_milter "$CONTAINER"
|
|
log "Rspamd installed. Start with: mailctl rspamd start"
|
|
;;
|
|
start|restart)
|
|
lxc-attach -n "$CONTAINER" -- systemctl restart rspamd
|
|
;;
|
|
stop)
|
|
lxc-attach -n "$CONTAINER" -- systemctl stop rspamd
|
|
;;
|
|
reload)
|
|
lxc-attach -n "$CONTAINER" -- systemctl reload rspamd
|
|
;;
|
|
status)
|
|
lxc-attach -n "$CONTAINER" -- rspamc stat 2>&1 | head -40
|
|
;;
|
|
dkim-keygen)
|
|
local domain="${1:-$DOMAIN}"
|
|
local sel="${2:-default}"
|
|
# Delegate to the lib function — runs rspamadm inside the LXC and
|
|
# resolves _rspamd uid via kernel idmap (image-agnostic).
|
|
LXC_BASE="$LXC_PATH" DATA_PATH="$DATA_PATH" \
|
|
rspamd_keygen "$CONTAINER" "$domain" "$sel"
|
|
log "DKIM keypair generated for $domain (selector $sel)"
|
|
;;
|
|
dns-records)
|
|
local domain="${1:-$DOMAIN}"
|
|
local sel="${2:-default}"
|
|
cat "$DATA_PATH/rspamd/dkim/$domain/$sel.txt" 2>/dev/null \
|
|
|| error "no DNS record for $domain/$sel"
|
|
;;
|
|
learn-spam)
|
|
local target="${1:-}"
|
|
[ -n "$target" ] || { echo "Usage: mailctl rspamd learn-spam <maildir-or-file>"; return 1; }
|
|
lxc-attach -n "$CONTAINER" -- rspamc learn_spam "$target"
|
|
;;
|
|
learn-ham)
|
|
local target="${1:-}"
|
|
[ -n "$target" ] || { echo "Usage: mailctl rspamd learn-ham <maildir-or-file>"; return 1; }
|
|
lxc-attach -n "$CONTAINER" -- rspamc learn_ham "$target"
|
|
;;
|
|
purge-legacy)
|
|
LXC_BASE="$LXC_PATH" rspamd_purge_legacy "$CONTAINER"
|
|
;;
|
|
*)
|
|
cat <<USAGE
|
|
Usage:
|
|
mailctl rspamd install
|
|
mailctl rspamd start | stop | restart | reload
|
|
mailctl rspamd status
|
|
mailctl rspamd dkim-keygen [<domain>] [<selector>]
|
|
mailctl rspamd dns-records [<domain>] [<selector>]
|
|
mailctl rspamd learn-spam <maildir-or-file>
|
|
mailctl rspamd learn-ham <maildir-or-file>
|
|
mailctl rspamd purge-legacy # purge SA/OpenDKIM after Rspamd verified
|
|
USAGE
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main
|
|
# ============================================================================
|
|
|
|
case "${1:-}" in
|
|
# Three-fold architecture
|
|
components) cmd_components ;;
|
|
access) cmd_access ;;
|
|
status) shift; cmd_status "$@" ;;
|
|
# Setup
|
|
install) shift; cmd_install "$@" ;;
|
|
uninstall) shift; cmd_uninstall "$@" ;;
|
|
migrate) shift; cmd_migrate "$@" ;;
|
|
migrate-config) shift; cmd_migrate_config "$@" ;;
|
|
# Phase 2 — Rspamd
|
|
rspamd) shift; cmd_rspamd "$@" ;;
|
|
# Service control
|
|
start) shift; cmd_start "$@" ;;
|
|
stop) shift; cmd_stop "$@" ;;
|
|
restart) shift; cmd_restart "$@" ;;
|
|
# Users and aliases
|
|
user) shift; cmd_user "$@" ;;
|
|
user-repair) shift; cmd_user_repair "$@" ;;
|
|
alias) shift; cmd_alias "$@" ;;
|
|
# Backup
|
|
backup) shift; cmd_backup "$@" ;;
|
|
restore) shift; cmd_restore "$@" ;;
|
|
# SSL, DNS, DKIM
|
|
ssl) shift; cmd_ssl "$@" ;;
|
|
dkim) shift; cmd_dkim "$@" ;;
|
|
dns-setup) cmd_dns_setup ;;
|
|
# Diagnostics
|
|
logs) shift; cmd_logs "$@" ;;
|
|
fix-ports) cmd_fix_ports ;;
|
|
sync) cmd_sync ;;
|
|
# Help
|
|
help|--help|-h|'') show_help ;;
|
|
*) error "Unknown command: $1"; show_help >&2; exit 1 ;;
|
|
esac
|
|
|
|
exit 0
|