first commit

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-03 16:52:28 +01:00
commit 2eb8aac027
91 files changed
+20139

No files matched your search

Executable
+239
View File
@@ -0,0 +1,239 @@
#!/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 -- "$@"