#!/usr/bin/env bash # # inBOXER Install Script # ====================== # Downloads the latest inBOXER release from the git repository and # deploys it to /opt/inboxer as a systemd service. # Supports Debian (apt) and RHEL (yum/dnf) families. # # Usage: # curl -sSL https://git.lohmar.co.uk/cclohmar/inboxer/raw/branch/main/install.sh | sudo bash # set -euo pipefail # --- Configuration ----------------------------------------------------------- INSTALL_DIR="/opt/inboxer" BIN_DIR="${INSTALL_DIR}/bin" DATA_DIR="${INSTALL_DIR}/data" LOGS_DIR="${INSTALL_DIR}/logs" SERVICE_USER="inboxer" SERVICE_GROUP="inboxer" SERVICE_NAME="inboxer" SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" # Repository raw content base URL REPO_BASE="https://git.lohmar.co.uk/cclohmar/inboxer/raw/branch/main" # --- Terminal colours -------------------------------------------------------- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' info() { echo -e "${GREEN}[INFO]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } # --- Pre-flight checks ------------------------------------------------------- if [[ $EUID -ne 0 ]]; then error "This script must be run as root (use sudo)." exit 1 fi if ! command -v systemctl &>/dev/null; then error "systemd is required but not found on this system." exit 1 fi # Prefer curl; fall back to wget DOWNLOADER="" if command -v curl &>/dev/null; then DOWNLOADER="curl -sSL" elif command -v wget &>/dev/null; then DOWNLOADER="wget -q -O" else error "Neither curl nor wget found. Install one of them and re-run." exit 1 fi # --- OS Detection (informational) ------------------------------------------- if [[ -f /etc/os-release ]]; then # shellcheck source=/dev/null . /etc/os-release info "Detected OS: ${NAME} ${VERSION_ID}" fi # --- Create system user & group ---------------------------------------------- info "Creating system user '${SERVICE_USER}'..." if getent group "${SERVICE_GROUP}" &>/dev/null; then info "Group '${SERVICE_GROUP}' already exists." else groupadd --system "${SERVICE_GROUP}" fi if getent passwd "${SERVICE_USER}" &>/dev/null; then info "User '${SERVICE_USER}' already exists." else useradd --system \ --no-create-home \ --gid "${SERVICE_GROUP}" \ --shell /sbin/nologin \ --comment "inBOXER Service User" \ "${SERVICE_USER}" fi # --- Create directory structure ---------------------------------------------- WEB_DIR="${INSTALL_DIR}/web" TEMPLATES_DIR="${WEB_DIR}/templates" STATIC_DIR="${WEB_DIR}/static" info "Creating directories under ${INSTALL_DIR}..." mkdir -p "${BIN_DIR}" "${DATA_DIR}" "${LOGS_DIR}" "${TEMPLATES_DIR}" "${STATIC_DIR}" # --- Download helper --------------------------------------------------------- download() { local src_url="$1" local dest_path="$2" local mode="$3" local tmpfile tmpfile="$(mktemp)" local http_code if [[ "${DOWNLOADER}" == curl* ]]; then http_code=$(curl -sSL -o "${tmpfile}" -w "%{http_code}" "${src_url}") else wget -q -O "${tmpfile}" "${src_url}" || { rm -f "${tmpfile}" error "Failed to download from ${src_url}" return 1 } # wget doesn't expose the HTTP code; assume success if no error http_code="200" fi # Check HTTP status code (curl: exact; wget: best-effort via content) if [[ "${DOWNLOADER}" == curl* ]] && [[ "${http_code}" != "200" ]]; then rm -f "${tmpfile}" error "Server returned HTTP ${http_code} for ${src_url}" return 1 fi # Check that the downloaded content is non-empty if [[ ! -s "${tmpfile}" ]]; then rm -f "${tmpfile}" error "Downloaded empty file from ${src_url}" return 1 fi install -m "${mode}" "${tmpfile}" "${dest_path}" rm -f "${tmpfile}" info " ${src_url##*/} -> ${dest_path}" } # --- Download release files -------------------------------------------------- info "Downloading inBOXER release from ${REPO_BASE}/bin/ ..." download "${REPO_BASE}/bin/inboxer" "${BIN_DIR}/inboxer" 755 download "${REPO_BASE}/bin/config.yaml" "${BIN_DIR}/config.yaml" 644 download "${REPO_BASE}/bin/prompt.txt" "${BIN_DIR}/prompt.txt" 644 # Web templates and static assets info "Downloading web templates and static assets..." download "${REPO_BASE}/web/templates/base.html" "${TEMPLATES_DIR}/base.html" 644 download "${REPO_BASE}/web/templates/login.html" "${TEMPLATES_DIR}/login.html" 644 download "${REPO_BASE}/web/templates/verify.html" "${TEMPLATES_DIR}/verify.html" 644 download "${REPO_BASE}/web/templates/dashboard.html" "${TEMPLATES_DIR}/dashboard.html" 644 download "${REPO_BASE}/web/templates/settings.html" "${TEMPLATES_DIR}/settings.html" 644 download "${REPO_BASE}/web/static/style.css" "${STATIC_DIR}/style.css" 644 # Adjust config paths for /opt/inboxer deployment info "Adjusting configuration paths for deployment..." sed -i 's|path: "bin/db.sqlite"|path: "'"${DATA_DIR}"'/db.sqlite"|' "${BIN_DIR}/config.yaml" sed -i 's|file: "bin/inboxer.log"|file: "'"${LOGS_DIR}"'/inboxer.log"|' "${BIN_DIR}/config.yaml" # --- Create systemd service unit --------------------------------------------- info "Creating systemd service unit at ${SERVICE_FILE} ..." cat > "${SERVICE_FILE}" << UNITEOF [Unit] Description=inBOXER - AI-Powered Email Classifier Documentation=https://git.lohmar.co.uk/cclohmar/inboxer After=network-online.target Wants=network-online.target [Service] Type=simple User=${SERVICE_USER} Group=${SERVICE_GROUP} WorkingDirectory=${INSTALL_DIR} ExecStart=${BIN_DIR}/inboxer Restart=on-failure RestartSec=10 # Logging StandardOutput=append:${LOGS_DIR}/stdout.log StandardError=append:${LOGS_DIR}/stderr.log # Security hardening NoNewPrivileges=true ProtectSystem=full ProtectHome=true PrivateTmp=true # Data and logs directories need write access (overrides ProtectSystem=full) ReadWritePaths=${DATA_DIR} ${LOGS_DIR} [Install] WantedBy=multi-user.target UNITEOF # --- Set file permissions ---------------------------------------------------- info "Setting file ownership and permissions..." chown -R "${SERVICE_USER}:${SERVICE_GROUP}" "${INSTALL_DIR}" chmod 755 "${BIN_DIR}" chmod 750 "${DATA_DIR}" chmod 750 "${LOGS_DIR}" chmod 640 "${BIN_DIR}/config.yaml" # --- Register & start service ----------------------------------------------- info "Reloading systemd daemon..." systemctl daemon-reload info "Enabling ${SERVICE_NAME} service (starts on boot)..." systemctl enable "${SERVICE_NAME}" info "Starting ${SERVICE_NAME} service (with 30s timeout)..." timeout 30 systemctl start "${SERVICE_NAME}" || warn "'systemctl start ${SERVICE_NAME}' timed out or failed — the service may need manual investigation" # Brief pause so the service can initialise sleep 2 # --- Verify ------------------------------------------------------------------ if systemctl is-active --quiet "${SERVICE_NAME}"; then info "Service '${SERVICE_NAME}' is running." systemctl status "${SERVICE_NAME}" --no-pager else warn "Service '${SERVICE_NAME}' did not start. Check logs:" warn " journalctl -u ${SERVICE_NAME} --no-pager" systemctl status "${SERVICE_NAME}" --no-pager || true fi # --- Summary ----------------------------------------------------------------- echo "" info "=================================================" info " inBOXER Installation Complete" info "=================================================" echo "" info " Install directory: ${INSTALL_DIR}" info " Binary: ${BIN_DIR}/inboxer" info " Configuration: ${BIN_DIR}/config.yaml" info " Prompt file: ${BIN_DIR}/prompt.txt" info " Data (SQLite): ${DATA_DIR}/" info " Logs: ${LOGS_DIR}/" echo "" info " * Edit config.yaml with your credentials before first start:" info " sudo nano ${BIN_DIR}/config.yaml" echo "" info " Required settings:" info " - ai.api_key (DeepSeek API key)" info " - smtp.host / port (SMTP server for OTP emails)" info " - smtp.username / pass (SMTP login credentials)" info " - server.session_secret (change from the default)" echo "" if grep -q "your_deepseek_api_key_here\|your.smtp.host\|change-me-in-production" \ "${BIN_DIR}/config.yaml" 2>/dev/null; then warn " ! config.yaml still contains placeholder values!" warn " Edit ${BIN_DIR}/config.yaml before the service will function." echo "" fi info " Service management:" info " sudo systemctl status ${SERVICE_NAME}" info " sudo systemctl restart ${SERVICE_NAME}" info " sudo systemctl stop ${SERVICE_NAME}" info " sudo journalctl -u ${SERVICE_NAME} -f" echo "" info " Web interface: http://$(hostname -s 2>/dev/null || echo "localhost"):8080" info "=================================================" echo ""