#!/bin/sh set -eu # Print a comma-separated list of unique registrable root domains (eTLD+1) # for non-suspended cPanel users based on /etc/userdomains. # # Tries to use `psl` (libpsl) if available for correct public suffix handling. # Falls back to a heuristic for common multi-level TLDs if `psl` is unavailable. USERDOMAINS_PATH="/etc/userdomains" SUSPENDED_DIR="/var/cpanel/suspended" error() { printf "Error: %s\n" "$*" 1>&2 } have_psl() { command -v psl >/dev/null 2>&1 } get_root_domain() { # Derive registrable domain (eTLD+1) from a hostname # 1) Prefer libpsl via `psl --print` # 2) Fallback heuristic for common multi-level TLDs hostname="$1" # strip trailing dot if any case "$hostname" in *.) hostname=${hostname%\.} ;; esac if have_psl; then printed=$(psl --print "$hostname" 2>/dev/null | tr -d '\r') if [ -n "$printed" ]; then printf "%s\n" "$printed" return 0 fi fi # Fallback heuristic implemented in awk to avoid bashisms printf "%s\n" "$hostname" | awk ' BEGIN { FS = "." } { n = NF if (n >= 3) { lastTwo = $(n-1) "." $n if (lastTwo ~ /^(co\.uk|org\.uk|ac\.uk|gov\.uk|ltd\.uk|plc\.uk|me\.uk|co\.jp|ne\.jp|or\.jp|go\.jp|ac\.jp|ed\.jp|gr\.jp|com\.au|net\.au|org\.au|edu\.au|gov\.au|id\.au|asn\.au|com\.br|com\.cn|net\.cn|org\.cn|com\.sg|com\.my|co\.nz)$/) { print $(n-2) "." lastTwo next } } if (n >= 2) { print $(n-1) "." $n } else { print $0 } }' } main() { if [ ! -r "${USERDOMAINS_PATH}" ]; then error "Cannot read ${USERDOMAINS_PATH}" exit 1 fi # Read, normalize whitespace removal like original, skip lines starting with '*' # Format is typically: domain: user # For each non-suspended user, emit root domain; then unique-sort and join with commas sed -e 's/[[:space:]]*//g' -e '/^\*/d' "${USERDOMAINS_PATH}" \ | awk -F: 'NF>=2 { print $1 ":" $2 }' \ | while IFS=":" read -r domain username; do if [ ! -f "${SUSPENDED_DIR}/${username}" ]; then get_root_domain "${domain}" fi done \ | sort -u \ | paste -sd, - } main "$@"