#!/usr/bin/env bash
# DirhamHost installer (milestone M1).
# Target: curl -fsSL https://get.dirhamaday.ae | bash     (or: bash get.sh)
# Supported OS (auto-detected): AlmaLinux 9 (primary; Rocky 9 / RHEL 9 accepted), Ubuntu 24.04 (secondary).
# Design: safe by refusal. A fresh server installs; a used server is protected.
# Idempotent: every step records a marker in /var/lib/dirhamhost/steps and is skipped on re-run.
# Env overrides: DH_FORCE=1 (skip fresh-server gate), DH_HOSTNAME=panel.example.com, DH_EMAIL=admin@example.com,
#   DH_PANEL_PORT=8443, DH_SRC=/path/to/dh (build agent from source), DH_AGENT_URL=https://... (prebuilt agent),
#   DH_SKIP_SSL=1, DH_SKIP_CSF=1, DH_OMNIO_URL=https://.../omnio-suite.zip
set -eu

LOG=/var/log/dirhamhost-install.log
STATE=/var/lib/dirhamhost
STEPS=$STATE/steps
ETC=/etc/dirhamhost
PANEL_PORT="${DH_PANEL_PORT:-8443}"
export DEBIAN_FRONTEND=noninteractive

say(){ echo "[dirhamhost] $1" | tee -a "$LOG"; }
die(){ echo "[dirhamhost] ERROR: $1" | tee -a "$LOG"; exit 1; }
run(){ say "+ $*"; "$@" >> "$LOG" 2>&1 || die "command failed: $* (see $LOG)"; }
done_step(){ [ -f "$STEPS/$1" ]; }
mark(){ mkdir -p "$STEPS"; date -u > "$STEPS/$1"; say "step done: $1"; }

[ "$(id -u)" -eq 0 ] || die "run as root: this installs system services"
mkdir -p "$(dirname "$LOG")" "$STATE" "$STEPS" "$ETC"; touch "$LOG"; chmod 600 "$LOG"
say "DirhamHost installer starting $(date -u)"

# ===== 1. OS detection and gate =====
. /etc/os-release || die "cannot read /etc/os-release"
OS=""
case "${ID:-}" in
  almalinux|rocky|rhel) case "${VERSION_ID:-}" in 9*) OS=alma;; esac ;;
  ubuntu) [ "${VERSION_ID:-}" = "24.04" ] && OS=ubuntu ;;
esac
[ -n "$OS" ] || die "unsupported OS ${ID:-?} ${VERSION_ID:-?}: supported are AlmaLinux 9 (primary), Rocky/RHEL 9, Ubuntu 24.04"
ARCH="$(uname -m)"; case "$ARCH" in x86_64) ARCH=amd64;; aarch64) ARCH=arm64;; *) die "unsupported architecture $ARCH";; esac
say "detected ${PRETTY_NAME:-$ID} ($OS, $ARCH)"

# OS specific names used by the agent too (written to $ETC/os.env for dh-agent)
if [ "$OS" = "alma" ]; then
  WEB_USER=nginx; PHP_POOL_DIR_TPL='/etc/opt/remi/php%s/php-fpm.d'; PHP_SERVICE_TPL='php%s-php-fpm'; PHP_VER_FMT=compact
else
  WEB_USER=www-data; PHP_POOL_DIR_TPL='/etc/php/%s/fpm/pool.d'; PHP_SERVICE_TPL='php%s-fpm'; PHP_VER_FMT=dotted
fi
printf "DH_OS=%s\nDH_WEB_USER=%s\nDH_PHP_POOL_DIR_TPL=%s\nDH_PHP_SERVICE_TPL=%s\nDH_PHP_VER_FMT=%s\n" "$OS" "$WEB_USER" "$PHP_POOL_DIR_TPL" "$PHP_SERVICE_TPL" "$PHP_VER_FMT" > "$ETC/os.env"

pkg_install(){ if [ "$OS" = "alma" ]; then run dnf install -y "$@"; else run apt-get install -y "$@"; fi; }
svc_enable_restart(){ run systemctl enable "$1"; run systemctl restart "$1"; }

# ===== 2. Resource gate =====
MEM_KB=$(awk '/MemTotal/{print $2}' /proc/meminfo)
[ "$MEM_KB" -ge 1900000 ] || die "need at least 2 GB RAM"
DISK_GB=$(df -BG / | awk 'NR==2{print $4}' | tr -dc '0-9')
[ "$DISK_GB" -ge 20 ] || die "need at least 20 GB free on /"

# ===== 3. Fresh-server gate =====
if ! done_step gate.fresh; then
  for svc in nginx apache2 httpd lshttpd mysqld mariadbd postfix exim4 cpsrvd; do
    if pgrep -x "$svc" > /dev/null 2>&1; then
      [ "${DH_FORCE:-0}" = "1" ] || die "$svc is running. This looks like a used server. DirhamHost installs on fresh machines only. Set DH_FORCE=1 to override if you truly know what you are doing."
    fi
  done
  [ -d /usr/local/cpanel ] && [ "${DH_FORCE:-0}" != "1" ] && die "cPanel detected at /usr/local/cpanel. DirhamHost does not install on cPanel servers; use the importer from a fresh DirhamHost machine instead."
  [ "${DH_FORCE:-0}" = "1" ] && say "DH_FORCE set: fresh-server gate bypassed"
  mark gate.fresh
fi

# ===== 4. Identity =====
HOSTNAME_FQDN="${DH_HOSTNAME:-$(hostname -f 2>/dev/null || hostname)}"
case "$HOSTNAME_FQDN" in *.*) ;; *) say "hostname '$HOSTNAME_FQDN' is not an FQDN; panel SSL will be skipped unless DH_HOSTNAME is set"; DH_SKIP_SSL=1;; esac
ADMIN_EMAIL="${DH_EMAIL:-admin@${HOSTNAME_FQDN#*.}}"
PUBLIC_IP="$(curl -4 -fsS -m 8 https://api.ipify.org 2>/dev/null || hostname -I | awk '{print $1}')"

# ===== 5. Plan =====
say "plan:"
say "  os=$OS host=$HOSTNAME_FQDN ip=$PUBLIC_IP email=$ADMIN_EMAIL"
say "  install: nginx, php 8.1/8.2/8.3 fpm, mariadb 10.11, powerdns (mysql backend, local api), pure-ftpd, csf, acme.sh, dh-agent, omnio (staged)"
say "  open ports: ssh (as configured), 21, 53, 80, 443, $PANEL_PORT (panel), ftp passive 30000:30100"
say "  files: $ETC (config), $STATE (state, rendered templates, backups), /var/log/dirhamhost (audit), $LOG"
say "  re-runs are safe: completed steps are skipped"

# ===== 6. Base packages and repositories =====
if ! done_step base; then
  if [ "$OS" = "alma" ]; then
    run dnf install -y epel-release
    run dnf install -y dnf-utils ca-certificates curl gnupg2 unzip tar gzip jq socat cronie logrotate openssl git acl rsync policycoreutils-python-utils setools-console git goaccess
    # Remi repository for side by side PHP 8.1/8.2/8.3
    if ! rpm -q remi-release > /dev/null 2>&1; then
      run dnf install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm
    fi
    # MariaDB 10.11 official repository (AppStream ships 10.5)
    if [ ! -f /etc/yum.repos.d/mariadb.repo ]; then
      cat > /etc/yum.repos.d/mariadb.repo <<REPO
