#!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── # JMAP Webmail — Interactive Setup Script # Fullscreen terminal installer for configuring and deploying JMAP Webmail # ───────────────────────────────────────────────────────────────────────────── set -euo pipefail # ── CLI Flags ─────────────────────────────────────────────────────────────── DRY_RUN=false for arg in "$@"; do case "$arg" in --dry-run) DRY_RUN=true ;; -h|--help) echo "Usage: bash setup.sh [--dry-run]" echo " --dry-run Walk through the installer without writing files or running commands" exit 0 ;; esac done # ── Colors & Symbols ──────────────────────────────────────────────────────── BOLD='\033[1m' DIM='\033[2m' UNDERLINE='\033[4m' RESET='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' WHITE='\033[1;37m' BG_BLUE='\033[44m' BG_GREEN='\033[42m' BG_RED='\033[41m' BG_MAGENTA='\033[45m' BG_CYAN='\033[46m' BG_DARK='\033[40m' CHECKMARK="${GREEN}✓${RESET}" CROSSMARK="${RED}✗${RESET}" ARROW="${CYAN}➜${RESET}" DOT="${DIM}·${RESET}" BULLET="${BLUE}●${RESET}" # ── State ─────────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" ENV_FILE="${SCRIPT_DIR}/.env.local" # Config values (defaults) CFG_APP_NAME="JMAP Webmail" CFG_JMAP_SERVER_URL="" CFG_STALWART_FEATURES="true" CFG_OAUTH_ENABLED="false" CFG_OAUTH_ONLY="false" CFG_OAUTH_CLIENT_ID="" CFG_OAUTH_CLIENT_SECRET="" CFG_OAUTH_ISSUER_URL="" CFG_SESSION_SECRET="" CFG_SETTINGS_SYNC_ENABLED="false" CFG_SETTINGS_DATA_DIR="./data/settings" CFG_LOG_FORMAT="text" CFG_LOG_LEVEL="info" CFG_LOGIN_COMPANY_NAME="" CFG_LOGIN_IMPRINT_URL="" CFG_LOGIN_PRIVACY_POLICY_URL="" CFG_LOGIN_WEBSITE_URL="" CFG_DEPLOY_METHOD="" # "docker" | "node" | "compose" CFG_PORT="3000" # Navigation CURRENT_STEP=0 TOTAL_STEPS=7 # ── Terminal Utilities ────────────────────────────────────────────────────── get_term_size() { TERM_COLS=$(tput cols 2>/dev/null || echo 80) TERM_ROWS=$(tput lines 2>/dev/null || echo 24) } clear_screen() { printf '\033[2J\033[H' } hide_cursor() { printf '\033[?25l' } show_cursor() { printf '\033[?25h' } move_to() { printf '\033[%d;%dH' "$1" "$2" } # Print centered text print_center() { local text="$1" local clean_text clean_text=$(echo -e "$text" | sed 's/\x1b\[[0-9;]*m//g') local padding=$(( (TERM_COLS - ${#clean_text}) / 2 )) [[ $padding -lt 0 ]] && padding=0 printf "%${padding}s" "" echo -e "$text" } # Print a horizontal rule print_hr() { local char="${1:-─}" local color="${2:-$DIM}" echo -e "${color}$(printf '%*s' "$TERM_COLS" '' | tr ' ' "$char")${RESET}" } # Print text right-aligned print_right() { local text="$1" local clean_text clean_text=$(echo -e "$text" | sed 's/\x1b\[[0-9;]*m//g') local padding=$(( TERM_COLS - ${#clean_text} )) [[ $padding -lt 0 ]] && padding=0 printf "%${padding}s" "" echo -e "$text" } # Read a single key press read_key() { local key IFS= read -rsn1 key # Handle escape sequences (arrow keys, etc.) if [[ "$key" == $'\x1b' ]]; then read -rsn2 -t 0.1 key2 || true key="${key}${key2}" fi echo "$key" } # Prompt with default value prompt_value() { local label="$1" local default="$2" local var_name="$3" local required="${4:-false}" show_cursor if [[ -n "$default" ]]; then echo -ne " ${BOLD}${label}${RESET} ${DIM}[${default}]${RESET}: " else echo -ne " ${BOLD}${label}${RESET}: " fi local input read -r input input="${input:-$default}" if [[ "$required" == "true" && -z "$input" ]]; then echo -e " ${RED}This field is required.${RESET}" prompt_value "$label" "$default" "$var_name" "$required" return fi eval "$var_name='$input'" hide_cursor } # Yes/No selector prompt_yesno() { local label="$1" local default="$2" # "true" or "false" local var_name="$3" local yn_display if [[ "$default" == "true" ]]; then yn_display="${GREEN}${BOLD}Y${RESET}${DIM}/n${RESET}" else yn_display="${DIM}y/${RESET}${GREEN}${BOLD}N${RESET}" fi show_cursor echo -ne " ${BOLD}${label}${RESET} [${yn_display}]: " local input read -r input input=$(echo "$input" | tr '[:upper:]' '[:lower:]') case "$input" in y|yes) eval "$var_name='true'" ;; n|no) eval "$var_name='false'" ;; "") eval "$var_name='$default'" ;; *) eval "$var_name='$default'" ;; esac hide_cursor } # Interactive menu selector menu_select() { local label="$1" shift local options=("$@") local selected=0 local count=${#options[@]} hide_cursor echo -e " ${BOLD}${label}${RESET}" echo "" # Save cursor position local start_row start_row=$(get_cursor_row) while true; do # Move back to the start for (( i=0; i/dev/null; do echo -ne "\r ${CYAN}${frames[$i]}${RESET} ${message}" i=$(( (i + 1) % ${#frames[@]} )) sleep 0.08 done echo -ne "\r ${CHECKMARK} ${message} \n" show_cursor } # ── Screen Rendering ──────────────────────────────────────────────────────── draw_header() { get_term_size clear_screen echo "" print_center "${BG_BLUE}${WHITE}${BOLD} ${RESET}" print_center "${BG_BLUE}${WHITE}${BOLD} JMAP WEBMAIL SETUP ${RESET}" print_center "${BG_BLUE}${WHITE}${BOLD} ${RESET}" echo "" if [[ "$DRY_RUN" == true ]]; then print_center "${BG_MAGENTA}${WHITE}${BOLD} DRY RUN — no files will be written ${RESET}" echo "" fi if [[ $CURRENT_STEP -gt 0 ]]; then draw_progress "$CURRENT_STEP" "$TOTAL_STEPS" echo "" fi print_hr "─" echo "" } draw_footer() { echo "" print_hr "─" echo "" if [[ $CURRENT_STEP -eq 0 ]]; then print_center "${DIM}Press Enter to begin${RESET}" elif [[ $CURRENT_STEP -le $TOTAL_STEPS ]]; then print_center "${DIM}Press Enter to continue · Ctrl+C to abort${RESET}" fi } # ── Screens ───────────────────────────────────────────────────────────────── screen_welcome() { CURRENT_STEP=0 draw_header local version version=$(cat "${SCRIPT_DIR}/VERSION" 2>/dev/null || echo "unknown") print_center "${BOLD}Welcome to the JMAP Webmail installer${RESET}" echo "" print_center "${DIM}Version ${version}${RESET}" echo "" echo "" print_center "This script will guide you through:" echo "" print_center "${BULLET} Configuring your JMAP mail server connection" print_center "${BULLET} Setting up authentication (Basic / OAuth2)" print_center "${BULLET} Configuring security & session settings" print_center "${BULLET} Customizing the login page" print_center "${BULLET} Choosing a deployment method" print_center "${BULLET} Installing dependencies & building" echo "" echo "" # System checks print_center "${BOLD}System Check${RESET}" echo "" local all_ok=true # Node.js if command -v node &>/dev/null; then local node_ver node_ver=$(node --version) print_center "${CHECKMARK} Node.js ${node_ver}" else print_center "${CROSSMARK} Node.js not found" all_ok=false fi # npm if command -v npm &>/dev/null; then local npm_ver npm_ver=$(npm --version) print_center "${CHECKMARK} npm v${npm_ver}" else print_center "${CROSSMARK} npm not found" all_ok=false fi # Docker if command -v docker &>/dev/null; then local docker_ver docker_ver=$(docker --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1 || echo "installed") print_center "${CHECKMARK} Docker v${docker_ver}" else print_center "${DIM}${DOT} Docker not found (optional)${RESET}" fi # Git if command -v git &>/dev/null; then local git_ver git_ver=$(git --version | grep -oP '\d+\.\d+\.\d+' || echo "installed") print_center "${CHECKMARK} Git v${git_ver}" else print_center "${DIM}${DOT} Git not found (optional)${RESET}" fi # openssl if command -v openssl &>/dev/null; then print_center "${CHECKMARK} OpenSSL available" else print_center "${DIM}${DOT} OpenSSL not found (can generate secrets manually)${RESET}" fi echo "" if [[ "$all_ok" == false ]]; then echo "" print_center "${YELLOW}⚠ Some required tools are missing.${RESET}" print_center "${YELLOW} Install Node.js 18+ before continuing.${RESET}" fi draw_footer read -r } screen_server_config() { CURRENT_STEP=1 draw_header print_center "${BOLD}${CYAN}Step 1: Server Configuration${RESET}" echo "" echo "" echo -e " ${BOLD}Configure your JMAP mail server connection.${RESET}" echo -e " ${DIM}This is the URL of your Stalwart (or compatible) mail server.${RESET}" echo "" prompt_value "Application name" "$CFG_APP_NAME" "CFG_APP_NAME" echo "" prompt_value "JMAP Server URL" "$CFG_JMAP_SERVER_URL" "CFG_JMAP_SERVER_URL" "true" echo "" # Validate URL format if [[ "$CFG_JMAP_SERVER_URL" =~ ^https?:// ]]; then echo -e " ${CHECKMARK} URL format looks valid" else echo -e " ${YELLOW}⚠ URL should start with https:// or http://${RESET}" prompt_value "JMAP Server URL" "https://${CFG_JMAP_SERVER_URL}" "CFG_JMAP_SERVER_URL" "true" fi echo "" prompt_yesno "Enable Stalwart-specific features? (password change, sieve filters)" "$CFG_STALWART_FEATURES" "CFG_STALWART_FEATURES" echo "" echo -e " ${DIM}Select the port to run on:${RESET}" prompt_value "Port" "$CFG_PORT" "CFG_PORT" draw_footer read -r } screen_auth_config() { CURRENT_STEP=2 draw_header print_center "${BOLD}${CYAN}Step 2: Authentication${RESET}" echo "" echo "" echo -e " ${BOLD}Configure how users authenticate.${RESET}" echo -e " ${DIM}Basic Auth is always available. You can add OAuth2/OIDC for SSO.${RESET}" echo "" prompt_yesno "Enable OAuth2/OIDC?" "$CFG_OAUTH_ENABLED" "CFG_OAUTH_ENABLED" if [[ "$CFG_OAUTH_ENABLED" == "true" ]]; then echo "" echo -e " ${ARROW} ${BOLD}OAuth2 Configuration${RESET}" echo "" prompt_yesno "OAuth-only mode? (hides password form)" "$CFG_OAUTH_ONLY" "CFG_OAUTH_ONLY" echo "" prompt_value "OAuth Client ID" "$CFG_OAUTH_CLIENT_ID" "CFG_OAUTH_CLIENT_ID" "true" echo "" prompt_value "OAuth Client Secret" "$CFG_OAUTH_CLIENT_SECRET" "CFG_OAUTH_CLIENT_SECRET" echo -e " ${DIM}Leave empty for public clients (PKCE-only).${RESET}" echo "" prompt_value "OAuth Issuer URL" "$CFG_OAUTH_ISSUER_URL" "CFG_OAUTH_ISSUER_URL" echo -e " ${DIM}Set this for external IdPs (Keycloak, Authentik). Leave empty for Stalwart's built-in OAuth.${RESET}" fi draw_footer read -r } screen_security_config() { CURRENT_STEP=3 draw_header print_center "${BOLD}${CYAN}Step 3: Security & Sessions${RESET}" echo "" echo "" echo -e " ${BOLD}Configure session security and \"Remember me\" functionality.${RESET}" echo -e " ${DIM}A session secret enables encrypted persistent sessions and settings sync.${RESET}" echo "" local generate_secret="false" if [[ -z "$CFG_SESSION_SECRET" ]]; then prompt_yesno "Generate a secure session secret?" "true" "generate_secret" if [[ "$generate_secret" == "true" ]]; then if command -v openssl &>/dev/null; then CFG_SESSION_SECRET=$(openssl rand -base64 32) echo -e " ${CHECKMARK} Generated session secret" else CFG_SESSION_SECRET=$(head -c 32 /dev/urandom | base64) echo -e " ${CHECKMARK} Generated session secret (via /dev/urandom)" fi else prompt_value "Session secret" "" "CFG_SESSION_SECRET" fi else echo -e " ${CHECKMARK} Session secret is already set" prompt_yesno "Regenerate session secret?" "false" "generate_secret" if [[ "$generate_secret" == "true" ]]; then if command -v openssl &>/dev/null; then CFG_SESSION_SECRET=$(openssl rand -base64 32) else CFG_SESSION_SECRET=$(head -c 32 /dev/urandom | base64) fi echo -e " ${CHECKMARK} Regenerated session secret" fi fi echo "" if [[ -n "$CFG_SESSION_SECRET" ]]; then echo -e " ${BULLET} \"Remember me\" checkbox will appear on login" echo -e " ${BULLET} Credentials encrypted with AES-256-GCM" echo "" prompt_yesno "Enable settings sync across devices?" "$CFG_SETTINGS_SYNC_ENABLED" "CFG_SETTINGS_SYNC_ENABLED" if [[ "$CFG_SETTINGS_SYNC_ENABLED" == "true" ]]; then echo "" prompt_value "Settings data directory" "$CFG_SETTINGS_DATA_DIR" "CFG_SETTINGS_DATA_DIR" fi else echo -e " ${DIM}Without a session secret, \"Remember me\" and settings sync are disabled.${RESET}" fi draw_footer read -r } screen_logging_config() { CURRENT_STEP=4 draw_header print_center "${BOLD}${CYAN}Step 4: Logging${RESET}" echo "" echo "" echo -e " ${BOLD}Configure application logging.${RESET}" echo "" menu_select "Log format:" "text (colored, human-readable)" "json (structured, for log aggregation)" case $MENU_RESULT in 0) CFG_LOG_FORMAT="text" ;; 1) CFG_LOG_FORMAT="json" ;; esac echo "" menu_select "Log level:" "error (errors only)" "warn (errors + warnings)" "info (standard — recommended)" "debug (verbose, for development)" case $MENU_RESULT in 0) CFG_LOG_LEVEL="error" ;; 1) CFG_LOG_LEVEL="warn" ;; 2) CFG_LOG_LEVEL="info" ;; 3) CFG_LOG_LEVEL="debug" ;; esac draw_footer read -r } screen_login_customization() { CURRENT_STEP=5 draw_header print_center "${BOLD}${CYAN}Step 5: Login Page Customization${RESET}" echo "" echo "" echo -e " ${BOLD}Customize the login page appearance.${RESET}" echo -e " ${DIM}All fields are optional. Leave blank to skip.${RESET}" echo "" prompt_value "Company / organization name" "$CFG_LOGIN_COMPANY_NAME" "CFG_LOGIN_COMPANY_NAME" echo "" prompt_value "Website URL" "$CFG_LOGIN_WEBSITE_URL" "CFG_LOGIN_WEBSITE_URL" echo "" prompt_value "Imprint / legal notice URL" "$CFG_LOGIN_IMPRINT_URL" "CFG_LOGIN_IMPRINT_URL" echo "" prompt_value "Privacy policy URL" "$CFG_LOGIN_PRIVACY_POLICY_URL" "CFG_LOGIN_PRIVACY_POLICY_URL" draw_footer read -r } screen_deployment() { CURRENT_STEP=6 draw_header print_center "${BOLD}${CYAN}Step 6: Deployment Method${RESET}" echo "" echo "" echo -e " ${BOLD}How would you like to deploy JMAP Webmail?${RESET}" echo "" local docker_available=false command -v docker &>/dev/null && docker_available=true menu_select "Choose deployment method:" \ "Node.js (npm install → npm run build → npm start)" \ "Docker (pull pre-built image and run)" \ "Docker Compose (use docker-compose.yml)" case $MENU_RESULT in 0) CFG_DEPLOY_METHOD="node" ;; 1) CFG_DEPLOY_METHOD="docker" ;; 2) CFG_DEPLOY_METHOD="compose" ;; esac if [[ "$CFG_DEPLOY_METHOD" != "node" && "$docker_available" == false ]]; then echo "" echo -e " ${YELLOW}⚠ Docker is not installed. You can still generate the config${RESET}" echo -e " ${YELLOW} and run Docker commands later.${RESET}" fi draw_footer read -r } screen_summary() { CURRENT_STEP=7 draw_header print_center "${BOLD}${CYAN}Step 7: Review & Confirm${RESET}" echo "" echo "" echo -e " ${BOLD}Configuration Summary${RESET}" echo "" print_hr "─" echo "" # Server echo -e " ${UNDERLINE}Server${RESET}" echo -e " App Name ${BOLD}${CFG_APP_NAME}${RESET}" echo -e " JMAP Server URL ${BOLD}${CFG_JMAP_SERVER_URL}${RESET}" echo -e " Stalwart Features ${BOLD}${CFG_STALWART_FEATURES}${RESET}" echo -e " Port ${BOLD}${CFG_PORT}${RESET}" echo "" # Auth echo -e " ${UNDERLINE}Authentication${RESET}" if [[ "$CFG_OAUTH_ENABLED" == "true" ]]; then echo -e " OAuth2/OIDC ${GREEN}Enabled${RESET}" echo -e " OAuth-only ${BOLD}${CFG_OAUTH_ONLY}${RESET}" echo -e " Client ID ${BOLD}${CFG_OAUTH_CLIENT_ID}${RESET}" [[ -n "$CFG_OAUTH_CLIENT_SECRET" ]] && echo -e " Client Secret ${BOLD}••••••••${RESET}" [[ -n "$CFG_OAUTH_ISSUER_URL" ]] && echo -e " Issuer URL ${BOLD}${CFG_OAUTH_ISSUER_URL}${RESET}" else echo -e " OAuth2/OIDC ${DIM}Disabled (Basic Auth only)${RESET}" fi echo "" # Security echo -e " ${UNDERLINE}Security${RESET}" if [[ -n "$CFG_SESSION_SECRET" ]]; then echo -e " Session Secret ${GREEN}Set${RESET} ${DIM}(Remember me enabled)${RESET}" echo -e " Settings Sync ${BOLD}${CFG_SETTINGS_SYNC_ENABLED}${RESET}" [[ "$CFG_SETTINGS_SYNC_ENABLED" == "true" ]] && \ echo -e " Settings Dir ${BOLD}${CFG_SETTINGS_DATA_DIR}${RESET}" else echo -e " Session Secret ${DIM}Not set${RESET}" fi echo "" # Logging echo -e " ${UNDERLINE}Logging${RESET}" echo -e " Format ${BOLD}${CFG_LOG_FORMAT}${RESET}" echo -e " Level ${BOLD}${CFG_LOG_LEVEL}${RESET}" echo "" # Login page echo -e " ${UNDERLINE}Login Page${RESET}" [[ -n "$CFG_LOGIN_COMPANY_NAME" ]] && echo -e " Company Name ${BOLD}${CFG_LOGIN_COMPANY_NAME}${RESET}" [[ -n "$CFG_LOGIN_WEBSITE_URL" ]] && echo -e " Website URL ${BOLD}${CFG_LOGIN_WEBSITE_URL}${RESET}" [[ -n "$CFG_LOGIN_IMPRINT_URL" ]] && echo -e " Imprint URL ${BOLD}${CFG_LOGIN_IMPRINT_URL}${RESET}" [[ -n "$CFG_LOGIN_PRIVACY_POLICY_URL" ]] && echo -e " Privacy Policy ${BOLD}${CFG_LOGIN_PRIVACY_POLICY_URL}${RESET}" if [[ -z "$CFG_LOGIN_COMPANY_NAME" && -z "$CFG_LOGIN_WEBSITE_URL" && -z "$CFG_LOGIN_IMPRINT_URL" && -z "$CFG_LOGIN_PRIVACY_POLICY_URL" ]]; then echo -e " ${DIM}(defaults)${RESET}" fi echo "" # Deployment echo -e " ${UNDERLINE}Deployment${RESET}" case "$CFG_DEPLOY_METHOD" in "node") echo -e " Method ${BOLD}Node.js (local build)${RESET}" ;; "docker") echo -e " Method ${BOLD}Docker (pre-built image)${RESET}" ;; "compose") echo -e " Method ${BOLD}Docker Compose${RESET}" ;; esac echo "" print_hr "─" echo "" local confirm prompt_yesno "Apply this configuration and proceed?" "true" "confirm" if [[ "$confirm" != "true" ]]; then echo "" echo -e " ${YELLOW}Setup cancelled. No files were modified.${RESET}" echo "" show_cursor exit 0 fi } # ── Write Configuration ──────────────────────────────────────────────────── write_env_file() { if [[ "$DRY_RUN" == true ]]; then echo -e " ${DIM}[dry-run]${RESET} Would back up existing .env.local" echo -e " ${DIM}[dry-run]${RESET} Would write .env.local with the above configuration" return fi # Backup existing if present if [[ -f "$ENV_FILE" ]]; then local backup="${ENV_FILE}.backup.$(date +%Y%m%d_%H%M%S)" cp "$ENV_FILE" "$backup" echo -e " ${CHECKMARK} Backed up existing .env.local → ${DIM}$(basename "$backup")${RESET}" fi cat > "$ENV_FILE" << ENVEOF # ───────────────────────────────────────────────────────────────────────────── # JMAP Webmail — Configuration # Generated by setup.sh on $(date '+%Y-%m-%d %H:%M:%S') # ───────────────────────────────────────────────────────────────────────────── # ============================================================================= # JMAP Server (required) # ============================================================================= APP_NAME=${CFG_APP_NAME} JMAP_SERVER_URL=${CFG_JMAP_SERVER_URL} # ============================================================================= # Stalwart Mail Server Integration # ============================================================================= STALWART_FEATURES=${CFG_STALWART_FEATURES} # ============================================================================= # OAuth / OpenID Connect # ============================================================================= OAUTH_ENABLED=${CFG_OAUTH_ENABLED} ENVEOF if [[ "$CFG_OAUTH_ENABLED" == "true" ]]; then cat >> "$ENV_FILE" << ENVEOF OAUTH_ONLY=${CFG_OAUTH_ONLY} OAUTH_CLIENT_ID=${CFG_OAUTH_CLIENT_ID} ENVEOF [[ -n "$CFG_OAUTH_CLIENT_SECRET" ]] && echo "OAUTH_CLIENT_SECRET=${CFG_OAUTH_CLIENT_SECRET}" >> "$ENV_FILE" [[ -n "$CFG_OAUTH_ISSUER_URL" ]] && echo "OAUTH_ISSUER_URL=${CFG_OAUTH_ISSUER_URL}" >> "$ENV_FILE" fi cat >> "$ENV_FILE" << ENVEOF # ============================================================================= # Session & Security # ============================================================================= ENVEOF if [[ -n "$CFG_SESSION_SECRET" ]]; then echo "SESSION_SECRET=${CFG_SESSION_SECRET}" >> "$ENV_FILE" else echo "# SESSION_SECRET=" >> "$ENV_FILE" fi cat >> "$ENV_FILE" << ENVEOF # ============================================================================= # Settings Sync # ============================================================================= SETTINGS_SYNC_ENABLED=${CFG_SETTINGS_SYNC_ENABLED} ENVEOF if [[ "$CFG_SETTINGS_SYNC_ENABLED" == "true" ]]; then echo "SETTINGS_DATA_DIR=${CFG_SETTINGS_DATA_DIR}" >> "$ENV_FILE" fi cat >> "$ENV_FILE" << ENVEOF # ============================================================================= # Logging # ============================================================================= LOG_FORMAT=${CFG_LOG_FORMAT} LOG_LEVEL=${CFG_LOG_LEVEL} # ============================================================================= # Login Page Customization # ============================================================================= ENVEOF [[ -n "$CFG_LOGIN_COMPANY_NAME" ]] && echo "LOGIN_COMPANY_NAME=${CFG_LOGIN_COMPANY_NAME}" >> "$ENV_FILE" [[ -n "$CFG_LOGIN_IMPRINT_URL" ]] && echo "LOGIN_IMPRINT_URL=${CFG_LOGIN_IMPRINT_URL}" >> "$ENV_FILE" [[ -n "$CFG_LOGIN_PRIVACY_POLICY_URL" ]] && echo "LOGIN_PRIVACY_POLICY_URL=${CFG_LOGIN_PRIVACY_POLICY_URL}" >> "$ENV_FILE" [[ -n "$CFG_LOGIN_WEBSITE_URL" ]] && echo "LOGIN_WEBSITE_URL=${CFG_LOGIN_WEBSITE_URL}" >> "$ENV_FILE" echo -e " ${CHECKMARK} Written ${BOLD}.env.local${RESET}" } update_docker_compose_port() { if [[ "$CFG_PORT" != "3000" && -f "${SCRIPT_DIR}/docker-compose.yml" ]]; then if [[ "$DRY_RUN" == true ]]; then echo -e " ${DIM}[dry-run]${RESET} Would update docker-compose.yml port → ${CFG_PORT}:3000" return fi sed -i.bak "s/\"3000:3000\"/\"${CFG_PORT}:3000\"/" "${SCRIPT_DIR}/docker-compose.yml" 2>/dev/null || true echo -e " ${CHECKMARK} Updated docker-compose.yml port mapping" fi } run_deployment() { echo "" if [[ "$DRY_RUN" == true ]]; then case "$CFG_DEPLOY_METHOD" in "node") echo -e " ${DIM}[dry-run]${RESET} Would run: npm install" echo -e " ${DIM}[dry-run]${RESET} Would run: npm run build" echo -e " ${DIM}[dry-run]${RESET} Would start with: npm start (port ${CFG_PORT})" ;; "docker") echo -e " ${DIM}[dry-run]${RESET} Would run: docker pull ghcr.io/root-fr/jmap-webmail:latest" echo -e " ${DIM}[dry-run]${RESET} Would start container on port ${CFG_PORT}" ;; "compose") echo -e " ${DIM}[dry-run]${RESET} Would update docker-compose.yml port mapping" echo -e " ${DIM}[dry-run]${RESET} Would run: docker compose up -d" ;; esac return fi case "$CFG_DEPLOY_METHOD" in "node") echo -e " ${ARROW} Installing dependencies..." (cd "$SCRIPT_DIR" && npm install --loglevel=warn) & spinner $! "Installing npm packages" echo -e " ${ARROW} Building application..." (cd "$SCRIPT_DIR" && npm run build 2>&1 | tail -5) & spinner $! "Building Next.js application" echo "" echo -e " ${CHECKMARK} Build complete!" echo "" echo -e " ${BOLD}Start the application:${RESET}" echo "" echo -e " ${CYAN}cd ${SCRIPT_DIR}${RESET}" if [[ "$CFG_PORT" != "3000" ]]; then echo -e " ${CYAN}PORT=${CFG_PORT} npm start${RESET}" else echo -e " ${CYAN}npm start${RESET}" fi ;; "docker") echo -e " ${ARROW} Pulling Docker image..." (docker pull ghcr.io/root-fr/jmap-webmail:latest 2>&1) & spinner $! "Pulling ghcr.io/root-fr/jmap-webmail:latest" echo "" echo -e " ${CHECKMARK} Image pulled!" echo "" echo -e " ${BOLD}Run the container:${RESET}" echo "" echo -e " ${CYAN}docker run -d \\" echo -e " --name jmap-webmail \\" echo -e " -p ${CFG_PORT}:3000 \\" echo -e " --env-file .env.local \\" echo -e " --restart unless-stopped \\" echo -e " ghcr.io/root-fr/jmap-webmail:latest${RESET}" ;; "compose") update_docker_compose_port echo -e " ${ARROW} Starting with Docker Compose..." (cd "$SCRIPT_DIR" && docker compose up -d 2>&1) & spinner $! "Starting Docker Compose services" echo "" echo -e " ${CHECKMARK} Services started!" echo "" echo -e " ${BOLD}Manage with:${RESET}" echo "" echo -e " ${CYAN}docker compose logs -f${RESET} ${DIM}# View logs${RESET}" echo -e " ${CYAN}docker compose restart${RESET} ${DIM}# Restart${RESET}" echo -e " ${CYAN}docker compose down${RESET} ${DIM}# Stop${RESET}" ;; esac } # ── Completion Screen ─────────────────────────────────────────────────────── screen_complete() { draw_header echo "" if [[ "$DRY_RUN" == true ]]; then print_center "${BG_MAGENTA}${WHITE}${BOLD} ${RESET}" print_center "${BG_MAGENTA}${WHITE}${BOLD} DRY RUN COMPLETE! ${RESET}" print_center "${BG_MAGENTA}${WHITE}${BOLD} ${RESET}" else print_center "${BG_GREEN}${WHITE}${BOLD} ${RESET}" print_center "${BG_GREEN}${WHITE}${BOLD} SETUP COMPLETE! ${RESET}" print_center "${BG_GREEN}${WHITE}${BOLD} ${RESET}" fi echo "" echo "" write_env_file echo "" run_deployment echo "" echo "" print_hr "─" echo "" local url="http://localhost:${CFG_PORT}" print_center "${BOLD}Access your webmail at:${RESET}" echo "" print_center "${UNDERLINE}${CYAN}${url}${RESET}" echo "" echo "" print_center "${DIM}Configuration: .env.local${RESET}" print_center "${DIM}Documentation: README.md${RESET}" echo "" print_hr "─" echo "" print_center "${DIM}Thank you for using JMAP Webmail!${RESET}" echo "" } # ── Signal Handling ───────────────────────────────────────────────────────── cleanup() { show_cursor clear_screen echo -e "\n ${YELLOW}Setup interrupted. No changes were made.${RESET}\n" exit 1 } trap cleanup INT TERM # ── Main ──────────────────────────────────────────────────────────────────── main() { # Check for minimum terminal size get_term_size if [[ $TERM_COLS -lt 60 || $TERM_ROWS -lt 20 ]]; then echo "Terminal too small. Minimum: 60x20 (current: ${TERM_COLS}x${TERM_ROWS})" exit 1 fi # Load existing .env.local values if present if [[ -f "$ENV_FILE" ]]; then load_existing_config fi hide_cursor screen_welcome screen_server_config screen_auth_config screen_security_config screen_logging_config screen_login_customization screen_deployment screen_summary screen_complete show_cursor } load_existing_config() { local val get_env_val() { local key="$1" val=$(grep -E "^${key}=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d'=' -f2-) val="${val#\"}" ; val="${val%\"}" # strip quotes val="${val#\'}" ; val="${val%\'}" } get_env_val "APP_NAME"; [[ -n "$val" ]] && CFG_APP_NAME="$val" get_env_val "JMAP_SERVER_URL"; [[ -n "$val" ]] && CFG_JMAP_SERVER_URL="$val" get_env_val "STALWART_FEATURES"; [[ -n "$val" ]] && CFG_STALWART_FEATURES="$val" get_env_val "OAUTH_ENABLED"; [[ -n "$val" ]] && CFG_OAUTH_ENABLED="$val" get_env_val "OAUTH_ONLY"; [[ -n "$val" ]] && CFG_OAUTH_ONLY="$val" get_env_val "OAUTH_CLIENT_ID"; [[ -n "$val" ]] && CFG_OAUTH_CLIENT_ID="$val" get_env_val "OAUTH_CLIENT_SECRET"; [[ -n "$val" ]] && CFG_OAUTH_CLIENT_SECRET="$val" get_env_val "OAUTH_ISSUER_URL"; [[ -n "$val" ]] && CFG_OAUTH_ISSUER_URL="$val" get_env_val "SESSION_SECRET"; [[ -n "$val" ]] && CFG_SESSION_SECRET="$val" get_env_val "SETTINGS_SYNC_ENABLED"; [[ -n "$val" ]] && CFG_SETTINGS_SYNC_ENABLED="$val" get_env_val "SETTINGS_DATA_DIR"; [[ -n "$val" ]] && CFG_SETTINGS_DATA_DIR="$val" get_env_val "LOG_FORMAT"; [[ -n "$val" ]] && CFG_LOG_FORMAT="$val" get_env_val "LOG_LEVEL"; [[ -n "$val" ]] && CFG_LOG_LEVEL="$val" get_env_val "LOGIN_COMPANY_NAME"; [[ -n "$val" ]] && CFG_LOGIN_COMPANY_NAME="$val" get_env_val "LOGIN_IMPRINT_URL"; [[ -n "$val" ]] && CFG_LOGIN_IMPRINT_URL="$val" get_env_val "LOGIN_PRIVACY_POLICY_URL"; [[ -n "$val" ]] && CFG_LOGIN_PRIVACY_POLICY_URL="$val" get_env_val "LOGIN_WEBSITE_URL"; [[ -n "$val" ]] && CFG_LOGIN_WEBSITE_URL="$val" } main "$@"