#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT" ENV_FILE="${POSTFIXADMIN_RS_ENV_FILE:-$ROOT/.env}" if [ -f "$ENV_FILE" ]; then set -a # shellcheck disable=SC1090 . "$ENV_FILE" set +a fi if ! command -v cargo >/dev/null 2>&1; then echo "error: Rust/Cargo is required" >&2 exit 1 fi DEV_DB_MODE="${POSTFIXADMIN_RS_DEV_DB_MODE:-mock}" DEV_DB="$ROOT/.dev/postfixadmin-rs.sqlite" TUNNEL_PID="" mkdir -p "$ROOT/.dev" cleanup() { if [ -n "$TUNNEL_PID" ] && kill -0 "$TUNNEL_PID" 2>/dev/null; then kill "$TUNNEL_PID" 2>/dev/null || true wait "$TUNNEL_PID" 2>/dev/null || true fi } trap cleanup EXIT INT TERM wait_for_port() { local port="$1" local i for i in $(seq 1 80); do if ! kill -0 "$TUNNEL_PID" 2>/dev/null; then echo "error: SSH tunnel exited before local port became ready" >&2 wait "$TUNNEL_PID" || true exit 1 fi if command -v nc >/dev/null 2>&1; then if nc -z 127.0.0.1 "$port" >/dev/null 2>&1; then return 0; fi elif (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then exec 3>&- 3<&- return 0 fi sleep 0.1 done echo "error: timed out waiting for SSH tunnel on 127.0.0.1:$port" >&2 exit 1 } if [ "${POSTFIXADMIN_RS_DEV_RESET:-0}" = "1" ]; then rm -f "$DEV_DB" "$DEV_DB-shm" "$DEV_DB-wal" rm -rf "$ROOT/.dev/rspamd" "$ROOT/.dev/rspamd-remote-public" echo "Development mock DB and Rspamd DKIM state reset." fi case "${RUST_LOG:-}" in ""|debug) export RUST_LOG="warn,postfixadmin_rs=debug,tower_http=info" ;; trace) export RUST_LOG="warn,postfixadmin_rs=trace,tower_http=debug" ;; info) export RUST_LOG="warn,postfixadmin_rs=info,tower_http=info" ;; *) : ;; esac case "$DEV_DB_MODE" in mock) export POSTFIXADMIN_RS_CONFIG="${POSTFIXADMIN_RS_CONFIG:-$ROOT/config.dev.toml}" ;; ssh) if ! command -v ssh >/dev/null 2>&1; then echo "error: ssh client is required for POSTFIXADMIN_RS_DEV_DB_MODE=ssh" >&2 exit 1 fi : "${POSTFIXADMIN_RS_SSH_HOST:?set POSTFIXADMIN_RS_SSH_HOST}" SSH_USER="${POSTFIXADMIN_RS_SSH_USER:-$USER}" SSH_PORT="${POSTFIXADMIN_RS_SSH_PORT:-22}" DB_BACKEND="${POSTFIXADMIN_RS_DEV_DB_BACKEND:-mysql}" case "$DB_BACKEND" in mysql|mysqli) DEFAULT_REMOTE_PORT=3306; DEFAULT_LOCAL_PORT=13306 ;; pgsql|postgres) DEFAULT_REMOTE_PORT=5432; DEFAULT_LOCAL_PORT=15432 ;; *) echo "error: POSTFIXADMIN_RS_DEV_DB_BACKEND must be mysql, mysqli, pgsql or postgres" >&2; exit 1 ;; esac REMOTE_DB_HOST="${POSTFIXADMIN_RS_DEV_DB_HOST:-127.0.0.1}" REMOTE_DB_PORT="${POSTFIXADMIN_RS_DEV_DB_PORT:-$DEFAULT_REMOTE_PORT}" LOCAL_DB_PORT="${POSTFIXADMIN_RS_SSH_LOCAL_PORT:-$DEFAULT_LOCAL_PORT}" DB_USER="${POSTFIXADMIN_RS_DEV_DB_USER:-postfix}" DB_NAME="${POSTFIXADMIN_RS_DEV_DB_NAME:-postfix}" DB_PASSWORD="${POSTFIXADMIN_RS_DEV_DB_PASSWORD:-${POSTFIXADMIN_RS__DATABASE__PASSWORD:-}}" DEV_SIEVE_ENABLED="${POSTFIXADMIN_RS_DEV_SIEVE_ENABLED:-${POSTFIXADMIN_RS_DEV_SIEVE_TUNNEL:-true}}" SIEVE_REMOTE_HOST="${POSTFIXADMIN_RS_DEV_SIEVE_REMOTE_HOST:-127.0.0.1}" SIEVE_REMOTE_PORT="${POSTFIXADMIN_RS_DEV_SIEVE_REMOTE_PORT:-4190}" SIEVE_LOCAL_PORT="${POSTFIXADMIN_RS_DEV_SIEVE_LOCAL_PORT:-14190}" DEV_RSPAMD_PUBLIC_SYNC="${POSTFIXADMIN_RS_DEV_RSPAMD_PUBLIC_SYNC:-true}" RSPAMD_SYNC_OK=0 RSPAMD_REMOTE_KEY_DIR="${POSTFIXADMIN_RS_DEV_RSPAMD_KEY_DIR:-/var/lib/rspamd/dkim}" RSPAMD_REMOTE_SIGNING_CONFIG="${POSTFIXADMIN_RS_DEV_RSPAMD_SIGNING_CONFIG:-/etc/rspamd/local.d/dkim_signing.conf}" RSPAMD_REMOTE_SELECTOR_MAP="${POSTFIXADMIN_RS_DEV_RSPAMD_SELECTOR_MAP:-/var/lib/rspamd/dkim/postfixadmin-rs-selectors.map}" RSPAMD_REMOTE_PATH_MAP="${POSTFIXADMIN_RS_DEV_RSPAMD_PATH_MAP:-/var/lib/rspamd/dkim/postfixadmin-rs-paths.map}" RSPAMD_DEFAULT_SELECTOR="${POSTFIXADMIN_RS_DEV_RSPAMD_DEFAULT_SELECTOR:-mail}" RSPAMD_PUBLIC_DIR="$ROOT/.dev/rspamd-remote-public" export POSTFIXADMIN_RS_CONFIG="${POSTFIXADMIN_RS_CONFIG:-${POSTFIXADMIN_RS_SSH_CONFIG:-$ROOT/config.from-user.toml}}" export POSTFIXADMIN_RS__ENVIRONMENT=development export POSTFIXADMIN_RS__DATABASE__BACKEND="$DB_BACKEND" export POSTFIXADMIN_RS__DATABASE__HOST=127.0.0.1 export POSTFIXADMIN_RS__DATABASE__PORT="$LOCAL_DB_PORT" export POSTFIXADMIN_RS__DATABASE__USER="$DB_USER" export POSTFIXADMIN_RS__DATABASE__NAME="$DB_NAME" export POSTFIXADMIN_RS__DATABASE__PASSWORD="$DB_PASSWORD" export POSTFIXADMIN_RS__DATABASE__DEV_MOCK=false export POSTFIXADMIN_RS__DATABASE__READ_ONLY=true export POSTFIXADMIN_RS__SECURITY__COOKIE_SECURE=false # Rspamd integration on the workstation is read from a public-only SSH cache below. # No private key bytes are copied from the mail server. export POSTFIXADMIN_RS__RSPAMD_DKIM__ENABLED=false export POSTFIXADMIN_RS__SIEVE__ENABLED="$DEV_SIEVE_ENABLED" if [ "$DEV_SIEVE_ENABLED" = "true" ] || [ "$DEV_SIEVE_ENABLED" = "1" ]; then export POSTFIXADMIN_RS__SIEVE__HOST=127.0.0.1 export POSTFIXADMIN_RS__SIEVE__PORT="$SIEVE_LOCAL_PORT" export POSTFIXADMIN_RS__SIEVE__TLS_MODE=none export POSTFIXADMIN_RS__SIEVE__ALLOW_INSECURE_AUTH=true export POSTFIXADMIN_RS__SIEVE__READ_ONLY=true if [ "${POSTFIXADMIN_RS_DEV_SIEVE_WRITES:-0}" = "1" ]; then export POSTFIXADMIN_RS__SIEVE__READ_ONLY=false echo "WARNING: production ManageSieve writes are ENABLED for this development session." >&2 fi fi if [ "${POSTFIXADMIN_RS_DEV_DB_WRITES:-0}" = "1" ]; then export POSTFIXADMIN_RS__DATABASE__READ_ONLY=false echo "WARNING: production DB writes are ENABLED for this development session." >&2 fi SSH_COMMON_ARGS=(-p "$SSH_PORT" -o BatchMode=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3) if [ -n "${POSTFIXADMIN_RS_SSH_IDENTITY:-}" ]; then SSH_COMMON_ARGS+=(-i "$POSTFIXADMIN_RS_SSH_IDENTITY") fi if [ -n "${POSTFIXADMIN_RS_SSH_KNOWN_HOSTS:-}" ]; then SSH_COMMON_ARGS+=(-o "UserKnownHostsFile=${POSTFIXADMIN_RS_SSH_KNOWN_HOSTS}") fi SSH_ARGS=(-N -T -L "127.0.0.1:${LOCAL_DB_PORT}:${REMOTE_DB_HOST}:${REMOTE_DB_PORT}" -o ExitOnForwardFailure=yes "${SSH_COMMON_ARGS[@]}") if [ "$DEV_SIEVE_ENABLED" = "true" ] || [ "$DEV_SIEVE_ENABLED" = "1" ]; then SSH_ARGS+=(-L "127.0.0.1:${SIEVE_LOCAL_PORT}:${SIEVE_REMOTE_HOST}:${SIEVE_REMOTE_PORT}") fi ssh "${SSH_ARGS[@]}" "${SSH_USER}@${POSTFIXADMIN_RS_SSH_HOST}" & TUNNEL_PID=$! wait_for_port "$LOCAL_DB_PORT" if [ "$DEV_SIEVE_ENABLED" = "true" ] || [ "$DEV_SIEVE_ENABLED" = "1" ]; then wait_for_port "$SIEVE_LOCAL_PORT" fi if [ "$DEV_RSPAMD_PUBLIC_SYNC" = "true" ] || [ "$DEV_RSPAMD_PUBLIC_SYNC" = "1" ]; then if ! command -v python3 >/dev/null 2>&1; then echo "warning: python3 is required for public-only Rspamd DKIM sync; disabling it for this session" >&2 DEV_RSPAMD_PUBLIC_SYNC=false fi fi if [ "$DEV_RSPAMD_PUBLIC_SYNC" = "true" ] || [ "$DEV_RSPAMD_PUBLIC_SYNC" = "1" ]; then rm -rf "$RSPAMD_PUBLIC_DIR" SYNC_ARGS=( --ssh "$(command -v ssh)" --host "$POSTFIXADMIN_RS_SSH_HOST" --user "$SSH_USER" --port "$SSH_PORT" --signing-config "$RSPAMD_REMOTE_SIGNING_CONFIG" --selector-map "$RSPAMD_REMOTE_SELECTOR_MAP" --path-map "$RSPAMD_REMOTE_PATH_MAP" --key-dir "$RSPAMD_REMOTE_KEY_DIR" --default-selector "$RSPAMD_DEFAULT_SELECTOR" --output-dir "$RSPAMD_PUBLIC_DIR" ) if [ -n "${POSTFIXADMIN_RS_SSH_IDENTITY:-}" ]; then SYNC_ARGS+=(--identity "$POSTFIXADMIN_RS_SSH_IDENTITY") fi if [ -n "${POSTFIXADMIN_RS_SSH_KNOWN_HOSTS:-}" ]; then SYNC_ARGS+=(--known-hosts "$POSTFIXADMIN_RS_SSH_KNOWN_HOSTS") fi if python3 "$ROOT/scripts/sync-rspamd-dkim-readonly.py" "${SYNC_ARGS[@]}"; then export POSTFIXADMIN_RS__RSPAMD_DKIM__ENABLED=true export POSTFIXADMIN_RS__RSPAMD_DKIM__KEY_DIR="$RSPAMD_PUBLIC_DIR/keys" export POSTFIXADMIN_RS__RSPAMD_DKIM__SIGNING_CONFIG="$RSPAMD_PUBLIC_DIR/dkim_signing.conf" export POSTFIXADMIN_RS__RSPAMD_DKIM__SELECTOR_MAP="$RSPAMD_PUBLIC_DIR/selectors.map" export POSTFIXADMIN_RS__RSPAMD_DKIM__PATH_MAP="$RSPAMD_PUBLIC_DIR/paths.map" export POSTFIXADMIN_RS__RSPAMD_DKIM__DEFAULT_SELECTOR="$RSPAMD_DEFAULT_SELECTOR" export POSTFIXADMIN_RS__RSPAMD_DKIM__RSPAMADM=/bin/false RSPAMD_SYNC_OK=1 else echo "warning: Rspamd DKIM public sync failed; Rspamd DKIM view is disabled for this dev session" >&2 export POSTFIXADMIN_RS__RSPAMD_DKIM__ENABLED=false fi elif [ "${POSTFIXADMIN_RS_DEV_RSPAMD_ENABLED:-false}" = "true" ] || [ "${POSTFIXADMIN_RS_DEV_RSPAMD_ENABLED:-false}" = "1" ]; then # Explicit escape hatch for testing a Rspamd installation local to the workstation. export POSTFIXADMIN_RS__RSPAMD_DKIM__ENABLED=true fi ;; *) echo "error: POSTFIXADMIN_RS_DEV_DB_MODE must be mock or ssh" >&2 exit 1 ;; esac [ -f "$POSTFIXADMIN_RS_CONFIG" ] || { echo "error: config not found: $POSTFIXADMIN_RS_CONFIG" >&2 exit 1 } echo "PostfixAdmin RS 0.0.1-dev: http://127.0.0.1:8080" echo "Config: $POSTFIXADMIN_RS_CONFIG" if [ "$DEV_DB_MODE" = "mock" ]; then echo "DB: SQLite mock ($DEV_DB)" echo "Admin: admin@example.test / admin123" echo "D-admin: domain-admin@example.test / domain123" echo "User: user@example.test / user123" echo "Rspamd: DEV mock (.dev/rspamd)" echo "Reset: POSTFIXADMIN_RS_DEV_RESET=1 bash dev.sh" else echo "DB: production through SSH tunnel 127.0.0.1:${LOCAL_DB_PORT} -> ${REMOTE_DB_HOST}:${REMOTE_DB_PORT}" echo "SSH: ${SSH_USER}@${POSTFIXADMIN_RS_SSH_HOST}:${SSH_PORT}" if [ "${DEV_SIEVE_ENABLED:-false}" = "true" ] || [ "${DEV_SIEVE_ENABLED:-false}" = "1" ]; then echo "Sieve: 127.0.0.1:${SIEVE_LOCAL_PORT} -> ${SIEVE_REMOTE_HOST}:${SIEVE_REMOTE_PORT} ($([ "${POSTFIXADMIN_RS_DEV_SIEVE_WRITES:-0}" = "1" ] && echo READ/WRITE || echo READ-ONLY))" fi if [ "${RSPAMD_SYNC_OK:-0}" = "1" ]; then echo "DKIM: Rspamd public DNS records synced over SSH (private keys stay remote)" fi if [ "${POSTFIXADMIN_RS_DEV_DB_WRITES:-0}" = "1" ]; then echo "Access: READ/WRITE (explicitly enabled)" else echo "Access: READ-ONLY (default)" fi fi [ -f "$ENV_FILE" ] && echo "Env: $ENV_FILE" echo cargo run --quiet --bin postfixadmin-rs -- --check-config cargo run --bin postfixadmin-rs -- "$@"