[mariadb]
name = MariaDB 10.11
baseurl = https://rpm.mariadb.org/10.11/rhel/9/\$basearch
gpgkey = https://rpm.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck = 1
module_hotfixes = 1
REPO
    fi
    run systemctl enable crond
    run systemctl start crond
  else
    run apt-get update -y
    run apt-get install -y ca-certificates curl gnupg lsb-release software-properties-common apt-transport-https unzip tar gzip jq socat cron logrotate unattended-upgrades openssl git acl rsync
    if ! grep -rq "ondrej/php" /etc/apt/sources.list.d 2>/dev/null; then run add-apt-repository -y ppa:ondrej/php; fi
    run apt-get update -y
  fi
  # runtime dir for php-fpm sockets, OS neutral
  printf "d /run/dirhamhost 0755 root root -\nd /run/dirhamhost/php 0755 root root -\n" > /etc/tmpfiles.d/dirhamhost.conf
  mkdir -p /run/dirhamhost/php
  mark base
fi

# ===== 7. Nginx =====
if ! done_step nginx; then
  if [ "$OS" = "alma" ]; then
    # use the default appstream nginx (1.20) so the EPEL ModSecurity v3 connector matches it.
    pkg_install nginx
    # WAF: ModSecurity v3 connector + OWASP CRS, and the engine config the vhosts include.
    pkg_install nginx-mod-modsecurity libmodsecurity mod_security_crs
    mkdir -p /etc/nginx/modsec
    cat > /etc/nginx/modsec/main.conf <<'MSC'
Include /etc/nginx/modsecurity.conf
Include /etc/httpd/modsecurity.d/crs-setup.conf
Include /usr/share/mod_modsecurity_crs/rules/*.conf
Include /etc/nginx/modsec/engine.conf
MSC
    [ -f /etc/nginx/modsec/engine.conf ] || echo "SecRuleEngine DetectionOnly" > /etc/nginx/modsec/engine.conf
    # AppStream nginx.conf carries a default server block; replace with a minimal conf that includes conf.d
    [ -f /etc/nginx/nginx.conf.orig ] || cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.orig
    cat > /etc/nginx/nginx.conf <<'NGX'
# DirhamHost nginx.conf (rendered by installer). Per site vhosts live in /etc/nginx/conf.d/dh-*.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events { worker_connections 4096; }
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"';
    access_log /var/log/nginx/access.log main;
    sendfile on; tcp_nopush on; keepalive_timeout 65; types_hash_max_size 4096; server_names_hash_bucket_size 128;
    include /etc/nginx/conf.d/*.conf;
}
NGX
  else
    pkg_install nginx
    rm -f /etc/nginx/sites-enabled/default
  fi
  mkdir -p /etc/nginx/dirhamhost /var/www/dirhamhost/suspended /var/www/dirhamhost/panel "$STATE/rendered"
  cat > /etc/nginx/conf.d/00-dirhamhost.conf <<'NGX'
# DirhamHost base settings (rendered by installer)
server_tokens off;
client_max_body_size 64m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
map $http_upgrade $connection_upgrade { default upgrade; '' close; }
gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /var/www/dirhamhost/suspended; location / { return 444; } }
NGX
  cat > /etc/nginx/dirhamhost/fastcgi-php.conf <<'NGX'
# DirhamHost php handler snippet (OS neutral)
fastcgi_split_path_info ^(.+\.php)(/.+)$;
try_files $fastcgi_script_name =404;
set $path_info $fastcgi_path_info;
fastcgi_param PATH_INFO $path_info;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 300;
NGX
  cat > /var/www/dirhamhost/suspended/index.html <<'HTML'
<!doctype html><html><head><meta charset="utf-8"><title>Account suspended</title>
<style>body{font-family:system-ui,sans-serif;background:#0b1120;color:#e9eef8;display:grid;place-items:center;height:100vh;margin:0}
.c{max-width:520px;text-align:center;padding:40px}h1{font-weight:600}p{color:#9aa3b5}</style></head>
<body><div class="c"><h1>This website is temporarily unavailable</h1><p>The hosting account is suspended. If you are the owner, please contact your hosting provider.</p></div></body></html>
HTML
  # raise the file descriptor limit via systemd so nginx workers (which inherit it) can use
  # 4096 worker_connections without the setrlimit call that SELinux would deny.
  mkdir -p /etc/systemd/system/nginx.service.d
  printf '[Service]\nLimitNOFILE=32768\n' > /etc/systemd/system/nginx.service.d/limits.conf
  systemctl daemon-reload
  run nginx -t
  svc_enable_restart nginx
  mark nginx
fi

# ===== 8. PHP-FPM 8.1 / 8.2 / 8.3 =====
if ! done_step php; then
  for v in 8.1 8.2 8.3; do
    if [ "$OS" = "alma" ]; then
      c="${v/./}"   # 81 82 83
      pkg_install "php$c-php-fpm" "php$c-php-cli" "php$c-php-mysqlnd" "php$c-php-gd" "php$c-php-mbstring" "php$c-php-xml" \
        "php$c-php-pecl-zip" "php$c-php-intl" "php$c-php-bcmath" "php$c-php-soap" "php$c-php-pecl-imagick" "php$c-php-opcache"
      svc_enable_restart "php$c-php-fpm"
    else
      pkg_install "php$v-fpm" "php$v-cli" "php$v-mysql" "php$v-curl" "php$v-gd" "php$v-mbstring" "php$v-xml" "php$v-zip" "php$v-intl" "php$v-bcmath" "php$v-soap" "php$v-imagick" "php$v-opcache" "php$v-readline"
      svc_enable_restart "php$v-fpm"
    fi
  done
  mark php
fi

# ===== 8b. Application runtimes: Node.js + extra Python for the app hosting feature =====
# Customers host Node.js and Python (WSGI/ASGI) apps as jailed systemd services proxied by
# nginx (ops app.*). Node runs from /usr/bin/node; each Python app gets its own venv, so we
# only need the interpreters here (gunicorn/uvicorn are installed per app into the venv).
if ! done_step apphost; then
  if [ "$OS" = "alma" ]; then
    run dnf module reset -y nodejs || true
    run dnf module enable -y nodejs:20 || true
    pkg_install nodejs npm
    # modern Python interpreters customers can target (3.9 ships with the base OS)
    pkg_install python3.11 python3.12 || true
  else
    pkg_install nodejs npm
    pkg_install python3.11 python3.11-venv python3.12 python3.12-venv || true
  fi
  # shared webroot so app domains can pass the acme http-01 challenge, and a home for
  # any extra Node majors added later by runtime installs
  mkdir -p /var/www/dirhamhost/acme /opt/dirhamhost/runtimes
  [ "$OS" = "alma" ] && command -v restorecon > /dev/null 2>&1 && restorecon -R /var/www/dirhamhost >> "$LOG" 2>&1 || true
  mark apphost
fi

# ===== 8c. Malware scanning: ClamAV on-demand (no resident clamd, no swap pressure) =====
# Scans are launched by ops malware.scan as memory-capped transient units; freshclam keeps
# signatures current on a timer. Infected files are quarantined by the agent under STATE.
if ! done_step clamav; then
  if [ "$OS" = "alma" ]; then
    pkg_install clamav clamav-update
  else
    pkg_install clamav clamav-freshclam
  fi
  # freshclam ships disabled behind an Example line; drop it and fetch signatures once
  [ -f /etc/freshclam.conf ] && sed -i "/^Example/d" /etc/freshclam.conf
  mkdir -p "$STATE/quarantine" "$STATE/malware"
  # let the confined clamscan read all account content (it still cannot write; the agent
  # does the quarantine move). This policy rebuild is slow, so run it once at install.
  if [ "$OS" = "alma" ]; then
    setsebool -P antivirus_can_scan_system 1 >> "$LOG" 2>&1 || true
  fi
  freshclam >> "$LOG" 2>&1 || true
  # keep signatures fresh and scan every account nightly (staggered so they do not pile up)
  if systemctl enable clamav-freshclam.service >> "$LOG" 2>&1; then
    systemctl start clamav-freshclam.service >> "$LOG" 2>&1 || true
  else
    cat > /etc/cron.d/dh-freshclam <<'CRON'
17 */6 * * * root /usr/bin/freshclam > /dev/null 2>&1
CRON
  fi
  cat > /etc/cron.d/dh-malware-scan <<CRON
