feat: add JMAP server URL validation and improve user prompts in setup script

This commit is contained in:
Linus Rath
2026-03-13 01:14:56 +01:00
parent 18485951c7
commit 25f0088957
+109 -34
View File
@@ -129,19 +129,77 @@ box() {
echo -e "${sp}${color}${top}${RESET}" echo -e "${sp}${color}${top}${RESET}"
} }
# Print a tip box (left-aligned, indented)
tip() {
local text="$1"
echo ""
echo -e " ${YELLOW}${BOLD}TIP:${RESET} ${DIM}${text}${RESET}"
}
# Print an info note # Print an info note
note() { note() {
local text="$1" local text="$1"
echo -e " ${CYAN}${BOLD}NOTE:${RESET} ${DIM}${text}${RESET}" echo -e " ${CYAN}${BOLD}NOTE:${RESET} ${DIM}${text}${RESET}"
} }
# Validate that a URL points to a JMAP server
check_jmap_server() {
local url="$1"
# Need curl or wget
local http_tool=""
if command -v curl &>/dev/null; then
http_tool="curl"
elif command -v wget &>/dev/null; then
http_tool="wget"
else
echo -e " ${SKIP} Cannot verify server (curl/wget not found)"
return 0
fi
# Strip trailing slash
url="${url%/}"
echo -ne " ${DIM}Checking JMAP server...${RESET}"
local response=""
local http_code=""
if [[ "$http_tool" == "curl" ]]; then
# Try /.well-known/jmap first
http_code=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 --max-time 10 "${url}/.well-known/jmap" 2>/dev/null || echo "000")
if [[ "$http_code" =~ ^(200|301|302|308)$ ]]; then
response=$(curl -s --connect-timeout 5 --max-time 10 -L "${url}/.well-known/jmap" 2>/dev/null || echo "")
fi
else
response=$(wget -q --timeout=10 -O - "${url}/.well-known/jmap" 2>/dev/null || echo "")
[[ -n "$response" ]] && http_code="200" || http_code="000"
fi
echo -ne "\r \r"
# Check if we got no response at all (server unreachable)
if [[ "$http_code" == "000" ]]; then
echo -e " ${FAIL} ${RED}Could not connect to ${url}${RESET}"
echo -e " ${DIM}Check that the URL is correct and the server is running.${RESET}"
return 1
fi
# Check for JMAP session resource indicators
if [[ -n "$response" ]] && echo "$response" | grep -qiE '"capabilities"|"apiUrl"|"downloadUrl"|jmap'; then
echo -e " ${OK} ${GREEN}Verified: JMAP server detected${RESET}"
return 0
fi
# Server responded but doesn't look like JMAP
echo -e " ${WARN} ${YELLOW}Server responded (HTTP ${http_code}) but no JMAP session found${RESET}"
echo -e " ${DIM}Expected a JMAP session resource at /.well-known/jmap${RESET}"
echo -e " ${DIM}This might still work if your server uses a different path.${RESET}"
local continue_anyway="true"
show_cursor
prompt_yesno "Continue anyway?" "true" "continue_anyway"
hide_cursor
if [[ "$continue_anyway" != "true" ]]; then
return 1
fi
return 0
}
# Read a single key press (for menu navigation) # Read a single key press (for menu navigation)
read_key() { read_key() {
local key local key
@@ -463,37 +521,53 @@ screen_server_config() {
"Point JMAP Webmail at your mail server." "Point JMAP Webmail at your mail server."
echo -e " ${BOLD}General${RESET}" echo -e " ${BOLD}General${RESET}"
echo -e " ${DIM}This name appears in the browser tab and on the login page.${RESET}"
echo "" echo ""
prompt_value "Application name" "$CFG_APP_NAME" "CFG_APP_NAME" prompt_value "Application name" "$CFG_APP_NAME" "CFG_APP_NAME"
tip "This name appears in the browser tab and on the login page."
echo "" echo ""
echo -e " ${BOLD}Mail Server${RESET}" echo -e " ${BOLD}Mail Server${RESET}"
echo "" echo -e " ${DIM}Enter the base URL of your Stalwart (or JMAP-compatible) server.${RESET}"
prompt_value "JMAP server URL" "$CFG_JMAP_SERVER_URL" "CFG_JMAP_SERVER_URL" "true"
# Validate URL format
if [[ "$CFG_JMAP_SERVER_URL" =~ ^https?:// ]]; then
echo -e " ${OK} URL format looks valid"
else
echo -e " ${WARN} URL should start with https:// or http://"
prompt_value "JMAP server URL" "https://${CFG_JMAP_SERVER_URL}" "CFG_JMAP_SERVER_URL" "true"
fi
tip "Enter the base URL of your Stalwart (or JMAP-compatible) server."
echo -e " ${DIM}Example: https://mail.example.com${RESET}" echo -e " ${DIM}Example: https://mail.example.com${RESET}"
echo "" echo ""
local jmap_url_valid=false
while [[ "$jmap_url_valid" == false ]]; do
prompt_value "JMAP server URL" "$CFG_JMAP_SERVER_URL" "CFG_JMAP_SERVER_URL" "true"
# Validate URL format
if [[ ! "$CFG_JMAP_SERVER_URL" =~ ^https?:// ]]; then
echo -e " ${WARN} URL should start with https:// or http://"
CFG_JMAP_SERVER_URL="https://${CFG_JMAP_SERVER_URL}"
echo -e " ${DIM}Auto-corrected to: ${CFG_JMAP_SERVER_URL}${RESET}"
fi
echo ""
# Validate it's actually a JMAP server
if check_jmap_server "$CFG_JMAP_SERVER_URL"; then
jmap_url_valid=true
else
echo ""
echo -e " ${DIM}Please enter a different URL or fix the server.${RESET}"
echo ""
CFG_JMAP_SERVER_URL=""
fi
done
echo ""
echo -e " ${BOLD}Features${RESET}" echo -e " ${BOLD}Features${RESET}"
echo -e " ${DIM}Adds password change and Sieve filter management.${RESET}"
echo -e " ${DIM}Safe to enable even on non-Stalwart servers.${RESET}"
echo "" echo ""
prompt_yesno "Enable Stalwart-specific features?" "$CFG_STALWART_FEATURES" "CFG_STALWART_FEATURES" prompt_yesno "Enable Stalwart-specific features?" "$CFG_STALWART_FEATURES" "CFG_STALWART_FEATURES"
tip "Adds password change and Sieve filter management. Safe to enable even on non-Stalwart servers."
echo "" echo ""
echo -e " ${BOLD}Network${RESET}" echo -e " ${BOLD}Network${RESET}"
echo -e " ${DIM}The port the web UI will listen on. Default is 3000.${RESET}"
echo "" echo ""
prompt_value "Port" "$CFG_PORT" "CFG_PORT" prompt_value "Port" "$CFG_PORT" "CFG_PORT"
tip "The port the web UI will listen on. Default is 3000."
draw_footer draw_footer
read -r read -r
@@ -519,24 +593,24 @@ screen_auth_config() {
echo -e " ${BOLD}OAuth2 / OIDC Configuration${RESET}" echo -e " ${BOLD}OAuth2 / OIDC Configuration${RESET}"
echo "" echo ""
echo -e " ${DIM}Enable this if ALL users authenticate via your identity provider.${RESET}"
prompt_yesno "OAuth-only mode? (hides the password form)" "$CFG_OAUTH_ONLY" "CFG_OAUTH_ONLY" prompt_yesno "OAuth-only mode? (hides the password form)" "$CFG_OAUTH_ONLY" "CFG_OAUTH_ONLY"
tip "Use this if ALL users authenticate via your identity provider."
echo "" echo ""
prompt_value "OAuth Client ID" "$CFG_OAUTH_CLIENT_ID" "CFG_OAUTH_CLIENT_ID" "true" prompt_value "OAuth Client ID" "$CFG_OAUTH_CLIENT_ID" "CFG_OAUTH_CLIENT_ID" "true"
echo "" echo ""
echo -e " ${DIM}Leave empty for public clients using PKCE only (no secret needed).${RESET}"
prompt_value "OAuth Client Secret" "$CFG_OAUTH_CLIENT_SECRET" "CFG_OAUTH_CLIENT_SECRET" prompt_value "OAuth Client Secret" "$CFG_OAUTH_CLIENT_SECRET" "CFG_OAUTH_CLIENT_SECRET"
tip "Leave empty for public clients using PKCE only (no secret needed)."
echo "" echo ""
prompt_value "OAuth Issuer URL" "$CFG_OAUTH_ISSUER_URL" "CFG_OAUTH_ISSUER_URL" echo -e " ${DIM}For external IdPs (Keycloak, Authentik, Entra ID, etc.).${RESET}"
tip "Set this for external IdPs (Keycloak, Authentik, Entra ID, etc.)."
echo -e " ${DIM}Leave empty to use Stalwart's built-in OAuth.${RESET}" echo -e " ${DIM}Leave empty to use Stalwart's built-in OAuth.${RESET}"
prompt_value "OAuth Issuer URL" "$CFG_OAUTH_ISSUER_URL" "CFG_OAUTH_ISSUER_URL"
else else
echo "" echo ""
note "Users will log in with their email and password (Basic Auth over HTTPS)." note "Users will log in with their email and password (Basic Auth over HTTPS)."
tip "You can enable OAuth2 later by editing .env.local." echo -e " ${DIM}You can enable OAuth2 later by editing .env.local.${RESET}"
fi fi
draw_footer draw_footer
@@ -603,13 +677,13 @@ screen_security_config() {
if [[ "$CFG_SETTINGS_SYNC_ENABLED" == "true" ]]; then if [[ "$CFG_SETTINGS_SYNC_ENABLED" == "true" ]]; then
echo "" echo ""
echo -e " ${DIM}Make sure this directory is persistent and backed up.${RESET}"
prompt_value "Data directory for synced settings" "$CFG_SETTINGS_DATA_DIR" "CFG_SETTINGS_DATA_DIR" prompt_value "Data directory for synced settings" "$CFG_SETTINGS_DATA_DIR" "CFG_SETTINGS_DATA_DIR"
tip "Make sure this directory is persistent and backed up."
fi fi
else else
echo "" echo ""
note "Without a session secret, \"Remember me\" and settings sync are disabled." note "Without a session secret, \"Remember me\" and settings sync are disabled."
tip "You can add a SESSION_SECRET to .env.local at any time." echo -e " ${DIM}You can add a SESSION_SECRET to .env.local at any time.${RESET}"
fi fi
draw_footer draw_footer
@@ -650,7 +724,8 @@ screen_logging_config() {
3) CFG_LOG_LEVEL="debug" ;; 3) CFG_LOG_LEVEL="debug" ;;
esac esac
tip "Use 'info' for production. Switch to 'debug' temporarily when troubleshooting." echo ""
note "Use 'info' for production. Switch to 'debug' temporarily when troubleshooting."
draw_footer draw_footer
read -r read -r
@@ -666,8 +741,8 @@ screen_login_customization() {
echo -e " All fields are ${BOLD}optional${RESET}. Press Enter to skip any field." echo -e " All fields are ${BOLD}optional${RESET}. Press Enter to skip any field."
echo "" echo ""
echo -e " ${DIM}Shown on the login page footer. Example: Acme Corp${RESET}"
prompt_value "Company / organization name" "$CFG_LOGIN_COMPANY_NAME" "CFG_LOGIN_COMPANY_NAME" prompt_value "Company / organization name" "$CFG_LOGIN_COMPANY_NAME" "CFG_LOGIN_COMPANY_NAME"
tip "Shown on the login page footer. Example: Acme Corp"
echo "" echo ""
prompt_value "Website URL" "$CFG_LOGIN_WEBSITE_URL" "CFG_LOGIN_WEBSITE_URL" prompt_value "Website URL" "$CFG_LOGIN_WEBSITE_URL" "CFG_LOGIN_WEBSITE_URL"
echo "" echo ""
@@ -712,15 +787,15 @@ screen_deployment() {
case "$CFG_DEPLOY_METHOD" in case "$CFG_DEPLOY_METHOD" in
"node") "node")
tip "Good for development or when you want full control over the build." note "Good for development or when you want full control over the build."
echo -e " ${DIM}Requires: Node.js 18+, npm${RESET}" echo -e " ${DIM}Requires: Node.js 18+, npm${RESET}"
;; ;;
"docker") "docker")
tip "Easiest option for production. No build tools needed on the host." note "Easiest option for production. No build tools needed on the host."
echo -e " ${DIM}Requires: Docker${RESET}" echo -e " ${DIM}Requires: Docker${RESET}"
;; ;;
"compose") "compose")
tip "Best for production. Easy to manage with 'docker compose up/down'." note "Best for production. Easy to manage with 'docker compose up/down'."
echo -e " ${DIM}Requires: Docker + Docker Compose v2${RESET}" echo -e " ${DIM}Requires: Docker + Docker Compose v2${RESET}"
;; ;;
esac esac