#!/usr/bin/env bash # # inBOXER Install Script # ====================== # Deploys the inBOXER binary and configuration to /opt/inboxer # Creates a systemd service for production deployment. # Supports Debian (apt) and RHEL (yum/dnf) families. # # Usage: sudo ./install.sh # set -euo pipefail # ─── Configuration ─────────────────────────────────────────────────────────── INSTALL_DIR="/opt/inboxer" SERVICE_USER="inboxer" SERVICE_GROUP="inboxer" SERVICE_NAME="inboxer" SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" BIN_DIR="${INSTALL_DIR}/bin" DATA_DIR="${INSTALL_DIR}/data" LOGS_DIR="${INSTALL_DIR}/logs" # Detect script/repo location SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ "$(basename "${SCRIPT_DIR}")" == "bin" ]]; then REPO_DIR="$(dirname "${SCRIPT_DIR}")" else REPO_DIR="${SCRIPT_DIR}" fi # ─── 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 REQUIRED_FILES=( "${REPO_DIR}/bin/inboxer" "${REPO_DIR}/bin/config.yaml" "${REPO_DIR}/bin/prompt.txt" ) for f in "${REQUIRED_FILES[@]}"; do if [[ ! -f "$f" ]]; then error "Required file not found: $f" error "Run this script from the inBOXER repository root." exit 1 fi done # ─── OS Detection (informational) ─────────────────────────────────────────── if [[ -f /etc/os-release ]]; then # shellcheck source=/dev/null . /etc/os-release info "Detected OS: ${NAME} ${VERSION_ID}" else info "Could not detect OS version (no /etc/os-release)." 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}" info "Group '${SERVICE_GROUP}' created." 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}" info "User '${SERVICE_USER}' created." fi # ─── Create directory structure ────────────────────────────────────────────── info "Creating directories under ${INSTALL_DIR}..." mkdir -p "${BIN_DIR}" "${DATA_DIR}" "${LOGS_DIR}" # ─── Install binary & config files ─────────────────────────────────────────── info "Installing binary..." install -m 755 "${REPO_DIR}/bin/inboxer" "${BIN_DIR}/inboxer" info "Installing configuration..." install -m 644 "${REPO_DIR}/bin/config.yaml" "${BIN_DIR}/config.yaml" install -m 644 "${REPO_DIR}/bin/prompt.txt" "${BIN_DIR}/prompt.txt" # 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" # prompt_file: "bin/prompt.txt" works as-is relative to the working directory, # since WorkingDirectory=/opt/inboxer resolves it to /opt/inboxer/bin/prompt.txt # ─── Create .env from template ────────────────────────────────────────────── ENV_FILE="${INSTALL_DIR}/.env" if [[ ! -f "${ENV_FILE}" ]]; then # Prefer .env.example from repo (single source of truth) if [[ -f "${REPO_DIR}/.env.example" ]]; then info "Copying .env.example from repository to ${ENV_FILE} ..." cp "${REPO_DIR}/.env.example" "${ENV_FILE}" else info "Creating .env template at ${ENV_FILE} ..." cat > "${ENV_FILE}" << 'ENVEOF' # inBOXER Environment Configuration # ==================================== # Set your credentials below. The service reads these variables on startup. # You can also set them directly in the systemd EnvironmentFile or via # the shell environment (godotenv.Load() checks both the .env file and os.Getenv). # DeepSeek API key – used to classify incoming emails DEEPSEEK_API_KEY=your_deepseek_api_key_here # SMTP credentials – used to send one-time passwords (OTP) via email SMTP_HOST=your.smtp.host SMTP_PORT=587 SMTP_USER=your-email@example.com SMTP_PASS=your-smtp-password # (Optional) Override the session_secret from config.yaml. # APP_SECRET=change-me-in-production ENVEOF fi info ".env file created." else info ".env already exists, keeping existing file." fi # ─── 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://github.com/cclohmar/inboxer After=network-online.target Wants=network-online.target [Service] Type=simple User=${SERVICE_USER} Group=${SERVICE_GROUP} WorkingDirectory=${INSTALL_DIR} EnvironmentFile=${ENV_FILE} 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 [Install] WantedBy=multi-user.target UNITEOF # ─── Set file permissions ──────────────────────────────────────────────────── info "Setting file ownership and permissions..." chown -R "${SERVICE_USER}:${SERVICE_GROUP}" "${INSTALL_DIR}" # Directory permissions chmod 755 "${BIN_DIR}" chmod 750 "${DATA_DIR}" # database file is sensitive chmod 750 "${LOGS_DIR}" # .env must be readable by the service user (and owner-only for secrets) chmod 640 "${ENV_FILE}" # ─── 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..." systemctl start "${SERVICE_NAME}" # 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}/" info " Environment: ${ENV_FILE}" echo "" info " Edit the environment file with your credentials:" info " sudo nano ${ENV_FILE}" echo "" if grep -q "your_deepseek_api_key_here" "${ENV_FILE}" 2>/dev/null; then warn " ⚠ The .env file still contains placeholder values!" warn " Edit ${ENV_FILE} 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 ""