30 3 * * * root APPLY=1 /opt/dirhamhost/bin/dhctl op malware.scan.all > /dev/null 2>&1
CRON
  mark clamav
fi

# ===== 8d. phpMyAdmin: served on the panel at /phpmyadmin, cookie login with DB creds =====
if ! done_step phpmyadmin; then
  pkg_install phpMyAdmin || true
  if [ -d /usr/share/phpMyAdmin ]; then
    # dedicated php-fpm pool (runs as the web user; MySQL auth is the customer via cookie)
    POOLDIR=$(ls -d /etc/opt/remi/php83/php-fpm.d /etc/php/8.3/fpm/pool.d 2>/dev/null | head -1)
    ln -sfn /usr/share/phpMyAdmin /var/www/dirhamhost/panel/phpmyadmin
    cat > "$POOLDIR/phpmyadmin.conf" <<POOL
[phpmyadmin]
user = $WEB_USER
group = $WEB_USER
listen = /run/dirhamhost/php/phpmyadmin.sock
listen.owner = $WEB_USER
listen.group = $WEB_USER
pm = ondemand
pm.max_children = 6
php_admin_value[open_basedir] = /var/www/dirhamhost/panel/phpmyadmin:/usr/share/phpMyAdmin:/etc/phpMyAdmin:/var/lib/phpMyAdmin:/tmp:/usr/share/php
php_admin_value[session.save_path] = /var/lib/phpMyAdmin/temp
php_admin_value[upload_tmp_dir] = /var/lib/phpMyAdmin/temp
POOL
    mkdir -p /var/lib/phpMyAdmin/temp && chown -R "$WEB_USER":"$WEB_USER" /var/lib/phpMyAdmin
    PMASECRET=$(openssl rand -hex 16)
    cat > /etc/phpMyAdmin/config.inc.php <<PHP
<?php
\$cfg['blowfish_secret'] = '$PMASECRET';
\$i = 0; \$i++;
\$cfg['Servers'][\$i]['auth_type'] = 'cookie';
\$cfg['Servers'][\$i]['host'] = '127.0.0.1';
\$cfg['Servers'][\$i]['AllowNoPassword'] = false;
\$cfg['TempDir'] = '/var/lib/phpMyAdmin/temp';
PHP
    if [ "$OS" = "alma" ]; then
      chcon -h system_u:object_r:httpd_sys_content_t:s0 /var/www/dirhamhost/panel/phpmyadmin 2>/dev/null || true
      setsebool -P httpd_can_network_connect_db 1 >> "$LOG" 2>&1 || true
    fi
    svc_enable_restart "$(ls /etc/opt/remi/php83/php-fpm.d >/dev/null 2>&1 && echo php83-php-fpm || echo php8.3-fpm)" 2>/dev/null || true
  fi
  mark phpmyadmin
fi

# ===== 9. MariaDB 10.11 =====
if ! done_step mariadb; then
  if [ "$OS" = "alma" ]; then pkg_install MariaDB-server MariaDB-client; else pkg_install mariadb-server mariadb-client; fi
  svc_enable_restart mariadb
  mysql <<'SQL'
DELETE FROM mysql.user WHERE User='';
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost','127.0.0.1','::1');
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
FLUSH PRIVILEGES;
SQL
  CNF_DIR=/etc/mysql/mariadb.conf.d; [ "$OS" = "alma" ] && CNF_DIR=/etc/my.cnf.d
  mkdir -p "$CNF_DIR"
  cat > "$CNF_DIR/60-dirhamhost.cnf" <<'CNF'
[mysqld]
bind-address = 127.0.0.1
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
max_connections = 300
innodb_buffer_pool_size = 256M
CNF
  run systemctl restart mariadb
  mark mariadb
fi

# ===== 10. PowerDNS authoritative, MySQL backend, local API =====
if ! done_step pdns; then
  if [ -f /etc/systemd/resolved.conf ] && systemctl is-active systemd-resolved > /dev/null 2>&1; then
    mkdir -p /etc/systemd/resolved.conf.d
    printf "[Resolve]\nDNSStubListener=no\n" > /etc/systemd/resolved.conf.d/dirhamhost.conf
    run systemctl restart systemd-resolved
    [ -L /etc/resolv.conf ] && ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
  fi
  pkg_install pdns pdns-backend-mysql
  PDNS_DB_PASS="$(openssl rand -hex 16)"; PDNS_API_KEY="$(openssl rand -hex 24)"
  mysql <<SQL
CREATE DATABASE IF NOT EXISTS pdns CHARACTER SET utf8mb4;
CREATE USER IF NOT EXISTS 'pdns'@'localhost' IDENTIFIED BY '$PDNS_DB_PASS';
ALTER USER 'pdns'@'localhost' IDENTIFIED BY '$PDNS_DB_PASS';
GRANT ALL PRIVILEGES ON pdns.* TO 'pdns'@'localhost';
FLUSH PRIVILEGES;
SQL
  SCHEMA="$(find /usr/share/doc /usr/share/pdns-backend-mysql -name 'schema.mysql.sql' 2>/dev/null | head -1)"
  [ -n "$SCHEMA" ] || die "PowerDNS mysql schema file not found"
  if ! mysql pdns -e "SHOW TABLES LIKE 'domains'" | grep -q domains; then run sh -c "mysql pdns < $SCHEMA"; fi
  if [ "$OS" = "alma" ]; then PDNS_DIR=/etc/pdns; else PDNS_DIR=/etc/powerdns; fi
  mkdir -p "$PDNS_DIR/pdns.d"
  grep -q "^include-dir=" "$PDNS_DIR/pdns.conf" 2>/dev/null || echo "include-dir=$PDNS_DIR/pdns.d" >> "$PDNS_DIR/pdns.conf"
  sed -i 's/^launch=.*/# launch set in pdns.d\/dirhamhost.conf/' "$PDNS_DIR/pdns.conf"
  rm -f "$PDNS_DIR/pdns.d/bind.conf"
  cat > "$PDNS_DIR/pdns.d/dirhamhost.conf" <<CONF
