#!/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 }