Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
76 changes: 76 additions & 0 deletions mail-stack/dashboard/api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php
/**
* QuickBox Mail Stack Dashboard API Bridge
*/

header('Content-Type: application/json');

// Security Check: Ensure only authorized requests are processed.
// In the QuickBox ecosystem, this bridge is called by the main dashboard.
// We implement a token-based check for management parity and security.

$env_file = dirname(__DIR__) . '/.env';
$config = file_exists($env_file) ? parse_ini_file($env_file) : [];

$api_token = $config['API_TOKEN'] ?? null;
$provided_token = $_SERVER['HTTP_X_API_TOKEN'] ?? $_REQUEST['token'] ?? null;

if (empty($api_token) || $provided_token !== $api_token) {
http_response_code(401);
echo json_encode(['error' => '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
]);
153 changes: 153 additions & 0 deletions mail-stack/deploy.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF > "${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."
56 changes: 56 additions & 0 deletions mail-stack/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions mail-stack/mail-stack.service
Original file line number Diff line number Diff line change
@@ -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
Loading