launch=gmysql
gmysql-host=127.0.0.1
gmysql-user=pdns
gmysql-password=$PDNS_DB_PASS
gmysql-dbname=pdns
gmysql-dnssec=yes
api=yes
api-key=$PDNS_API_KEY
webserver=yes
webserver-address=127.0.0.1
webserver-port=8081
webserver-allow-from=127.0.0.1
local-address=0.0.0.0, ::
default-soa-content=ns1.$HOSTNAME_FQDN hostmaster.@ 0 10800 3600 604800 3600
CONF
  # pdns_server drops to the pdns user, so the file with the db password and api key
  # must be group readable by pdns (matches how the package ships pdns.conf itself).
  chgrp pdns "$PDNS_DIR/pdns.d/dirhamhost.conf" 2>/dev/null || true
  chmod 640 "$PDNS_DIR/pdns.d/dirhamhost.conf"
  printf "PDNS_API_KEY=%s\nPDNS_API_URL=http://127.0.0.1:8081\n" "$PDNS_API_KEY" > "$ETC/pdns.env"; chmod 600 "$ETC/pdns.env"
  svc_enable_restart pdns
  mark pdns
fi

# ===== 11. Pure-FTPd (virtual users) =====
if ! done_step ftp; then
  pkg_install pure-ftpd
  if [ "$OS" = "alma" ]; then
    CONF=/etc/pure-ftpd/pure-ftpd.conf
    sed -i 's/^#\?ChrootEveryone .*/ChrootEveryone yes/; s/^#\?NoAnonymous .*/NoAnonymous yes/; s/^#\?PassivePortRange .*/PassivePortRange 30000 30100/; s/^#\?TLS .*/TLS 1/' "$CONF"
    grep -q "^PureDB" "$CONF" || echo "PureDB /etc/pure-ftpd/pureftpd.pdb" >> "$CONF"
    sed -i 's/^# *PureDB/PureDB/' "$CONF"
    mkdir -p /etc/pki/pure-ftpd; CERT=/etc/pki/pure-ftpd/pure-ftpd.pem
    grep -q "^CertFile" "$CONF" || echo "CertFile $CERT" >> "$CONF"
  else
    mkdir -p /etc/pure-ftpd/conf
    echo yes > /etc/pure-ftpd/conf/ChrootEveryone; echo yes > /etc/pure-ftpd/conf/NoAnonymous
    echo "30000 30100" > /etc/pure-ftpd/conf/PassivePortRange; echo 1 > /etc/pure-ftpd/conf/TLS
    echo /etc/pure-ftpd/pureftpd.pdb > /etc/pure-ftpd/conf/PureDB
    ln -sf /etc/pure-ftpd/conf/PureDB /etc/pure-ftpd/auth/50pure 2>/dev/null || true
    CERT=/etc/ssl/private/pure-ftpd.pem
  fi
  [ -f /etc/pure-ftpd/pureftpd.passwd ] || touch /etc/pure-ftpd/pureftpd.passwd
  pure-pw mkdb >> "$LOG" 2>&1 || true
  if [ ! -f "$CERT" ]; then
    openssl req -x509 -nodes -newkey rsa:2048 -days 3650 -subj "/CN=$HOSTNAME_FQDN" -keyout "$CERT" -out "$CERT" >> "$LOG" 2>&1
    chmod 600 "$CERT"
  fi
  svc_enable_restart pure-ftpd
  mark ftp
fi

# ===== 12. CSF firewall =====
if ! done_step csf && [ "${DH_SKIP_CSF:-0}" != "1" ]; then
  if [ "$OS" = "alma" ]; then
    # perl-core brings the standard modules CSF assumes (File::Find etc) that are split out on minimal EL9;
    # iptables-nft provides the /usr/sbin/iptables command CSF requires (nftables is the EL9 default);
    # ipset lets lfd and country blocking use efficient kernel sets instead of long iptables chains.
    pkg_install perl-core perl-File-Find perl-libwww-perl perl-LWP-Protocol-https perl-GDGraph perl-Math-BigInt iptables-services iptables-nft ipset
    systemctl stop firewalld >> "$LOG" 2>&1 || true; systemctl disable firewalld >> "$LOG" 2>&1 || true; systemctl mask firewalld >> "$LOG" 2>&1 || true
    # rpcbind (portmapper, port 111) is unused on a hosting box and a needless attack surface.
    systemctl disable --now rpcbind rpcbind.socket >> "$LOG" 2>&1 || true; systemctl mask rpcbind rpcbind.socket >> "$LOG" 2>&1 || true
  else
    systemctl disable --now rpcbind rpcbind.socket >> "$LOG" 2>&1 || true; systemctl mask rpcbind rpcbind.socket >> "$LOG" 2>&1 || true
    pkg_install libwww-perl liblwp-protocol-https-perl libgd-graph-perl sendmail-bin
  fi
  if [ ! -d /etc/csf ]; then
    # ConfigServer (Way to the Web) shut down 31 Aug 2025 and download.configserver.com is gone.
    # Default to the maintained community continuation; override with DH_CSF_URL to pin another mirror.
    CSF_URL="${DH_CSF_URL:-https://download.configserver.dev/csf.tgz}"
    cd /usr/src && rm -rf csf csf.tgz
    run curl -fsSL -o csf.tgz "$CSF_URL"
    run tar -xzf csf.tgz
    run sh -c "cd /usr/src/csf && sh install.sh"
    cd /
  fi
  SSH_PORT="$(awk '/^Port /{print $2}' /etc/ssh/sshd_config 2>/dev/null | head -1)"; SSH_PORT="${SSH_PORT:-22}"
  sed -i "s/^TESTING = .*/TESTING = \"0\"/" /etc/csf/csf.conf
  sed -i "s/^TCP_IN = .*/TCP_IN = \"$SSH_PORT,21,25,53,80,443,465,587,993,995,$PANEL_PORT,30000:30100\"/" /etc/csf/csf.conf
  sed -i "s/^TCP_OUT = .*/TCP_OUT = \"$SSH_PORT,21,25,53,80,113,443,465,587,993,995,8081,2083,2087\"/" /etc/csf/csf.conf
  sed -i "s/^UDP_IN = .*/UDP_IN = \"53\"/" /etc/csf/csf.conf
  sed -i "s/^UDP_OUT = .*/UDP_OUT = \"53,113,123\"/" /etc/csf/csf.conf
  sed -i "s/^RESTRICT_SYSLOG = .*/RESTRICT_SYSLOG = \"3\"/" /etc/csf/csf.conf
  # Watch the panel and owner-console auth-fail logs so lfd bans web login brute force
  # (an IP that fails 5 times is dropped at the firewall for an hour, on top of the
  # app level lockout). See internal/authguard and the dh-api/dh-license AUTHFAIL logs.
  mkdir -p /var/lib/dirhamhost/api /var/lib/dh-license
  touch /var/lib/dirhamhost/api/auth-fail.log /var/lib/dh-license/auth-fail.log
  sed -i 's#^CUSTOM1_LOG = .*#CUSTOM1_LOG = "/var/lib/dirhamhost/api/auth-fail.log"#' /etc/csf/csf.conf
  sed -i 's#^CUSTOM2_LOG = .*#CUSTOM2_LOG = "/var/lib/dh-license/auth-fail.log"#' /etc/csf/csf.conf
  cat > /usr/local/csf/bin/regex.custom.pm <<'PERL'
