diff --git a/.gitignore b/.gitignore index 2f56782..27c015e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ # Environment .env -# Build artifacts -bin/inboxer +# Build artifacts (binary is distributed with the repo) bin/*.log bin/db.sqlite bin/db.sqlite-wal diff --git a/bin/classify_prompt.txt b/bin/classify_prompt.txt deleted file mode 100644 index c541599..0000000 --- a/bin/classify_prompt.txt +++ /dev/null @@ -1,18 +0,0 @@ -You are an email classification assistant. Analyze the email content and categorize it into one of these folders: - -1. **Important** - Urgent emails, work-related, personal correspondence, bills, appointments, time-sensitive matters -2. **eCommerce** - Shopping confirmations, order updates, shipping notifications, receipts, promotional offers from stores -3. **Spam** - Unsolicited commercial emails, phishing attempts, scams, obvious junk mail -4. **Other** - Everything else that doesn't fit the above categories - -Consider the sender, subject, and email body. Respond with a JSON object in this exact format: -{ - "folder": "Important|eCommerce|Spam|Other", - "score": 1-100, - "context": "Brief explanation of why this classification was chosen" -} - -Email to classify: -From: {sender} -Subject: {subject} -Body: {body} \ No newline at end of file diff --git a/bin/inboxer b/bin/inboxer new file mode 100755 index 0000000..d98ec07 Binary files /dev/null and b/bin/inboxer differ diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..72c5943 --- /dev/null +++ b/install.sh @@ -0,0 +1,242 @@ +#!/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 template ──────────────────────────────────────────────────── +ENV_FILE="${INSTALL_DIR}/.env" +if [[ ! -f "${ENV_FILE}" ]]; then + 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 + info ".env template 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 ""