#!/bin/sh
# pdmsc-live-setup -- configure the PDMSC live rspamd module (live.pdmsc.com).
# Copyright (c) 2026 Michelle I. Sullivan (PDMSC). SPDX-License-Identifier: BSD-2-Clause (see COPYRIGHT).
#
# Installs your API key, writes the rspamd configuration for the module, checks it with
# rspamadm configtest and (optionally) reloads rspamd. Everything it writes is either a
# file of its own or a marked block it can remove again; it never edits other settings.
#
#   pdmsc-live-setup --key pdl_XXXX            set up (external_services mode, mirror scores)
#   pdmsc-live-setup --key -                   read the key from standard input
#   pdmsc-live-setup                           prompt for the key
#   pdmsc-live-setup --test                    send a sample message to live.pdmsc.com
#   pdmsc-live-setup --status                  show what is configured
#   pdmsc-live-setup --remove-config           take the configuration out again
#
# Options:
#   --mode external|standalone   external = three external_services rules (default)
#   --engines pdmsc,s1,s2        which engines to enable (default: all three)
#   --scored                     use the production scores (default: mirror mode, all 0)
#   --reload                     reload rspamd once the configuration tests clean
#   --confdir DIR                rspamd config directory (default: auto-detect)
#   --group NAME                 group that must be able to read the key (default: auto)
#   --purge                      with --remove-config: also delete the key file
#   --no-chown                   do not set ownership (testing / non-root)
#   -h, --help
set -u

PROG=pdmsc-live-setup
VERSION="1.0.0"
BEGIN_MARK="# BEGIN pdmsc-live (managed by pdmsc-live-setup; do not edit between the markers)"
END_MARK="# END pdmsc-live"
URL_BASE="https://live.pdmsc.com"

die()  { printf '%s: %s\n' "$PROG" "$*" >&2; exit 1; }
warn() { printf '%s: warning: %s\n' "$PROG" "$*" >&2; }
info() { printf '%s\n' "$*"; }

usage() { sed -n '2,/^set -u/p' "$0" | sed '$d' | sed 's/^# \{0,1\}//'; }

ACTION=setup MODE=external ENGINES="pdmsc,s1,s2" SCORED=0 RELOAD=0 CONFDIR="" GROUP=""
KEY="" PURGE=0 CHOWN=1
while [ $# -gt 0 ]; do
  case "$1" in
    --key)           [ $# -ge 2 ] || die "--key needs a value (or - for stdin)"; KEY="$2"; shift ;;
    --mode)          [ $# -ge 2 ] || die "--mode needs a value"; MODE="$2"; shift ;;
    --engines)       [ $# -ge 2 ] || die "--engines needs a value"; ENGINES="$2"; shift ;;
    --scored)        SCORED=1 ;;
    --reload)        RELOAD=1 ;;
    --confdir)       [ $# -ge 2 ] || die "--confdir needs a value"; CONFDIR="$2"; shift ;;
    --group)         [ $# -ge 2 ] || die "--group needs a value"; GROUP="$2"; shift ;;
    --purge)         PURGE=1 ;;
    --no-chown)      CHOWN=0 ;;
    --test)          ACTION=test ;;
    --status)        ACTION=status ;;
    --remove-config) ACTION=remove ;;
    --version)       echo "$PROG $VERSION"; exit 0 ;;
    -h|--help)       usage; exit 0 ;;
    *)               die "unknown option: $1 (try --help)" ;;
  esac
  shift
done

case "$MODE" in external|standalone) ;; *) die "--mode must be external or standalone" ;; esac
ENGINES=$(printf '%s' "$ENGINES" | tr 'A-Z' 'a-z' | tr -d ' ')
for e in $(printf '%s' "$ENGINES" | tr ',' ' '); do
  case "$e" in pdmsc|s1|s2) ;; *) die "unknown engine '$e' (use pdmsc, s1, s2)" ;; esac
done
[ -n "$ENGINES" ] || die "--engines is empty"
has_engine() { case ",$ENGINES," in *",$1,"*) return 0 ;; esac; return 1; }

# ------------------------------------------------------------------ locate rspamd's config
if [ -z "$CONFDIR" ]; then
  for d in /etc/rspamd /usr/local/etc/rspamd; do
    if [ -f "$d/rspamd.conf" ]; then CONFDIR="$d"; break; fi
  done
fi
[ -n "$CONFDIR" ] && [ -d "$CONFDIR" ] || die "cannot find rspamd's config directory; use --confdir DIR"
MODULE="$CONFDIR/lua.local.d/pdmsc_live.lua"
KEYFILE="$CONFDIR/pdmsc_live.key"
COMMON="$CONFDIR/local.d/pdmsc_live_common.inc"
EXTRA="$CONFDIR/local.d/pdmsc_live_extra.inc"
ES_CONF="$CONFDIR/local.d/external_services.conf"
LOCAL_CONF="$CONFDIR/rspamd.conf.local"