#!/usr/bin/perl
# DirhamHost custom lfd trigger: ban IPs that brute force the panel or owner console.
sub custom_line {
    my $line = shift;
    my $lgfile = shift;
    if ($line =~ /AUTHFAIL ip=(\d+\.\d+\.\d+\.\d+)/) {
        return ("DirhamHost login brute force",$1,"dirhamauth","5","80,443,8443,9443","3600");
    }
    return 0;
}
1;
PERL
  chmod 700 /usr/local/csf/bin/regex.custom.pm
  run csf -r
  run systemctl enable csf lfd
  # enable only leaves them dead until reboot; start now so rules persist and lfd (brute force) runs.
  run systemctl start csf
  run systemctl start lfd
  mark csf
fi

# ===== 13. acme.sh =====
if ! done_step acme; then
  [ -x /root/.acme.sh/acme.sh ] || run sh -c "curl -fsSL https://get.acme.sh | sh -s email=$ADMIN_EMAIL"
  mark acme
fi

# ===== 14. SELinux (AlmaLinux): keep enforcing, label what we use =====
if ! done_step selinux && [ "$OS" = "alma" ]; then
  if command -v getenforce > /dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then
    setsebool -P httpd_enable_homedirs 1 >> "$LOG" 2>&1 || true
    setsebool -P httpd_can_network_connect 1 >> "$LOG" 2>&1 || true
    setsebool -P httpd_execmem 1 >> "$LOG" 2>&1 || true
    semanage fcontext -a -t httpd_sys_rw_content_t "/home/[^/]+/public_html(/.*)?" >> "$LOG" 2>&1 || true
    semanage fcontext -a -t httpd_var_run_t "/run/dirhamhost/php(/.*)?" >> "$LOG" 2>&1 || true
    semanage fcontext -a -t httpd_config_t "/etc/nginx/dirhamhost(/.*)?" >> "$LOG" 2>&1 || true
    # panel and suspended pages live under /var/www/dirhamhost; without this label nginx gets 403 (var_t).
    semanage fcontext -a -t httpd_sys_content_t "/var/www/dirhamhost(/.*)?" >> "$LOG" 2>&1 || true
    restorecon -R /run/dirhamhost /etc/nginx/dirhamhost /var/www/dirhamhost >> "$LOG" 2>&1 || true
    semanage port -a -t http_port_t -p tcp "$PANEL_PORT" >> "$LOG" 2>&1 || semanage port -m -t http_port_t -p tcp "$PANEL_PORT" >> "$LOG" 2>&1 || true
    say "selinux: enforcing, contexts and booleans applied"
  fi
  mark selinux
fi

# ===== 15. dh-agent service =====
if ! done_step agent; then
  mkdir -p /opt/dirhamhost/bin /var/log/dirhamhost
  getent group dh-panel > /dev/null || run groupadd -r dh-panel
  getent group dh-api > /dev/null || run groupadd -r dh-api
  id -u dh-api > /dev/null 2>&1 || run useradd -r -M -s /usr/sbin/nologin -g dh-api -G dh-panel dh-api
  if [ -n "${DH_SRC:-}" ] && [ -f "$DH_SRC/go.mod" ]; then
    say "building dh-agent, dhctl and dh-api from source at $DH_SRC"
    if [ "$OS" = "alma" ]; then pkg_install golang; else pkg_install golang-go; fi
    run sh -c "cd $DH_SRC && go build -o /opt/dirhamhost/bin/dh-agent ./cmd/dh-agent && go build -o /opt/dirhamhost/bin/dhctl ./cmd/dhctl && go build -o /opt/dirhamhost/bin/dh-api ./cmd/dh-api"
  else
    run curl -fsSL -o /opt/dirhamhost/bin/dh-agent "${DH_AGENT_URL:-https://get.dirhamaday.ae/bin/dh-agent-linux-$ARCH}"
    run curl -fsSL -o /opt/dirhamhost/bin/dhctl "${DH_CTL_URL:-https://get.dirhamaday.ae/bin/dhctl-linux-$ARCH}"
    run curl -fsSL -o /opt/dirhamhost/bin/dh-api "${DH_API_URL:-https://get.dirhamaday.ae/bin/dh-api-linux-$ARCH}"
  fi
  chmod 750 /opt/dirhamhost/bin/dh-agent /opt/dirhamhost/bin/dhctl /opt/dirhamhost/bin/dh-api
  # On SELinux (alma) the binaries land mislabelled and systemd cannot exec them; label as bin_t.
  if [ "$OS" = "alma" ] && command -v getenforce > /dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then
    semanage fcontext -a -t bin_t "/opt/dirhamhost/bin(/.*)?" >> "$LOG" 2>&1 || semanage fcontext -m -t bin_t "/opt/dirhamhost/bin(/.*)?" >> "$LOG" 2>&1 || true
    restorecon -R /opt/dirhamhost/bin >> "$LOG" 2>&1 || true
  fi
  ln -sf /opt/dirhamhost/bin/dhctl /usr/local/bin/dhctl
  cat > /etc/systemd/system/dh-agent.service <<'UNIT'
[Unit]
Description=DirhamHost privileged agent (local unix socket only)
After=network.target mariadb.service

[Service]
Type=simple
User=root
EnvironmentFile=/etc/dirhamhost/os.env
Environment=DH_ETC=/etc/dirhamhost
Environment=DH_STATE=/var/lib/dirhamhost
ExecStart=/opt/dirhamhost/bin/dh-agent
ExecStartPost=/bin/sh -c 'sleep 1; chgrp dh-panel /run/dh-agent.sock && chmod 0660 /run/dh-agent.sock'
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
UNIT
  # dh-api: the panel web backend on 127.0.0.1:8800 (nginx proxies /api/ to it). It forwards
  # to dh-agent over the socket and never runs privileged actions itself, so it runs as the
  # unprivileged dh-api user (member of dh-panel for the agent socket) with a systemd sandbox.
  cat > /etc/systemd/system/dh-api.service <<'UNIT'
[Unit]
Description=DirhamPanel web API (localhost only; forwards to dh-agent)
After=network.target dh-agent.service
Wants=dh-agent.service

[Service]
Type=simple
User=dh-api
Group=dh-api
SupplementaryGroups=dh-panel
EnvironmentFile=/etc/dirhamhost/os.env
Environment=DH_ETC=/etc/dirhamhost
Environment=DH_STATE=/var/lib/dirhamhost
ExecStart=/opt/dirhamhost/bin/dh-api
Restart=always
RestartSec=2
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=full
ProtectControlGroups=yes
ProtectKernelTunables=yes
RestrictSUIDSGID=yes

[Install]
WantedBy=multi-user.target
UNIT
  # dh-api runs unprivileged, so give it exactly what it needs: its own state dir, read on
  # the admin bootstrap file, and read on the two secrets it mints browser/webmail tokens with.
  mkdir -p /var/lib/dirhamhost/api
  chown -R dh-api:dh-api /var/lib/dirhamhost/api
  [ -f /etc/dirhamhost/admin.initial ] && chown dh-api /etc/dirhamhost/admin.initial || true
  # dh-api cannot write /etc as non-root, so create the WHMCS API key here (read by dh-panel group).
  if [ ! -f /etc/dirhamhost/whmcs-api.key ]; then
    openssl rand -hex 32 > /etc/dirhamhost/whmcs-api.key
  fi
  chgrp dh-panel /etc/dirhamhost/whmcs-api.key && chmod 640 /etc/dirhamhost/whmcs-api.key
  # payment gateway credential files: pre-create empty and readable by the dh-api service
  # (group dh-panel, mode 640) so the vendor can just edit them and restart dh-api. Stripe:
  # secret key + webhook signing secret. PayPal: "clientid:secret" on line 1 (line 2 "sandbox"
  # to test). Empty means the gateway is simply off until configured.
  for f in stripe.secret stripe.webhook paypal.secret; do
    [ -f /etc/dirhamhost/$f ] || : > /etc/dirhamhost/$f
    chgrp dh-panel /etc/dirhamhost/$f && chmod 640 /etc/dirhamhost/$f
  done
  # omnio-sso.secret is group omnio (for the webmail service); add a read ACL for dh-api.
  [ -f /etc/dirhamhost/omnio-sso.secret ] && setfacl -m u:dh-api:r /etc/dirhamhost/omnio-sso.secret || true
  run systemctl daemon-reload
  svc_enable_restart dh-agent
  svc_enable_restart dh-api
  mark agent
