diff --git a/README.md b/README.md index 74a3671..94d6f66 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 diff --git a/mail-stack/dashboard/api.php b/mail-stack/dashboard/api.php new file mode 100644 index 0000000..42cb285 --- /dev/null +++ b/mail-stack/dashboard/api.php @@ -0,0 +1,64 @@ + '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'; +} + +$escaped_args = array_map('escapeshellarg', $args); +$full_command = 'sudo ' . escapeshellarg($cli_path) . ' ' . $command . ' ' . implode(' ', $escaped_args) . ' 2>&1'; + +exec($full_command, $output, $return_var); + +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..c957bfc --- /dev/null +++ b/mail-stack/deploy.sh @@ -0,0 +1,139 @@ +#!/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 +chmod -R 775 "${MAIL_STACK_DIR}" + +# 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 + +# 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/tests/mail-stack/verify_mail_stack.sh b/tests/mail-stack/verify_mail_stack.sh new file mode 100755 index 0000000..3836ac7 --- /dev/null +++ b/tests/mail-stack/verify_mail_stack.sh @@ -0,0 +1,44 @@ +#!/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 + +export MAIL_STACK_DIR="$(pwd)/mail-stack-test" +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 "[✓] All verifications passed!"