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
33 changes: 33 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
64 changes: 64 additions & 0 deletions mail-stack/dashboard/api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?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 = [];
if (file_exists($env_file)) {
$lines = file($env_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(ltrim($line), '#') === 0) continue;
$parts = explode('=', $line, 2);
if (count($parts) === 2) {
$config[trim($parts[0])] = trim($parts[1]);
}
}
}

$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';
}

$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
]);
139 changes: 139 additions & 0 deletions mail-stack/deploy.sh
Original file line number Diff line number Diff line change
@@ -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 <<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

# 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
71 changes: 71 additions & 0 deletions mail-stack/manage-mail.sh
Original file line number Diff line number Diff line change
@@ -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 <email> <password> Add a new email account"
echo " del <email> Delete an email account"
echo " list List all email accounts"
echo " passwd <email> <password> Update password for an email account"
echo " quota <email> <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 <email> <password>" && exit 1
${DOCKER_COMPOSE} exec -T mailserver setup email add "$1" "$2"
;;
del)
[ $# -lt 1 ] && echo "Usage: $0 del <email>" && 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 <email> <password>" && exit 1
${DOCKER_COMPOSE} exec -T mailserver setup email update "$1" "$2"
;;
quota)
[ $# -lt 2 ] && echo "Usage: $0 quota <email> <quota>" && exit 1
${DOCKER_COMPOSE} exec -T mailserver setup email quota "$1" "$2"
;;
dkim)
${DOCKER_COMPOSE} exec -T mailserver setup config dkim
;;
*)
usage
;;
esac
Loading