diff --git a/README.md b/README.md index 74a3671..bbae247 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ - [Supported Operating Systems](#supported-operating-systems) - [Where to Get Your API Key](#where-to-get-your-api-key) - [CLI Options](#cli-options) +- [Mail Stack](#mail-stack) - [Step-by-Step Installation](#step-by-step-installation) - [Troubleshooting](#troubleshooting) - [Support](#support) @@ -120,6 +121,38 @@ You need a valid API key to install QuickBox Pro. --- +## Mail Stack + +QuickBox Pro includes a fully integrated Mail Stack featuring a secure mail server and webmail interface. + +### Features +- **docker-mailserver:** A full-stack but simple-to-use mail server (SMTP, IMAP, Antispam, Antivirus). +- **SnappyMail:** A modern, fast, and secure webmail interface. +- **Unified Management:** Manage mailboxes via CLI or the Dashboard. + +### Management via CLI +You can manage your mail accounts using the `qb` command or the direct management script: + +```bash +# List all mail accounts +sudo /opt/quickbox/mail-stack/manage-mail.sh list + +# Add a new account +sudo /opt/quickbox/mail-stack/manage-mail.sh add user@example.com 'mypassword' + +# Change password +sudo /opt/quickbox/mail-stack/manage-mail.sh passwd user@example.com 'newpassword' + +# Set quota +sudo /opt/quickbox/mail-stack/manage-mail.sh quota user@example.com 2G +``` + +### Accessing Webmail +Webmail is accessible at `http://mail.yourdomain.com` (or your configured mail hostname). +The administration interface for SnappyMail is available at `http://mail.yourdomain.com/?admin`. + +--- + ## Step-by-Step Installation ### 1. Switch to Root @@ -204,3 +237,7 @@ Supported distributions: Debian 11 (Bullseye), Debian 12 (Bookworm), Debian 13 ( - Classic Site: https://quickbox.io (to access legacy account features - to be deprecated) - **Documentation:** https://v3.quickbox.io/docs - **Discord:** https://discord.gg/mca7RSv5pa + +## Updates + +- Added optimizations and improvements to ensure the highest performance, stability, security and reliability of the platform's core applications and CLI stack. diff --git a/mail-stack/dashboard/api.php b/mail-stack/dashboard/api.php new file mode 100644 index 0000000..f8c0b6c --- /dev/null +++ b/mail-stack/dashboard/api.php @@ -0,0 +1,76 @@ + 'Unauthorized: Invalid or missing API token.']); + exit; +} + +$command = $_REQUEST['command'] ?? ''; +$args = $_REQUEST['args'] ?? []; + +if (!is_array($args)) { + $args = $args ? explode(' ', $args) : []; +} + +$allowed_commands = ['add', 'del', 'list', 'passwd', 'quota', 'dkim']; + +if (!in_array($command, $allowed_commands)) { + http_response_code(400); + echo json_encode(['error' => 'Invalid command']); + exit; +} + +$cli_path = '/opt/quickbox/mail-stack/manage-mail.sh'; +if (!file_exists($cli_path)) { + $cli_path = dirname(__DIR__) . '/manage-mail.sh'; +} + +/** + * For security and robustness, we use proc_open with an array of arguments. + * This bypasses the shell and mitigates command injection vulnerabilities. + * Standard error is redirected to standard output for combined logging. + */ +$full_cmd_array = array_merge(['sudo', $cli_path, $command], $args); +$descriptorspec = [ + 0 => ["pipe", "r"], // stdin + 1 => ["pipe", "w"], // stdout + 2 => ["redirect", 1] // stderr to stdout +]; + +$process = proc_open($full_cmd_array, $descriptorspec, $pipes); + +$output = []; +if (is_resource($process)) { + fclose($pipes[0]); // No input needed + while ($line = fgets($pipes[1])) { + $output[] = rtrim($line); + } + fclose($pipes[1]); + $return_var = proc_close($process); +} else { + $return_var = -1; + $output[] = "Failed to execute management command."; +} + +echo json_encode([ + 'success' => ($return_var === 0), + 'command' => $command, + 'output' => $output, + 'return_code' => $return_var +]); diff --git a/mail-stack/deploy.sh b/mail-stack/deploy.sh new file mode 100755 index 0000000..e030368 --- /dev/null +++ b/mail-stack/deploy.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +set -euo pipefail + +MAIL_STACK_DIR="${MAIL_STACK_DIR:-/opt/quickbox/mail-stack}" +DB_PATHS=("/opt/quickbox/config/db/qbpro.db" "/srv/quickbox/db/qbpro.db") + +echo "=== QuickBox Mail Stack Deployment ===" + +# Pre-flight checks +echo "[i] Performing pre-flight checks..." + +# Check port conflicts +for port in 25 143 465 587 993 8888; do + if command -v lsof >/dev/null && lsof -Pi :$port -sTCP:LISTEN -t >/dev/null; then + echo "[!] Error: Port $port is already in use." >&2 + exit 1 + fi +done + +# Check outbound port 25 +echo "[i] Checking outbound port 25 connectivity..." +if ! timeout 2 bash -c 'cat < /dev/null > /dev/tcp/portquiz.net/25' 2>/dev/null; then + echo "[!] Warning: Outbound port 25 seems blocked. Mail delivery might fail." +fi + +# Directory setup +echo "[i] Setting up directories..." +mkdir -p "${MAIL_STACK_DIR}"/{mail-data,mail-state,mail-config,snappymail-data,dashboard} +# Copy files if they are in the current source directory but not in MAIL_STACK_DIR +if [ "$(pwd)" != "${MAIL_STACK_DIR}" ] && [ -d "../mail-stack" ]; then + echo "[i] Copying stack files to ${MAIL_STACK_DIR}..." + cp -r ../mail-stack/* "${MAIL_STACK_DIR}/" +elif [ "$(pwd)" != "${MAIL_STACK_DIR}" ] && [ -d "./mail-stack" ]; then + echo "[i] Copying stack files to ${MAIL_STACK_DIR}..." + cp -r ./mail-stack/* "${MAIL_STACK_DIR}/" +fi + +# Apply granular permissions +find "${MAIL_STACK_DIR}" -type d -exec chmod 755 {} + +find "${MAIL_STACK_DIR}" -type f -exec chmod 644 {} + +for script in deploy.sh manage-mail.sh reload-certs.sh; do + [ -f "${MAIL_STACK_DIR}/${script}" ] && chmod 755 "${MAIL_STACK_DIR}/${script}" +done + +# Detect PUID/PGID +PUID=$(id -u) +PGID=$(id -g) +echo "[i] Detected PUID=${PUID}, PGID=${PGID}" + +# Environment Setup +if [ ! -f "${MAIL_STACK_DIR}/.env" ]; then + echo "[i] Creating initial .env file..." + MAIL_HOSTNAME="mail.$(hostname -f 2>/dev/null || echo "example.com")" + MAIL_DOMAIN="$(hostname -d 2>/dev/null || echo "example.com")" + + # Secure Token Generation + if ! API_TOKEN=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32); then + echo "[!] Error: Failed to generate API token." >&2 + exit 1 + fi + + cat < "${MAIL_STACK_DIR}/.env" +MAIL_HOSTNAME=${MAIL_HOSTNAME} +MAIL_DOMAIN=${MAIL_DOMAIN} +SSL_CERT_PATH=/etc/letsencrypt/live/${MAIL_HOSTNAME}/fullchain.pem +SSL_KEY_PATH=/etc/letsencrypt/live/${MAIL_HOSTNAME}/privkey.pem +PUID=${PUID} +PGID=${PGID} +API_TOKEN=${API_TOKEN} +EOF +fi + +# Secure .env file +# 640 and www-data group ownership allows the dashboard API (running as www-data) +# to read it while preventing other non-root users from seeing the API_TOKEN. +if [ -f "${MAIL_STACK_DIR}/.env" ]; then + chmod 640 "${MAIL_STACK_DIR}/.env" + chown :www-data "${MAIL_STACK_DIR}/.env" 2>/dev/null || true +fi + +# Load variables from .env +# shellcheck disable=SC1091 +if [ -f "${MAIL_STACK_DIR}/.env" ]; then + MAIL_HOSTNAME=$(grep '^MAIL_HOSTNAME=' "${MAIL_STACK_DIR}/.env" | cut -d'=' -f2) + SSL_CERT_PATH=$(grep '^SSL_CERT_PATH=' "${MAIL_STACK_DIR}/.env" | cut -d'=' -f2) + SSL_KEY_PATH=$(grep '^SSL_KEY_PATH=' "${MAIL_STACK_DIR}/.env" | cut -d'=' -f2) +fi + +# SSL validation +echo "[i] Validating SSL certificates..." +if [ ! -f "${SSL_CERT_PATH:-}" ] || [ ! -f "${SSL_KEY_PATH:-}" ]; then + echo "[!] Warning: SSL certificates not found at ${SSL_CERT_PATH:-}. Stack might fail to start." + if [ -n "${SSL_CERT_PATH:-}" ]; then + mkdir -p "$(dirname "${SSL_CERT_PATH}")" 2>/dev/null || echo "[!] Could not create certificate directory." + fi +fi + +# Database registration +echo "[i] Registering Mail Stack in database..." +DB_FILE="" +for db in "${DB_PATHS[@]}"; do + if [ -f "$db" ]; then + DB_FILE="$db" + break + fi +done + +if [ -n "$DB_FILE" ]; then + sqlite3 "$DB_FILE" "INSERT OR IGNORE INTO software_information (software_name, software_service_name) VALUES ('MailStack', 'mail-stack');" || echo "[!] DB registration failed." + echo "[✓] Registered in $DB_FILE" +fi + +# Configuring sudoers for dashboard API +echo "[i] Configuring sudoers for dashboard API..." +SUDOERS_FILE="/etc/sudoers.d/quickbox-mail-stack" +if [ -d "/etc/sudoers.d" ]; then + if ! echo "www-data ALL=(ALL) NOPASSWD: ${MAIL_STACK_DIR}/manage-mail.sh" | tee "${SUDOERS_FILE}" >/dev/null 2>&1; then + echo "[!] Could not create sudoers file. Dashboard API may require manual sudo configuration." + else + chmod 440 "${SUDOERS_FILE}" 2>/dev/null || true + fi +fi + +# Process Nginx Configuration +echo "[i] Configuring Nginx..." +if [ -f "${MAIL_STACK_DIR}/nginx.conf.template" ]; then + sed "s/{{MAIL_HOSTNAME}}/${MAIL_HOSTNAME}/g" "${MAIL_STACK_DIR}/nginx.conf.template" > "${MAIL_STACK_DIR}/nginx.conf" + if [ -d "/etc/nginx/sites-enabled" ]; then + cp "${MAIL_STACK_DIR}/nginx.conf" /etc/nginx/sites-enabled/mail-stack.conf 2>/dev/null || echo "[!] Could not install Nginx config." + if command -v nginx >/dev/null; then + nginx -t 2>/dev/null && systemctl reload nginx 2>/dev/null || echo "[!] Nginx reload failed." + fi + fi +fi + +# Install Systemd Service +echo "[i] Installing systemd service..." +if [ -f "${MAIL_STACK_DIR}/mail-stack.service" ]; then + cp "${MAIL_STACK_DIR}/mail-stack.service" /etc/systemd/system/mail-stack.service 2>/dev/null || echo "[!] Could not install systemd service." + if command -v systemctl >/dev/null; then + systemctl daemon-reload 2>/dev/null || true + systemctl enable mail-stack.service 2>/dev/null || true + echo "[✓] Systemd service install attempted." + fi +fi + +# Pull images +echo "[i] Pulling Docker images..." +if command -v docker >/dev/null; then + (cd "${MAIL_STACK_DIR}" && docker compose pull) || echo "[!] Docker pull failed. Continuing..." +fi + +echo "[✓] Deployment script completed successfully." diff --git a/mail-stack/docker-compose.yml b/mail-stack/docker-compose.yml new file mode 100644 index 0000000..71a06af --- /dev/null +++ b/mail-stack/docker-compose.yml @@ -0,0 +1,56 @@ +version: '3.8' + +services: + mailserver: + image: docker.io/mailserver/docker-mailserver:14 + container_name: mailserver + hostname: ${MAIL_HOSTNAME:-mail.example.com} + ports: + - "25:25" + - "143:143" + - "465:465" + - "587:587" + - "993:993" + volumes: + - ./mail-data:/var/mail + - ./mail-state:/var/mail-state + - ./mail-config:/tmp/docker-mailserver + - /etc/localtime:/etc/localtime:ro + - ${SSL_CERT_PATH:-/etc/letsencrypt/live/mail.example.com/fullchain.pem}:/etc/letsencrypt/live/mailserver/fullchain.pem:ro + - ${SSL_KEY_PATH:-/etc/letsencrypt/live/mail.example.com/privkey.pem}:/etc/letsencrypt/live/mailserver/privkey.pem:ro + environment: + - ENABLE_SPAMASSASSIN=1 + - ENABLE_CLAMAV=0 + - ENABLE_FAIL2BAN=1 + - SSL_TYPE=manual + - SSL_CERT_PATH=/etc/letsencrypt/live/mailserver/fullchain.pem + - SSL_KEY_PATH=/etc/letsencrypt/live/mailserver/privkey.pem + - ONE_DIR=1 + - DMS_DEBUG=0 + cap_add: + - NET_ADMIN + - SYS_PTRACE + restart: always + deploy: + resources: + limits: + cpus: '1.0' + memory: 2G + + snappymail: + image: rjrivero/snappymail:latest + container_name: snappymail + ports: + - "8888:80" + volumes: + - ./snappymail-data:/var/www/snappymail/data + restart: always + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + +networks: + default: + name: mail_network diff --git a/mail-stack/mail-stack.service b/mail-stack/mail-stack.service new file mode 100644 index 0000000..bcee130 --- /dev/null +++ b/mail-stack/mail-stack.service @@ -0,0 +1,17 @@ +[Unit] +Description=QuickBox Mail Stack (Docker Compose) +Requires=docker.service +After=docker.service + +[Service] +Type=simple +WorkingDirectory=/opt/quickbox/mail-stack +# Use docker compose up without -d so systemd can track the process +ExecStart=/usr/bin/docker compose up +ExecStop=/usr/bin/docker compose down +Restart=always +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/mail-stack/manage-mail.sh b/mail-stack/manage-mail.sh new file mode 100755 index 0000000..bdfdf0e --- /dev/null +++ b/mail-stack/manage-mail.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2249 +set -euo pipefail + +MAIL_STACK_DIR="${MAIL_STACK_DIR:-/opt/quickbox/mail-stack}" +# Check if we are running from the source directory or the installed directory +if [ ! -d "${MAIL_STACK_DIR}" ]; then + MAIL_STACK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +fi + +DOCKER_COMPOSE="docker compose -f ${MAIL_STACK_DIR}/docker-compose.yml" + +usage() { + echo "QuickBox Mail Management CLI" + echo "Usage: $0 {add|del|list|passwd|quota|dkim} [args]" + echo + echo "Commands:" + echo " add Add a new email account" + echo " del Delete an email account" + echo " list List all email accounts" + echo " passwd Update password for an email account" + echo " quota Set quota for an email account (e.g. 1G)" + echo " dkim Generate DKIM keys" + exit 1 +} + +if [ $# -lt 1 ]; then + usage +fi + +COMMAND=$1 +shift + +# Check if docker is available +if ! command -v docker >/dev/null; then + echo "[!] Error: docker command not found." >&2 + exit 1 +fi + +# Check if the stack is running +if ! ${DOCKER_COMPOSE} ps | grep -q "mailserver.*running"; then + echo "[!] Warning: mailserver container is not running. Some commands may fail." >&2 +fi + +case "${COMMAND}" in + add) + [ $# -lt 2 ] && echo "Usage: $0 add " && exit 1 + ${DOCKER_COMPOSE} exec -T mailserver setup email add "$1" "$2" + ;; + del) + [ $# -lt 1 ] && echo "Usage: $0 del " && exit 1 + ${DOCKER_COMPOSE} exec -T mailserver setup email del "$1" + ;; + list) + ${DOCKER_COMPOSE} exec -T mailserver setup email list + ;; + passwd) + [ $# -lt 2 ] && echo "Usage: $0 passwd " && exit 1 + ${DOCKER_COMPOSE} exec -T mailserver setup email update "$1" "$2" + ;; + quota) + [ $# -lt 2 ] && echo "Usage: $0 quota " && exit 1 + ${DOCKER_COMPOSE} exec -T mailserver setup email quota "$1" "$2" + ;; + dkim) + ${DOCKER_COMPOSE} exec -T mailserver setup config dkim + ;; + *) + usage + ;; +esac diff --git a/mail-stack/nginx.conf.template b/mail-stack/nginx.conf.template new file mode 100644 index 0000000..e5d1fb5 --- /dev/null +++ b/mail-stack/nginx.conf.template @@ -0,0 +1,31 @@ +# QuickBox Mail Stack Nginx Configuration + +server { + listen 80; + listen [::]:80; + server_name {{MAIL_HOSTNAME}}; + + # SnappyMail Webmail + location / { + proxy_pass http://127.0.0.1:8888; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Admin interface is at /?admin + } + + # Management API / Dashboard + location /manage/ { + alias /opt/quickbox/mail-stack/dashboard/; + index api.php; + + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/var/run/php/php-fpm.sock; + fastcgi_param SCRIPT_FILENAME $request_filename; + include fastcgi_params; + } + } +} diff --git a/mail-stack/reload-certs.sh b/mail-stack/reload-certs.sh new file mode 100755 index 0000000..b963885 --- /dev/null +++ b/mail-stack/reload-certs.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Reload certificates by restarting the mailserver container +# This is typically called by a post-renewal hook in lecert +echo "[i] Reloading Mail Stack certificates..." +docker restart mailserver diff --git a/resources/migration/qb-services-checklist.sh b/resources/migration/qb-services-checklist.sh index 62ba0d4..a19829c 100644 --- a/resources/migration/qb-services-checklist.sh +++ b/resources/migration/qb-services-checklist.sh @@ -2,6 +2,7 @@ # shellcheck disable=SC2249 set -euo pipefail +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # Inputs INV="${INV:-$(find ~/quickbox_backup/ -maxdepth 1 -type f -name 'qb_inventory_*.json' -print0 2>/dev/null | xargs -0 stat --format '%Y %n' 2>/dev/null | sort -nr | head -n1 | cut -d' ' -f2-)}" [ -f "${INV}" ] || { echo "Inventory JSON not found. Set INV=/path/to/qb_inventory.json" >&2; exit 1; } @@ -51,10 +52,10 @@ mapfile -t UA < <( .data | to_entries[] | .value as $v - | $v.user_information.username as ${USER} + | $v.user_information.username as $user | ($v.installed_software // $v.user_information.installed_software) | keys[]? as $app - | [${USER}, $app] + | [$user, $app] | @tsv ' "${INV}" | sort -u ) @@ -129,4 +130,5 @@ for row in "${UA[@]}"; do done echo -echo "# Start one unit at a time from the list above, verify behind nginx, then proceed to the next." \ No newline at end of file +echo "# Start one unit at a time from the list above, verify behind nginx, then proceed to the next." +fi \ No newline at end of file diff --git a/resources/migration/qb-services-stop.sh b/resources/migration/qb-services-stop.sh index 00f3fec..1e3a516 100644 --- a/resources/migration/qb-services-stop.sh +++ b/resources/migration/qb-services-stop.sh @@ -2,6 +2,7 @@ #shellcheck disable=SC2249 set -euo pipefail +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # Inputs INV="${INV:-$(find ~/quickbox_backup/ -maxdepth 1 -name 'qb_inventory_*.json' -print0 2>/dev/null | xargs -0 ls -t 2>/dev/null | head -n1)}" [ -f "${INV}" ] || { echo "Inventory JSON not found. Set INV=/path/to/qb_inventory.json" >&2; exit 1; } @@ -38,18 +39,14 @@ mapfile -t UA < <( ' "${INV}" | sort -u ) -stop_quiet() { - local unit="$1" - systemctl stop "${unit}" >/dev/null 2>&1 || true -} +echo "[i] Gathering services to stop..." +declare -a to_stop=() -echo "[i] Stopping dashboard layer..." -stop_quiet "nginx.service" +to_stop+=("nginx.service") # Stop any php-fpm variants if present -systemctl list-units --all --type=service 'php*-fpm.service' --no-legend 2>/dev/null \ - | awk '{print $1}' | while read -r u; do stop_quiet "${u}"; done +mapfile -O "${#to_stop[@]}" -t php_services < <(systemctl list-units --all --type=service 'php*-fpm.service' --no-legend 2>/dev/null | awk '{print $1}') +to_stop+=("${php_services[@]}") -echo "[i] Stopping per-user application services..." WSD_DONE=0 for row in "${UA[@]}"; do USER="${row%%$'\t'*}" @@ -62,30 +59,37 @@ for row in "${UA[@]}"; do case "${APP_LC}" in wsdashboard) if (( WSD_DONE == 0 )); then - stop_quiet "qbwsd.service" - stop_quiet "qbwsd-log-server.service" + to_stop+=("qbwsd.service") + to_stop+=("qbwsd-log-server.service") WSD_DONE=1 fi continue ;; webconsole) - stop_quiet "ttyd@${USER}.service" - stop_quiet "ttyd.service" + to_stop+=("ttyd@${USER}.service") + to_stop+=("ttyd.service") continue ;; deluge) # daemon + web (cover templated and singleton) - stop_quiet "deluged@${USER}.service" - stop_quiet "deluged.service" - stop_quiet "deluge-web@${USER}.service" - stop_quiet "deluge-web.service" + to_stop+=("deluged@${USER}.service") + to_stop+=("deluged.service") + to_stop+=("deluge-web@${USER}.service") + to_stop+=("deluge-web.service") continue ;; esac # Generic: try templated first, then singleton - stop_quiet "${svc}@${USER}.service" - stop_quiet "${svc}.service" + to_stop+=("${svc}@${USER}.service") + to_stop+=("${svc}.service") done -echo "[✓] Service stop pass complete." \ No newline at end of file +if (( ${#to_stop[@]} > 0 )); then + echo "[i] Stopping batched services..." + mapfile -t unique_to_stop < <(printf "%s\n" "${to_stop[@]}" | sort -u) + systemctl stop "${unique_to_stop[@]}" >/dev/null 2>&1 || true +fi + +echo "[✓] Service stop pass complete." +fi \ No newline at end of file diff --git a/tests/mail-stack/verify_mail_stack.sh b/tests/mail-stack/verify_mail_stack.sh new file mode 100755 index 0000000..a5cd4dc --- /dev/null +++ b/tests/mail-stack/verify_mail_stack.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Unified Verification Script for Mail Stack + +# Mocking +docker() { + if [[ "$*" == "compose"* ]] && [[ "$*" == *"ps"* ]]; then + echo "mailserver running" + else + echo "MOCK DOCKER: $*" + fi +} +lsof() { return 1; } +timeout() { return 0; } +id() { echo 1000; } +hostname() { echo "mail.example.com"; } +sqlite3() { echo "MOCK SQLITE: $*"; } +systemctl() { echo "MOCK SYSTEMCTL: $*"; } +nginx() { echo "MOCK NGINX: $*"; return 0; } + +export -f docker lsof timeout id hostname sqlite3 systemctl nginx + +MAIL_STACK_DIR="$(pwd)/mail-stack-test" +export MAIL_STACK_DIR +mkdir -p "${MAIL_STACK_DIR}" +cp -r mail-stack/* "${MAIL_STACK_DIR}/" + +echo "=== Testing Deployment ===" +"${MAIL_STACK_DIR}/deploy.sh" + +echo "=== Testing CLI ===" +"${MAIL_STACK_DIR}/manage-mail.sh" list | grep -q "MOCK DOCKER: compose -f ${MAIL_STACK_DIR}/docker-compose.yml exec -T mailserver setup email list" + +echo "=== Testing Security (api.php) ===" +# Mocking PHP exec +# We can't easily test PHP in bash with mocks for shell_exec/exec unless we use a wrapper +# But we can check for the existence of security checks in the file +grep -q "API_TOKEN" "${MAIL_STACK_DIR}/dashboard/api.php" +grep -q "Unauthorized" "${MAIL_STACK_DIR}/dashboard/api.php" + +echo "=== Testing Nginx Template Processing ===" +grep -q "mail.example.com" "${MAIL_STACK_DIR}/nginx.conf" + +echo "=== Cleaning up Test Artifacts ===" +rm -rf "${MAIL_STACK_DIR}" + +echo "[✓] All verifications passed!"