# ------------------------------------------------------------------ helpers
have_block() { [ -f "$1" ] && grep -q "^$BEGIN_MARK\$" "$1" 2>/dev/null; }

# remove the marked block from a file (keeps the file's owner and mode)
strip_block() {
  [ -f "$1" ] || return 0
  have_block "$1" || return 0
  tmp=$(mktemp "${TMPDIR:-/tmp}/pdmsc-live.XXXXXX") || die "mktemp failed"
  awk -v b="$BEGIN_MARK" -v e="$END_MARK" '
    $0 == b { skip = 1; next }
    skip && $0 == e { skip = 0; next }
    !skip { print }' "$1" > "$tmp" || { rm -f "$tmp"; die "cannot rewrite $1"; }
  cat "$tmp" > "$1"; rm -f "$tmp"
  # a file we created that is now empty (and has no pristine backup) is removed again
  if ! grep -q '[^[:space:]]' "$1" 2>/dev/null && [ ! -f "$1.pdmsc-live.bak" ]; then rm -f "$1"; fi
}

SNAPDIR=""
snapshot() {         # remember the exact current state of files we may change
  SNAPDIR=$(mktemp -d "${TMPDIR:-/tmp}/pdmsc-live-snap.XXXXXX") || die "mktemp failed"
  n=0
  for f in "$@"; do
    n=$((n + 1))
    if [ -f "$f" ]; then cp -p "$f" "$SNAPDIR/$n.file" && printf '%s\n' "$f" > "$SNAPDIR/$n.path"
    else printf '%s\n' "$f" > "$SNAPDIR/$n.absent"; fi
  done
}
restore_snapshot() {
  for p in "$SNAPDIR"/*.path; do
    [ -f "$p" ] || continue
    cat "${p%.path}.file" > "$(cat "$p")"
  done
  for a in "$SNAPDIR"/*.absent; do
    [ -f "$a" ] || continue
    rm -f "$(cat "$a")"
  done
}
backup_once() {      # keep one pristine copy of a file we are about to change
  [ -f "$1" ] || return 0
  if [ ! -f "$1.pdmsc-live.bak" ] && ! have_block "$1"; then
    cp -p "$1" "$1.pdmsc-live.bak" || die "cannot back up $1"
  fi
}

append_block() {     # $1 file, content on stdin
  mkdir -p "$(dirname "$1")" || die "cannot create $(dirname "$1")"
  [ -f "$1" ] || : > "$1"
  { printf '\n%s\n' "$BEGIN_MARK"; cat; printf '%s\n' "$END_MARK"; } >> "$1"
}

pick_group() {
  [ -n "$GROUP" ] && return 0
  for g in _rspamd rspamd; do
    if { command -v getent >/dev/null 2>&1 && getent group "$g" >/dev/null 2>&1; } ||
       grep -q "^$g:" /etc/group 2>/dev/null; then GROUP="$g"; return 0; fi
  done
  return 1
}

run_configtest() {
  command -v rspamadm >/dev/null 2>&1 || { warn "rspamadm not found; skipping the configuration test"; return 0; }
  if [ -f "$CONFDIR/rspamd.conf" ]; then rspamadm configtest -c "$CONFDIR/rspamd.conf" 2>&1
  else rspamadm configtest 2>&1; fi
}

do_reload() {
  if command -v rspamadm >/dev/null 2>&1 && rspamadm control reload >/dev/null 2>&1; then return 0; fi
  if command -v systemctl >/dev/null 2>&1 && systemctl reload rspamd >/dev/null 2>&1; then return 0; fi
  if command -v service   >/dev/null 2>&1 && service rspamd reload   >/dev/null 2>&1; then return 0; fi
  return 1
}

need_root() {
  [ "$CHOWN" -eq 0 ] && return 0
  [ "$(id -u)" -eq 0 ] || die "run as root (sudo $PROG ...)"
}

# ------------------------------------------------------------------ actions
do_status() {
  info "$PROG $VERSION"
  info "rspamd config dir : $CONFDIR"
  if [ -f "$MODULE" ]; then info "module            : installed ($MODULE)"; else info "module            : NOT installed"; fi
  if [ -f "$KEYFILE" ]; then
    info "api key file      : present ($KEYFILE, $(ls -l "$KEYFILE" | awk '{print $1, $3":"$4}'))"
  else info "api key file      : not set"; fi
  if have_block "$ES_CONF"; then
    info "external_services : configured ($(grep -o '^pdmsc_live_[a-z0-9]*' "$ES_CONF" | tr '\n' ' '))"
  else info "external_services : not configured"; fi
  if have_block "$LOCAL_CONF"; then info "standalone section: configured ($LOCAL_CONF)"; else info "standalone section: not configured"; fi
  if [ -f "$COMMON" ]; then
    if grep -q '^scored = true;' "$COMMON"; then info "scoring           : production scores (scored = true)"
    else info "scoring           : mirror mode (all symbols score 0)"; fi
  fi
  [ -f "$EXTRA" ] && info "your overrides    : $EXTRA"
  return 0
}

do_remove() {
  need_root
  strip_block "$ES_CONF"; strip_block "$LOCAL_CONF"
  rm -f "$COMMON"
  [ "$PURGE" -eq 1 ] && rm -f "$KEYFILE"
  info "PDMSC live configuration removed from $CONFDIR."
  [ "$PURGE" -eq 1 ] || info "The key file ($KEYFILE) was kept; add --purge to delete it too."
  info "Your own overrides ($EXTRA) were left alone. Reload rspamd to apply."
  if [ "$RELOAD" -eq 1 ]; then do_reload && info "rspamd reloaded." || warn "could not reload rspamd; reload it yourself"; fi
}

do_test() {
  command -v curl >/dev/null 2>&1 || die "curl is needed for --test"
  [ -f "$KEYFILE" ] || die "no key set up yet; run: $PROG --key pdl_..."
  k=$(head -n 1 "$KEYFILE" | tr -d '[:space:]')
  msg=$(mktemp "${TMPDIR:-/tmp}/pdmsc-live-test.XXXXXX") || die "mktemp failed"
  printf 'From: Test Sender <test@example.org>\r\nTo: you@example.net\r\nSubject: PDMSC live test message\r\nDate: Thu, 29 Oct 2026 09:00:00 +1000\r\nMessage-ID: <pdmsc-live-setup-test@example.org>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nHello,\r\n\r\nThis is a test message sent by pdmsc-live-setup --test to check that the PDMSC live\r\nservice accepts your key. It contains nothing private.\r\n\r\nRegards\r\n' > "$msg"
  out=$(mktemp "${TMPDIR:-/tmp}/pdmsc-live-out.XXXXXX") || die "mktemp failed"
  # shellcheck disable=SC2086
  code=$(curl -sS -m 20 -o "$out" -w '%{http_code}' ${PDMSC_LIVE_CURL_OPTS:-} -X POST \
         -H "Authorization: Bearer $k" -H 'Content-Type: message/rfc822' \
         --data-binary "@$msg" "${PDMSC_LIVE_URL:-$URL_BASE/v1/scan}" 2>&1) || { rm -f "$msg" "$out"; die "could not reach ${PDMSC_LIVE_URL:-$URL_BASE}: $code"; }
  info "HTTP $code from ${PDMSC_LIVE_URL:-$URL_BASE/v1/scan}"
  case "$code" in
    200) info "OK: your key and licence are accepted. Reply (trimmed):"; head -c 600 "$out"; echo ;;
    401) info "The key was not accepted. Check it is copied exactly, and has not been revoked." ;;
    403) info "The key is valid but the licence is not: $(head -c 200 "$out")"
         info "(expired, suspended, revoked, or the key is not bound to an active licence)." ;;
    429) info "Rate limited: wait a few seconds and try again." ;;
    503) info "Service busy or all engines failed: $(head -c 200 "$out")" ;;
    *)   info "Unexpected reply: $(head -c 200 "$out")" ;;
  esac
  rm -f "$msg" "$out"
  [ "$code" = "200" ]
}

do_setup() {
  need_root
  [ -f "$MODULE" ] || die "the module is not installed at $MODULE; install the package (or run install.sh) first"
  {
    echo "NOTE: this is a live TEST service for individuals and for evaluation and testing only."
    echo "      Once configured, every message rspamd scans is SENT OFF THIS SYSTEM, in full, to"
    echo "      live.pdmsc.com (operated by PDMSC). Do not use it for mail that must not leave your"
    echo "      network or that you have no right to share. Terms: TERMS.txt in the documentation"
    echo "      directory (e.g. /usr/share/doc/rspamd-pdmsc-live). Undo: $PROG --remove-config --reload"
  } >&2

  # --- the key (never echoed, never put on a command line we run)
  if [ "$KEY" = "-" ]; then IFS= read -r KEY || true
  elif [ -z "$KEY" ]; then
    if [ -f "$KEYFILE" ] && [ -t 0 ]; then
      info "A key is already installed at $KEYFILE; keeping it. (Pass --key to replace it.)"
    elif [ -t 0 ]; then
      printf 'PDMSC API key (pdl_...): ' >&2; stty -echo 2>/dev/null; IFS= read -r KEY || true; stty echo 2>/dev/null; printf '\n' >&2
    elif [ ! -f "$KEYFILE" ]; then die "no key given: use --key pdl_... or --key - (stdin)"
    fi
  fi
  KEY=$(printf '%s' "$KEY" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')   # trim the ends only (stray newline / CR)
  if [ -n "$KEY" ]; then
    case "$KEY" in pdl_*) ;; *) die "that does not look like a PDMSC API key (it should start with pdl_)" ;; esac
    printf '%s' "$KEY" | grep -Eq '^pdl_[A-Za-z0-9_-]{16,190}$' || die "the key has unexpected characters or length; copy it again from the portal"
    if [ "$CHOWN" -eq 1 ]; then pick_group || die "cannot find the rspamd group (_rspamd / rspamd); use --group NAME"; fi
    old_umask=$(umask); umask 077
    printf '%s\n' "$KEY" > "$KEYFILE.new" || die "cannot write $KEYFILE.new"
    umask "$old_umask"
    if [ "$CHOWN" -eq 1 ]; then chown "root:$GROUP" "$KEYFILE.new" || die "chown failed"; chmod 0640 "$KEYFILE.new"; else chmod 0600 "$KEYFILE.new"; fi
    mv "$KEYFILE.new" "$KEYFILE" || die "cannot install $KEYFILE"
    KEY=""
  fi
  [ -f "$KEYFILE" ] || die "no key file at $KEYFILE"

  snapshot "$COMMON" "$ES_CONF" "$LOCAL_CONF"
  # --- shared settings file (regenerated; your own overrides live in pdmsc_live_extra.inc)
  mkdir -p "$CONFDIR/local.d" || die "cannot create $CONFDIR/local.d"
  { echo "# Generated by $PROG $VERSION -- re-running the tool rewrites this file."
    echo "# Put your own scoring overrides in $EXTRA instead."
    echo "servers      = \"live.pdmsc.com\";"
    echo "api_key_file = \"$KEYFILE\";"
    if [ "$SCORED" -eq 1 ]; then echo "scored = true;"; else echo "# scored = true;      (mirror mode: everything scores 0)"; fi
    echo ".include(try=true) \"$EXTRA\""
  } > "$COMMON"
  chmod 0644 "$COMMON"

  # --- the rspamd configuration (marked blocks only)
  if [ "$MODE" = "external" ]; then
    backup_once "$ES_CONF"; strip_block "$ES_CONF"; strip_block "$LOCAL_CONF"
    {
      has_engine pdmsc && printf 'pdmsc_live_pdmsc {\n    type = "pdmsc_live";\n    .include "%s"\n}\n' "$COMMON"
      has_engine s1    && printf 'pdmsc_live_s1 {\n    type = "pdmsc_live_s1";\n    .include "%s"\n}\n' "$COMMON"
      has_engine s2    && printf 'pdmsc_live_s2 {\n    type = "pdmsc_live_s2";\n    .include "%s"\n}\n' "$COMMON"
    } | append_block "$ES_CONF"
  else
    backup_once "$LOCAL_CONF"; strip_block "$LOCAL_CONF"; strip_block "$ES_CONF"
    {
      printf 'pdmsc_live {\n    servers = "live.pdmsc.com";\n    api_key_file = "%s";\n' "$KEYFILE"
      [ "$SCORED" -eq 1 ] && printf '    scored = true;\n'
      has_engine pdmsc || printf '    pdmsc { enabled = false; }\n'
      has_engine s1    || printf '    s1 { enabled = false; }\n'
      has_engine s2    || printf '    s2 { enabled = false; }\n'
      printf '    .include(try=true) "%s"\n}\n' "$EXTRA"
    } | append_block "$LOCAL_CONF"
  fi

  # --- test it; put things back if rspamd rejects the result
  if ! out=$(run_configtest); then
    restore_snapshot; rm -rf "$SNAPDIR"
    printf '%s\n' "$out" >&2
    die "rspamd rejected the configuration; your previous configuration files were restored unchanged"
  fi
  rm -rf "$SNAPDIR"
  info "PDMSC live is configured ($MODE mode, engines: $ENGINES, $( [ "$SCORED" -eq 1 ] && echo 'production scores' || echo 'mirror mode: all scores 0'))."
  if [ "$RELOAD" -eq 1 ]; then
    do_reload && info "rspamd reloaded." || warn "could not reload rspamd automatically; reload it yourself"
  else
    info "Reload rspamd to start using it (or re-run with --reload)."
  fi
  info "Check the service and your licence with:  $PROG --test"
}

case "$ACTION" in
  setup)  do_setup ;;
  test)   do_test ;;
  status) do_status ;;
  remove) do_remove ;;
esac