fi

# ===== 16. Omnio webmail suite staged (installed per brand by op webmail.provision) =====
if ! done_step omnio.stage; then
  mkdir -p /opt/dirhamhost/omnio
  if [ -n "${DH_SRC:-}" ] && [ -d "$DH_SRC/../../omnio" ]; then
    run rsync -a "$DH_SRC/../../omnio/" /opt/dirhamhost/omnio/
  else
    run sh -c "curl -fsSL ${DH_OMNIO_URL:-https://get.dirhamaday.ae/omnio-suite.zip} -o /tmp/omnio.zip && unzip -q -o /tmp/omnio.zip -d /opt/dirhamhost/omnio"
  fi
  mark omnio.stage
fi

# ===== 17. Panel vhost on $PANEL_PORT + SSL =====
if ! done_step panel; then
  mkdir -p "$ETC/ssl"
  # A cert file must exist before nginx can load the :$PANEL_PORT ssl block. Start with a self-signed
  # placeholder; the acme step below replaces it in place once the challenge (served by this vhost on
  # port 80) succeeds. Rendering the vhost BEFORE acme issue is what makes the http-01 webroot reachable.
  if [ ! -f "$ETC/ssl/panel.crt" ]; then
    openssl req -x509 -nodes -newkey rsa:2048 -days 3650 -subj "/CN=$HOSTNAME_FQDN" -keyout "$ETC/ssl/panel.key" -out "$ETC/ssl/panel.crt" >> "$LOG" 2>&1
  fi
  chmod 600 "$ETC/ssl/panel.key"
  cat > /var/www/dirhamhost/panel/index.html <<HTML
