Files
mailsystem/archive/selective-srs-2026/helpers/srs-common.sh
T

686 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
# ----------
# Common functions and settings for selective SRS management scripts
#
# This file is intended to be sourced by:
#
# activate-srs.sh
# deactivate-srs.sh
# install-update-srs.sh
#
# It must not make configuration changes by itself.
# ----------
# ----------
# Script paths
# ----------
SRS_COMMON_FILE="$(realpath "${BASH_SOURCE[0]}")"
SRS_HELPERS_DIR="$(dirname "$SRS_COMMON_FILE")"
SRS_SCRIPT_DIR="$(dirname "$SRS_HELPERS_DIR")"
SRS_CONF_FILE="${SRS_SCRIPT_DIR}/conf/srs.conf"
SRS_CONF_SAMPLE_FILE="${SRS_SCRIPT_DIR}/conf/srs.conf.sample"
# ----------
# System paths
# ----------
POSTFIX_CONF_DIR="/etc/postfix"
POSTFIX_MAIN_CF="${POSTFIX_CONF_DIR}/main.cf"
POSTFIX_MASTER_CF="${POSTFIX_CONF_DIR}/master.cf"
POSTFIX_SRS_DIR="/etc/postfix-srs"
POSTFIX_SRS_DELIVERY_DIR="${POSTFIX_SRS_DIR}/delivery"
SRS_STATE_FILE="${POSTFIX_SRS_DIR}/state.conf"
# SRS bounce alias ownership is tracked by a comment marker in main.cf;
# original version-1 state.conf is immutable and needs no unsafe migration.
SRS_ALIAS_HELPER="${SRS_HELPERS_DIR}/srs-virtual-alias.py"
POSTSRSD_CONF_DIR="/etc/postsrsd-srs"
POSTSRSD_CONF_FILE="${POSTSRSD_CONF_DIR}/postsrsd.conf"
POSTSRSD_SECRET_FILE="${POSTSRSD_CONF_DIR}/secret"
SRS_ALIAS_DECISION_SCRIPT="/usr/local/sbin/srs-alias-decision.py"
SRS_FORWARD_SCRIPT="/usr/local/sbin/srs-forward.py"
SRS_REVERSE_MAP_SCRIPT="/usr/local/sbin/srs-reverse-map.py"
SRS_REVERSE_POLICY_SCRIPT="/usr/local/sbin/srs-reverse-policy.py"
# ----------
# Services
# ----------
POSTFIX_SERVICE="postfix.service"
POSTSRSD_SERVICE="postsrsd-srs.service"
SRS_REVERSE_MAP_SERVICE="srs-reverse-map.service"
SRS_DELIVERY_SERVICE="postfix@srs-delivery.service"
POSTFIX_SRS_DROPIN_DIR="/etc/systemd/system/postfix.service.d"
POSTFIX_SRS_DROPIN_FILE="${POSTFIX_SRS_DROPIN_DIR}/srs.conf"
SRS_DELIVERY_DROPIN_DIR="/etc/systemd/system/postfix@srs-delivery.service.d"
SRS_DELIVERY_DROPIN_FILE="${SRS_DELIVERY_DROPIN_DIR}/srs.conf"
# ----------
# SRS network endpoints
# ----------
POSTSRSD_SOCKET_HOST="127.0.0.1"
POSTSRSD_SOCKET_PORT="10003"
SRS_REVERSE_SOCKET_HOST="127.0.0.1"
SRS_REVERSE_SOCKET_PORT="10004"
SRS_REINJECT_HOST="127.0.0.1"
SRS_REINJECT_PORT="10028"
SRS_REVERSE_HOST="127.0.0.1"
SRS_REVERSE_PORT="10029"
# ----------
# Managed block markers
# ----------
SRS_MAIN_CF_BEGIN_MARKER="# BEGIN OOPEN-SRS-MANAGED"
SRS_MAIN_CF_END_MARKER="# END OOPEN-SRS-MANAGED"
SRS_MASTER_CF_BEGIN_MARKER="# BEGIN OOPEN-SRS-MANAGED"
SRS_MASTER_CF_END_MARKER="# END OOPEN-SRS-MANAGED"
# ----------
# Runtime variables
# ----------
script_name="$(basename "$0")"
terminal=false
SRS_LOCK_DIR="/run/lock/oopen-srs.lock"
SRS_LOCK_OWNER_FILE="${SRS_LOCK_DIR}/pid"
log_file=""
backup_date="$(date +%Y-%m-%d-%H%M%S)"
SRS_LOCK_ACQUIRED=false
SRS_CONFIG_LOADED=false
SRS_STATE_LOADED=false
# ----------
# Base functions
# ----------
blank_line() {
if $terminal ; then
echo ""
fi
}
echononl() {
if $terminal ; then
echo -e -n " $*" 1>&2
fi
}
echo_ok() {
if $terminal ; then
echo -e "\033[85G[ \033[32mok\033[m ]"
fi
}
echo_failed() {
if $terminal ; then
echo -e "\033[85G[ \033[1;31mfailed\033[m ]"
fi
}
echo_skipped() {
if $terminal ; then
echo -e "\033[85G[ \033[33m\033[1mskipped\033[m ]"
fi
}
echo_warning() {
if $terminal ; then
echo -e "\033[85G[ \033[33m\033[1mwarn\033[m ]"
fi
}
echo_differs() {
if $terminal ; then
echo -e "\033[85G[ \033[33m\033[1mdiffers\033[m ]"
fi
}
echo_inactive() {
if $terminal ; then
echo -e "\033[85G[ \033[33m\033[1minactive\033[m ]"
fi
}
echo_absent() {
if $terminal ; then
echo -e "\033[85G[ \033[33m\033[1mabsent\033[m ]"
fi
}
info() {
if $terminal ; then
echo ""
echo -e " [ \033[32m\033[1mInfo\033[m ] $*"
echo ""
else
echo " [ Info ] $*"
fi
}
warn() {
echo ""
if $terminal ; then
echo -e " [ \033[33m\033[1mWarning\033[m ] $*"
else
echo " [ Warning ] $*"
fi
echo ""
}
error() {
echo ""
if $terminal ; then
echo -e " [ \033[31m\033[1mError\033[m ] $*"
else
echo " [ Error ] $*"
fi
echo ""
}
clean_up() {
local rc="${1:-0}"
if $SRS_LOCK_ACQUIRED && [[ -d "$SRS_LOCK_DIR" ]]; then
rm -rf "$SRS_LOCK_DIR"
SRS_LOCK_ACQUIRED=false
fi
[[ -n "$log_file" && -f "$log_file" ]] && rm -f "$log_file"
blank_line
exit "$rc"
}
fatal() {
echo ""
if $terminal ; then
echo -e " [ \033[31m\033[1mFatal\033[m ] $*"
echo ""
echo -e " \033[1mScript terminated\033[m.."
else
echo " [ Fatal ] $*"
echo ""
echo " Script terminated.."
fi
echo ""
clean_up 1
}
fatal_with_command_hint() {
local message="$1"
local hint="$2"
local command="$3"
echo ""
if $terminal ; then
echo -e " [ \033[31m\033[1mFatal\033[m ] ${message}"
echo ""
echo -e " \033[1mScript terminated\033[m.."
else
echo " [ Fatal ] ${message}"
echo ""
echo " Script terminated.."
fi
echo ""
if $terminal ; then
echo -e " [ \033[33m\033[1mWarning\033[m ] ${hint}"
else
echo " [ Warning ] ${hint}"
fi
echo ""
echo " ${command}"
echo ""
clean_up 1
}
fatal_with_queue_help() {
local message="$1"
local queue_config_dir="$2"
echo ""
if $terminal ; then
echo -e " [ \033[31m\033[1mFatal\033[m ] ${message}"
echo ""
echo -e " \033[1mScript terminated\033[m.."
else
echo " [ Fatal ] ${message}"
echo ""
echo " Script terminated.."
fi
echo ""
if $terminal ; then
echo -e " [ \033[33m\033[1mWarning\033[m ] To inspect the SRS delivery queue, run:"
else
echo " [ Warning ] To inspect the SRS delivery queue, run:"
fi
echo ""
echo " postqueue -c ${queue_config_dir} -p"
echo ""
if $terminal ; then
echo -e " [ \033[33m\033[1mWarning\033[m ] To inspect one queued message, replace QUEUE_ID and run:"
else
echo " [ Warning ] To inspect one queued message, replace QUEUE_ID and run:"
fi
echo ""
echo " postcat -c ${queue_config_dir} -q QUEUE_ID"
echo ""
if $terminal ; then
echo -e " [ \033[33m\033[1mWarning\033[m ] If you really want to delete ALL messages from the SRS delivery queue, run:"
else
echo " [ Warning ] If you really want to delete ALL messages from the SRS delivery queue, run:"
fi
echo ""
echo " postsuper -c ${queue_config_dir} -d ALL"
echo ""
clean_up 1
}
ask_yes_no() {
local question="$1"
local default_answer="${2:-no}"
local answer=""
local prompt="[yes/no]"
if [[ "$default_answer" = "yes" ]]; then
prompt="[yes/no, default: yes]"
elif [[ "$default_answer" = "no" ]]; then
prompt="[yes/no, default: no]"
fi
while true ; do
echononl "$question $prompt: "
read -r answer
answer="${answer,,}"
[[ -z "$answer" ]] && answer="$default_answer"
case "$answer" in
yes) return 0 ;;
no) return 1 ;;
*) warn "Wrong entry! Please enter 'yes' or 'no'." ;;
esac
done
}
# ----------
# Runtime handling
# ----------
init_srs_runtime() {
local script_path=""
local transcript_file=""
if [[ -t 1 ]]; then
terminal=true
else
script_path="$(realpath "$0" 2>/dev/null || printf '%s' "$0")"
transcript_file="/tmp/${script_name%.sh}.out"
echo ""
echo " [ Fatal ] Script requires an interactive terminal."
echo ""
echo " Script terminated.."
echo ""
echo " [ Warning ] Piping the script output, for example through 'tee',"
echo " removes the terminal from stdout."
echo ""
echo " To run the script interactively and record its complete"
echo " terminal session, use:"
echo ""
echo " script -q -e -c \"${script_path}\" \\"
echo " ${transcript_file}"
echo ""
clean_up 1
fi
if ! mkdir "$SRS_LOCK_DIR" 2>/dev/null ; then
if [[ -r "$SRS_LOCK_OWNER_FILE" ]]; then
fatal "Another SRS management process seems to be running (PID $(cat "$SRS_LOCK_OWNER_FILE"))."
else
fatal "Cannot create SRS lock directory '${SRS_LOCK_DIR}'."
fi
fi
SRS_LOCK_ACQUIRED=true
printf '%s\n' "$$" > "$SRS_LOCK_OWNER_FILE" || fatal "Cannot write SRS lock owner file '${SRS_LOCK_OWNER_FILE}'."
log_file="${SRS_LOCK_DIR}/${script_name%%.*}.log"
: > "$log_file" || fatal "Cannot create log file '${log_file}'."
}
handle_signal() {
trap - SIGHUP SIGINT SIGTERM
warn "Script interrupted by signal."
clean_up 1
}
# ----------
# Basic checks
# ----------
check_root() {
echononl "Check if script is running as root.."
if [[ "$(id -u)" -eq 0 ]]; then
echo_ok
else
echo_failed
fatal "This script must be run as root."
fi
}
check_required_commands() {
local cmd
for cmd in "$@" ; do
echononl "Check for command '${cmd}'.."
if command -v "$cmd" > /dev/null 2>&1 ; then
echo_ok
else
echo_failed
fatal "Required command '${cmd}' was not found."
fi
done
}
check_readable_file() {
local file="$1"
local description="${2:-file}"
echononl "Check ${description} '${file}'.."
if [[ -r "$file" ]]; then
echo_ok
else
echo_failed
fatal "${description^} '${file}' does not exist or is not readable."
fi
}
check_service_exists() {
local service="$1"
echononl "Check systemd service '${service}'.."
if systemctl cat "$service" > /dev/null 2>&1 ; then
echo_ok
else
echo_failed
fatal "Systemd service '${service}' was not found."
fi
}
check_service_active() {
local service="$1"
echononl "Check systemd service '${service}' is active.."
if systemctl is-active --quiet "$service" ; then
echo_ok
else
echo_failed
fatal "Systemd service '${service}' is not active."
fi
}
# ----------
# Security checks for sourced files
# ----------
check_secure_source_file() {
local file="$1"
local description="${2:-configuration file}"
local owner_uid
local owner_gid
local mode
echononl "Check security of ${description} '${file}'.."
if [[ ! -f "$file" || ! -r "$file" ]]; then
echo_failed
fatal "${description^} '${file}' does not exist or is not readable."
fi
owner_uid="$(stat -c '%u' "$file" 2>/dev/null)" || {
echo_failed
fatal "Cannot determine owner of '${file}'."
}
owner_gid="$(stat -c '%g' "$file" 2>/dev/null)" || {
echo_failed
fatal "Cannot determine group of '${file}'."
}
mode="$(stat -c '%a' "$file" 2>/dev/null)" || {
echo_failed
fatal "Cannot determine permissions of '${file}'."
}
if [[ "$owner_uid" -ne 0 || "$owner_gid" -ne 0 ]]; then
echo_failed
fatal "${description^} '${file}' must be owned by root:root."
fi
if (( (8#$mode & 0022) != 0 )); then
echo_failed
fatal "${description^} '${file}' must not be writable by group or others."
fi
echo_ok
}
check_state_file_permissions() {
local mode
echononl "Check permissions of persistent SRS state '${SRS_STATE_FILE}'.."
if [[ ! -f "$SRS_STATE_FILE" ]]; then
echo_failed
fatal "Persistent SRS state '${SRS_STATE_FILE}' does not exist."
fi
if [[ "$(stat -c '%U:%G' "$SRS_STATE_FILE" 2>/dev/null)" != "root:root" ]]; then
echo_failed
fatal "Persistent SRS state '${SRS_STATE_FILE}' must be owned by root:root."
fi
mode="$(stat -c '%a' "$SRS_STATE_FILE" 2>/dev/null)" || {
echo_failed
fatal "Cannot determine permissions of '${SRS_STATE_FILE}'."
}
if [[ "$mode" != "600" ]]; then
echo_failed
fatal "Persistent SRS state '${SRS_STATE_FILE}' must have mode 0600 (current: ${mode})."
fi
echo_ok
}
# ----------
# SRS configuration
# ----------
load_srs_config() {
if [[ ! -r "$SRS_CONF_FILE" ]]; then
fatal "SRS configuration '${SRS_CONF_FILE}' does not exist or is not readable. Use '${SRS_CONF_SAMPLE_FILE}' as template."
fi
check_secure_source_file "$SRS_CONF_FILE" "SRS configuration"
echononl "Read SRS configuration '${SRS_CONF_FILE}'.."
unset _SRS_DOMAIN
# shellcheck source=/dev/null
source "$SRS_CONF_FILE"
if [[ -z "${_SRS_DOMAIN:-}" ]]; then
echo_failed
fatal "Variable '_SRS_DOMAIN' is not set in '${SRS_CONF_FILE}'."
fi
if [[ "$_SRS_DOMAIN" =~ [[:space:]] ]]; then
echo_failed
fatal "Invalid SRS domain '${_SRS_DOMAIN}': whitespace is not allowed."
fi
if [[ ! "$_SRS_DOMAIN" =~ ^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$ ]]; then
echo_failed
fatal "Invalid SRS domain '${_SRS_DOMAIN}'."
fi
SRS_CONFIG_LOADED=true
echo_ok
}
# ----------
# Persistent pre-SRS state
# ----------
_is_boolean_value() {
[[ "$1" = "true" || "$1" = "false" ]]
}
load_srs_state() {
if [[ ! -r "$SRS_STATE_FILE" ]]; then
fatal "Persistent SRS state '${SRS_STATE_FILE}' does not exist or is not readable."
fi
check_state_file_permissions
check_secure_source_file "$SRS_STATE_FILE" "persistent SRS state"
echononl "Read persistent SRS state '${SRS_STATE_FILE}'.."
unset _STATE_VERSION
unset _STATE_ORIGIN
unset _STATE_HOSTNAME
unset _PRE_SRS_DEFAULT_TRANSPORT_SET
unset _PRE_SRS_DEFAULT_TRANSPORT
unset _PRE_SRS_RECIPIENT_CANONICAL_MAPS_SET
unset _PRE_SRS_RECIPIENT_CANONICAL_MAPS
unset _PRE_SRS_SRS_FORWARD_DESTINATION_RECIPIENT_LIMIT_SET
unset _PRE_SRS_SRS_FORWARD_DESTINATION_RECIPIENT_LIMIT
unset _PRE_SRS_SMTP_RECEIVE_OVERRIDE_OPTIONS_SET
unset _PRE_SRS_SMTP_RECEIVE_OVERRIDE_OPTIONS
unset _PRE_SRS_AMAVIS_RECEIVE_OVERRIDE_OPTIONS_SET
unset _PRE_SRS_AMAVIS_RECEIVE_OVERRIDE_OPTIONS
# shellcheck source=/dev/null
source "$SRS_STATE_FILE"
if [[ "${_STATE_VERSION:-}" != "1" ]]; then
echo_failed
fatal "Unsupported or missing SRS state version in '${SRS_STATE_FILE}'."
fi
local bool_var
for bool_var in _PRE_SRS_DEFAULT_TRANSPORT_SET _PRE_SRS_RECIPIENT_CANONICAL_MAPS_SET _PRE_SRS_SRS_FORWARD_DESTINATION_RECIPIENT_LIMIT_SET _PRE_SRS_SMTP_RECEIVE_OVERRIDE_OPTIONS_SET _PRE_SRS_AMAVIS_RECEIVE_OVERRIDE_OPTIONS_SET
do
if ! _is_boolean_value "${!bool_var:-}" ; then
echo_failed
fatal "Invalid boolean value for '${bool_var}' in '${SRS_STATE_FILE}'."
fi
done
if [[ -n "${_STATE_HOSTNAME:-}" && "${_STATE_HOSTNAME}" != "$(hostname -f)" ]]; then
echo_warning
warn "SRS state was created for host '${_STATE_HOSTNAME}', current hostname is '$(hostname -f)'."
else
echo_ok
fi
SRS_STATE_LOADED=true
}
# ----------
# Managed block helpers
# ----------
count_exact_line() {
local file="$1"
local line="$2"
grep -Fxc "$line" "$file" 2>/dev/null || true
}
check_managed_block_markers() {
local file="$1"
local begin_marker="$2"
local end_marker="$3"
local begin_count
local end_count
local begin_line
local end_line
begin_count="$(count_exact_line "$file" "$begin_marker")"
end_count="$(count_exact_line "$file" "$end_marker")"
if [[ "$begin_count" -eq 0 && "$end_count" -eq 0 ]]; then
return 1
fi
if [[ "$begin_count" -ne 1 || "$end_count" -ne 1 ]]; then
return 2
fi
begin_line="$(grep -Fnx "$begin_marker" "$file" | cut -d: -f1)"
end_line="$(grep -Fnx "$end_marker" "$file" | cut -d: -f1)"
if [[ -z "$begin_line" || -z "$end_line" ]]; then
return 2
fi
if (( begin_line >= end_line )); then
return 2
fi
return 0
}