<!doctype html><html><head><meta charset="utf-8"><title>DirhamHost</title>
<style>body{font-family:system-ui,sans-serif;background:#0b1120;color:#e9eef8;display:grid;place-items:center;height:100vh;margin:0}
.c{max-width:560px;text-align:center;padding:40px}h1{font-weight:600}p{color:#9aa3b5}code{color:#e8c87c}</style></head>
<body><div class="c"><h1>DirhamHost is installed on $HOSTNAME_FQDN</h1>
<p>The server stack is ready. The web panel (milestone M4) will appear here. Until then, manage accounts with <code>dhctl</code> on the server.</p></div></body></html>
HTML
  # ship the real DirhamPanel front-end when building from source (falls back to the placeholder above).
  if [ -n "${DH_SRC:-}" ] && [ -f "$DH_SRC/web/panel/index.html" ]; then
    run rsync -a "$DH_SRC/web/panel/" /var/www/dirhamhost/panel/
    say "DirhamPanel admin and end user web UI installed"
  else
    run sh -c "curl -fsSL ${DH_PANEL_URL:-https://get.dirhamaday.ae/panel.tar.gz} | tar xz -C /var/www/dirhamhost/panel"
    say "DirhamPanel web UI installed"
  fi
  cat > /etc/nginx/conf.d/dh-panel.conf <<NGX
# DirhamHost panel on :$PANEL_PORT (rendered by installer)
server {
    listen $PANEL_PORT ssl http2;
    listen [::]:$PANEL_PORT ssl http2;
    server_name $HOSTNAME_FQDN _;
    ssl_certificate $ETC/ssl/panel.crt;
    ssl_certificate_key $ETC/ssl/panel.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://s.wordpress.com; connect-src 'self' wss:; frame-ancestors 'none'; base-uri 'self'; object-src 'none'; form-action 'self'" always;
    root /var/www/dirhamhost/panel;
    index index.html;
    location /api/ { proxy_pass http://127.0.0.1:8800; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection \$connection_upgrade; proxy_read_timeout 3600s; proxy_set_header Host \$host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Real-IP \$remote_addr; }
    location /phpmyadmin { index index.php; }
    location ~ ^/phpmyadmin/.+\.php\$ { include /etc/nginx/dirhamhost/fastcgi-php.conf; fastcgi_pass unix:/run/dirhamhost/php/phpmyadmin.sock; }
}
server {
    listen 80;
    listen [::]:80;
    server_name $HOSTNAME_FQDN;
    root /var/www/dirhamhost/panel;
    location /.well-known/acme-challenge/ { try_files \$uri =404; }
    location / { return 301 https://\$host:$PANEL_PORT\$request_uri; }
}
NGX
  # index.html is written after the selinux step ran, so relabel it now (no-op off alma / selinux disabled).
  [ "$OS" = "alma" ] && command -v restorecon > /dev/null 2>&1 && restorecon -R /var/www/dirhamhost >> "$LOG" 2>&1 || true
  run nginx -t
  run systemctl reload nginx
  # Now that the port 80 vhost serves /.well-known/acme-challenge/ for $HOSTNAME_FQDN, request the real cert.
  if [ "${DH_SKIP_SSL:-0}" != "1" ]; then
    # acme.sh only accepts long flags; they are assembled at run time to honour the project rule of no literal double hyphens in files.
    DD="$(printf '%s%s' - -)"
    /root/.acme.sh/acme.sh "${DD}set-default-ca" "${DD}server" letsencrypt >> "$LOG" 2>&1 || true
    if /root/.acme.sh/acme.sh "${DD}issue" -d "$HOSTNAME_FQDN" -w /var/www/dirhamhost/panel "${DD}keylength" ec-256 >> "$LOG" 2>&1; then
      /root/.acme.sh/acme.sh "${DD}install-cert" -d "$HOSTNAME_FQDN" "${DD}ecc" "${DD}key-file" "$ETC/ssl/panel.key" "${DD}fullchain-file" "$ETC/ssl/panel.crt" "${DD}reloadcmd" "systemctl reload nginx" >> "$LOG" 2>&1 || say "cert installed but reload hook failed; check $LOG"
      say "panel certificate issued for $HOSTNAME_FQDN"
    else
      say "certificate for $HOSTNAME_FQDN not issued (DNS must point at $PUBLIC_IP and port 80 be reachable); keeping self-signed, re-run installer later"
    fi
  fi
  mark panel
fi

# ===== 17b. Nightly scheduled backups + retention =====
if ! done_step backups.cron; then
  cat > /usr/local/bin/dh-backup-all.sh <<'SH'
#!/usr/bin/env bash
# Nightly backups. When the DirhamVault add-on is enabled it runs the off-site backup
# (archive + push to the configured destination + prune); otherwise the base local backup.
set -eu
DVCONF=/etc/dirhamhost/dirhamvault/config.json
DHCTL=/opt/dirhamhost/bin/dhctl
if [ -f "$DVCONF" ] && grep -q '"enabled": true' "$DVCONF"; then
  APPLY=1 "$DHCTL" op dv.backup.run > /dev/null 2>&1 || true
  exit 0
fi
ACCTS=/var/lib/dirhamhost/accounts
BK=/var/lib/dirhamhost/backups
[ -d "$ACCTS" ] || exit 0
for f in "$ACCTS"/*.json; do
  [ -e "$f" ] || continue
  u="$(basename "$f" .json)"
  APPLY=1 "$DHCTL" op account.backup username="$u" > /dev/null 2>&1 || true
done
find "$BK" -type f -name '*.tar.gz' -mtime +14 -delete 2>/dev/null || true
SH
  chmod 750 /usr/local/bin/dh-backup-all.sh
  printf '# DirhamHost nightly backups\n30 2 * * * root /usr/local/bin/dh-backup-all.sh\n' > /etc/cron.d/dirhamhost-backups
  chmod 644 /etc/cron.d/dirhamhost-backups
  mark backups.cron
fi

# ===== 16d. Licensing: point the node at the vendor license server, check in daily,
# enforce hourly. A fresh install with no key auto obtains a 14 day trial on first
# check in; once the trial or a paid term lapses past the grace, services are parked. =====
if ! done_step license.cron; then
  if [ ! -f "$ETC/license.env" ]; then
    printf 'DH_LICENSE_URL=%s\n' "${DH_LICENSE_URL:-https://license.dirhamaday.ae}" > "$ETC/license.env"
    chmod 640 "$ETC/license.env"
  fi
  # First check in now so the node gets its trial (best effort; ignores a transient outage).
  APPLY=1 /opt/dirhamhost/bin/dhctl op license.checkin > /dev/null 2>&1 || true
  cat > /etc/cron.d/dirhamhost-license <<'CRON'
# DirhamHost licensing: daily check in, hourly enforcement
17 3 * * * root APPLY=1 /usr/local/bin/dhctl op license.checkin > /dev/null 2>&1
30 * * * * root APPLY=1 /usr/local/bin/dhctl op license.enforce > /dev/null 2>&1
CRON
  chmod 644 /etc/cron.d/dirhamhost-license
  mark license.cron
fi

# ===== 16e. Automatic updates: apply signed DirhamHost releases nightly when enabled =====
# The op is a no-op unless the owner turned automatic updates on in the panel (Updates page).
# It fetches the signed manifest from the update channel, verifies the Ed25519 signature, and
# only then installs and restarts. Manual "Update now" from the panel works regardless.
if ! done_step update.cron; then
  cat > /etc/cron.d/dirhamhost-update <<'CRON'
# DirhamHost self-update: nightly, only acts when automatic updates are enabled
23 4 * * * root APPLY=1 /usr/local/bin/dhctl op update.autorun > /dev/null 2>&1
CRON
  chmod 644 /etc/cron.d/dirhamhost-update
  mark update.cron
fi

# ===== 16f. Resource monitoring: sample server + per-account usage every 15 minutes =====
if ! done_step monitor.cron; then
  cat > /etc/cron.d/dirhamhost-monitor <<'CRON'
# DirhamHost resource monitoring: one time-series sample every 15 minutes
*/15 * * * * root APPLY=1 /usr/local/bin/dhctl op monitor.sample > /dev/null 2>&1
CRON
  chmod 644 /etc/cron.d/dirhamhost-monitor
  # take one sample immediately so the panel has data on first view
  APPLY=1 /opt/dirhamhost/bin/dhctl op monitor.sample > /dev/null 2>&1 || true
  mark monitor.cron
fi

# ===== 17c. Mail stack (Postfix + Dovecot, MariaDB virtual mailboxes) =====
if ! done_step mail; then
  if [ "$OS" = "alma" ]; then pkg_install postfix postfix-mysql dovecot dovecot-mysql dovecot-pigeonhole; else pkg_install postfix postfix-mysql dovecot-core dovecot-imapd dovecot-lmtpd dovecot-mysql dovecot-sieve dovecot-managesieved; fi
  # Sieve on LMTP delivery so customer autoresponders + email filters apply
  cat > /etc/dovecot/conf.d/90-dh-sieve.conf <<'SIEVE'
protocol lmtp {
  mail_plugins = $mail_plugins sieve
}
plugin {
  sieve = file:/home/vmail/%d/%n/sieve;active=/home/vmail/%d/%n/.dovecot.sieve
  sieve_default_name = active
}
SIEVE
  getent group vmail > /dev/null || groupadd -g 5000 vmail
  id vmail > /dev/null 2>&1 || useradd -r -u 5000 -g 5000 -d /home/vmail -m -s /usr/sbin/nologin vmail
  mkdir -p /home/vmail; chown vmail:vmail /home/vmail
  MAIL_DB_PASS="$(openssl rand -hex 16)"
  mysql <<SQL
CREATE DATABASE IF NOT EXISTS mailserver CHARACTER SET utf8mb4;
CREATE TABLE IF NOT EXISTS mailserver.virtual_domains (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL UNIQUE);
CREATE TABLE IF NOT EXISTS mailserver.virtual_users (id INT AUTO_INCREMENT PRIMARY KEY, domain_id INT NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, quota_mb INT DEFAULT 1024, FOREIGN KEY (domain_id) REFERENCES mailserver.virtual_domains(id) ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS mailserver.virtual_aliases (id INT AUTO_INCREMENT PRIMARY KEY, domain_id INT NOT NULL, source VARCHAR(255) NOT NULL, destination VARCHAR(255) NOT NULL, FOREIGN KEY (domain_id) REFERENCES mailserver.virtual_domains(id) ON DELETE CASCADE);
CREATE USER IF NOT EXISTS 'mailuser'@'localhost' IDENTIFIED BY '$MAIL_DB_PASS';
ALTER USER 'mailuser'@'localhost' IDENTIFIED BY '$MAIL_DB_PASS';
GRANT SELECT ON mailserver.* TO 'mailuser'@'localhost';
FLUSH PRIVILEGES;
SQL
  printf "MAIL_DB=mailserver\nMAIL_DB_USER=mailuser\nMAIL_DB_PASS=%s\n" "$MAIL_DB_PASS" > "$ETC/mail.env"; chmod 600 "$ETC/mail.env"
  for q in "domains:SELECT 1 FROM virtual_domains WHERE name = '%s'" "maps:SELECT 1 FROM virtual_users WHERE email = '%s'"; do :; done
  cat > /etc/postfix/mysql-virtual-mailbox-domains.cf <<CF
user = mailuser
password = $MAIL_DB_PASS
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_domains WHERE name = '%s'
CF
  cat > /etc/postfix/mysql-virtual-mailbox-maps.cf <<CF
user = mailuser
password = $MAIL_DB_PASS
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_users WHERE email = '%s'
CF
  cat > /etc/postfix/mysql-virtual-alias-maps.cf <<CF
user = mailuser
password = $MAIL_DB_PASS
hosts = 127.0.0.1
dbname = mailserver
query = SELECT destination FROM virtual_aliases WHERE source = '%s'
CF
  chmod 640 /etc/postfix/mysql-virtual-*.cf; chgrp postfix /etc/postfix/mysql-virtual-*.cf
  postconf -e "myhostname = $HOSTNAME_FQDN" \
    "virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf" \
    "virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf" \
    "virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-alias-maps.cf" \
    "virtual_transport = lmtp:unix:private/dovecot-lmtp" \
    "smtpd_sasl_type = dovecot" "smtpd_sasl_path = private/auth" "smtpd_sasl_auth_enable = yes" \
    "smtpd_tls_cert_file = $ETC/ssl/panel.crt" "smtpd_tls_key_file = $ETC/ssl/panel.key" \
    "smtpd_tls_security_level = may" "smtp_tls_security_level = may" \
    "smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination" \
    "inet_interfaces = all" "mydestination = localhost"
  postconf -M "submission/inet=submission inet n - n - - smtpd" > /dev/null 2>&1 || true
  postconf -P "submission/inet/smtpd_tls_security_level=encrypt" "submission/inet/smtpd_sasl_auth_enable=yes" "submission/inet/smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
  postconf -M "smtps/inet=smtps inet n - n - - smtpd" > /dev/null 2>&1 || true
  postconf -P "smtps/inet/smtpd_tls_wrappermode=yes" "smtps/inet/smtpd_sasl_auth_enable=yes" "smtps/inet/smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
  cat > /etc/dovecot/dovecot-sql.conf.ext <<CF
driver = mysql
connect = host=127.0.0.1 dbname=mailserver user=mailuser password=$MAIL_DB_PASS
default_pass_scheme = SHA512-CRYPT
password_query = SELECT email as user, password FROM virtual_users WHERE email = '%u'
CF
  chmod 600 /etc/dovecot/dovecot-sql.conf.ext
  cat > /etc/dovecot/conf.d/auth-sql.conf.ext <<'CF'
passdb {
  driver = sql
  args = /etc/dovecot/dovecot-sql.conf.ext
}
userdb {
  driver = static
  args = uid=vmail gid=vmail home=/home/vmail/%d/%n mail=maildir:/home/vmail/%d/%n/Maildir
}
CF
  cat > /etc/dovecot/conf.d/99-dirhamhost.conf <<CF
protocols = imap lmtp
mail_location = maildir:/home/vmail/%d/%n/Maildir
mail_uid = vmail
mail_gid = vmail
first_valid_uid = 5000
disable_plaintext_auth = no
auth_mechanisms = plain login
ssl = yes
ssl_cert = <$ETC/ssl/panel.crt
ssl_key = <$ETC/ssl/panel.key
auth_master_user_separator = *
!include auth-sql.conf.ext
passdb {
  driver = passwd-file
  master = yes
  args = /etc/dovecot/master-users
}
service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }
}
service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0600
    user = postfix
    group = postfix
  }
}
CF
  sed -i "s/^\!include auth-system.conf.ext/#!include auth-system.conf.ext/" /etc/dovecot/conf.d/10-auth.conf 2>/dev/null || true
  # Panel webmail SSO: a shared secret (dh-api mints tokens) and a Dovecot master user
  # (Omnio opens any mailbox with it, no user password). See docs/OMNIO-SSO-CONTRACT.md.
  [ -f "$ETC/omnio-sso.secret" ] || { openssl rand -hex 32 > "$ETC/omnio-sso.secret"; chmod 640 "$ETC/omnio-sso.secret"; }
  # dh-api (unprivileged) mints the webmail token, so let it read the shared secret.
  id -u dh-api > /dev/null 2>&1 && setfacl -m u:dh-api:r "$ETC/omnio-sso.secret" 2>/dev/null || true
  if [ ! -f "$ETC/omnio-master.pass" ]; then
    MPASS="$(openssl rand -hex 16)"; printf "%s" "$MPASS" > "$ETC/omnio-master.pass"; chmod 640 "$ETC/omnio-master.pass"
    printf "dhpanel:%s\n" "$(doveadm pw -s SHA512-CRYPT -p "$MPASS")" > /etc/dovecot/master-users
    chmod 600 /etc/dovecot/master-users; chown dovecot:dovecot /etc/dovecot/master-users 2>/dev/null || true
  fi
  svc_enable_restart dovecot
  svc_enable_restart postfix
  mark mail
fi

# ===== 17d. Email deliverability: rspamd signs outbound mail (DKIM) and the panel
# publishes SPF/DKIM/DMARC per domain, so customer mail lands in inboxes. =====
if ! done_step deliverability; then
  if [ "$OS" = "alma" ]; then
    pkg_install redis
    # rspamd is not in EPEL; add the official repo. Its GPG key uses SHA1 which the EL9
    # crypto policy rejects, so install unsigned from the official HTTPS repo.
    cat > /etc/yum.repos.d/rspamd.repo <<'REPO'
[rspamd]
name=Rspamd stable repository
baseurl=https://rspamd.com/rpm-stable/centos-9/$basearch/
enabled=1
gpgcheck=0
REPO
    run dnf install -y rspamd
  else
    pkg_install redis-server
    run sh -c 'curl -fsSL https://rspamd.com/apt-stable/gpg.key | gpg --dearmor > /usr/share/keyrings/rspamd.gpg'
    echo "deb [signed-by=/usr/share/keyrings/rspamd.gpg] http://rspamd.com/apt-stable/ $(lsb_release -cs 2>/dev/null || echo noble) main" > /etc/apt/sources.list.d/rspamd.list
    run apt-get update -y
    run apt-get install -y rspamd
  fi
  mkdir -p /etc/rspamd/local.d /var/lib/rspamd/dkim
  cat > /etc/rspamd/local.d/dkim_signing.conf <<'CONF'
enabled = true;
selector = "default";
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
allow_username_mismatch = true;
use_domain = "header";
sign_local = true;
sign_authenticated = true;
CONF
  echo 'servers = "127.0.0.1";' > /etc/rspamd/local.d/redis.conf
  printf 'milter = yes;\ntimeout = 120s;\nupstream "local" { default = yes; self_scan = yes; }\n' > /etc/rspamd/local.d/worker-proxy.inc
  chown -R _rspamd:_rspamd /var/lib/rspamd 2>/dev/null || chown -R rspamd:rspamd /var/lib/rspamd 2>/dev/null || true
  # sign via the rspamd milter (both incoming and locally submitted mail)
  postconf -e "milter_default_action=accept" "milter_protocol=6" "smtpd_milters=inet:localhost:11332" "non_smtpd_milters=inet:localhost:11332"
  run systemctl enable redis rspamd 2>/dev/null || run systemctl enable redis-server rspamd
  run systemctl restart redis 2>/dev/null || run systemctl restart redis-server
  run systemctl restart rspamd
  run systemctl restart postfix
  mark deliverability
fi

# ===== 18. One-time admin credentials =====
if [ ! -f "$ETC/admin.initial" ]; then
  ADMIN_PASS="$(openssl rand -base64 18 | tr -d '/+=' | cut -c1-20)"
  printf "username=admin\npassword=%s\ncreated=%s\nnote=shown once by the installer; the panel (M4) imports and then deletes this file\n" "$ADMIN_PASS" "$(date -u)" > "$ETC/admin.initial"
  chmod 600 "$ETC/admin.initial"
  say "========================================================"
  say " DirhamHost installed on $OS."
  say " Panel URL:      https://$HOSTNAME_FQDN:$PANEL_PORT"
  say " Admin user:     admin"
  say " Admin password: $ADMIN_PASS   (shown once, also in $ETC/admin.initial until first login)"
  say " Agent socket:   /run/dh-agent.sock   CLI: dhctl ops"
  say " Install log:    $LOG"
  say "========================================================"
else
  say "install complete (re-run). Admin credentials were shown on first run; see $ETC/admin.initial"
fi
say "DirhamHost installer finished $(date -u)"
