#!/usr/bin/env bash # ---------- # Selective SRS deactivation # # Restores the Postfix configuration state recorded before selective SRS # was activated. # # The script is intentionally conservative: # # - DISABLED -> no changes, rc=0 # - ENABLED -> deactivate after explicit confirmation # - INCONSISTENT -> abort without changes # # Persistent pre-SRS state is read from: # # /etc/postfix-srs/state.conf # # The state file and installed SRS components are intentionally retained. # ---------- # ---------- # Script setup # ---------- working_dir="$(dirname "$(realpath "$0")")" # shellcheck source=helpers/srs-common.sh source "${working_dir}/helpers/srs-common.sh" trap 'handle_signal' SIGHUP SIGINT SIGTERM init_srs_runtime # ---------- # Deactivation runtime state # ---------- DEACTIVATION_CHANGES_STARTED=false DEACTIVATION_BACKUP_DIR="" POSTFIX_SRS_DROPIN_WAS_PRESENT=false SRS_DELIVERY_DROPIN_WAS_PRESENT=false declare -A SERVICE_WAS_ACTIVE declare -A SERVICE_WAS_ENABLED # Queue manager processes active before the first (phase-1) reload. SRS_OLD_QMGR_PIDS=() # ---------- # Local helper functions # ---------- main_cf_explicit_value() { local parameter="$1" awk -v parameter="$parameter" ' /^[[:space:]]*#/ { next } $0 ~ "^[[:space:]]*" parameter "[[:space:]]*=" { line=$0 } END { if (line != "") { sub("^[[:space:]]*" parameter "[[:space:]]*=[[:space:]]*", "", line) print line } } ' "$POSTFIX_MAIN_CF" } main_cf_parameter_is_set() { local parameter="$1" grep -Eq \ "^[[:space:]]*${parameter}[[:space:]]*=" \ "$POSTFIX_MAIN_CF" } master_service_option_value() { local service="$1" local option="$2" awk -v service="$service" -v option="$option" ' /^[[:space:]]*#/ { next } $1 == service && $2 == "inet" { in_service=1 next } in_service && $0 !~ /^[[:space:]]/ { exit } in_service && $1 == "-o" { prefix=option "=" if (index($2, prefix) == 1) { value=$2 sub("^" option "=", "", value) print value exit } } ' "$POSTFIX_MASTER_CF" } master_srs_forward_present() { local master_contents # Read failures must not be indistinguishable from an absent service. master_contents="$(< "$POSTFIX_MASTER_CF")" || return 2 awk ' /^[[:space:]]*#/ { next } $1 == "srs-forward" && $2 == "unix" { found_service=1 next } found_service && $0 !~ /^[[:space:]]/ { exit } found_service && $0 ~ /argv=\/usr\/local\/sbin\/srs-forward\.py/ { found_argv=1 } END { if (found_service && found_argv) exit 0 exit 1 } ' <<< "$master_contents" } port_is_listening() { local host="$1" local port="$2" local sockets rc # Capture the status of ss separately: without this, an ss failure # can look like an empty socket listing. sockets="$(ss -ltnH 2>> "$log_file")" || return 2 awk -v host="$host" -v port="$port" ' { local_address=$4 if (local_address == host ":" port || local_address == "[" host "]:" port) { found=1 exit } } END { exit(found ? 0 : 1) } ' <<< "$sockets" rc=$? case "$rc" in 0|1) return "$rc" ;; *) return 2 ;; esac } service_exists() { systemctl cat "$1" > /dev/null 2>&1 } # One validated systemd snapshot per call. Neither a failed query nor an # unsupported/transient state may be mistaken for inactive or disabled. # Returns: 0 = understood; 2 = unknown, unsupported, or query error. # Results are set in SRS_SERVICE_ACTIVE_STATE and SRS_SERVICE_ENABLED_STATE. read_srs_service_state() { local output line key value local load_state="" active_state="" enabled_state="" SRS_SERVICE_ACTIVE_STATE="" SRS_SERVICE_ENABLED_STATE="" output="$(systemctl show "$1" --no-pager \ -p LoadState -p ActiveState -p UnitFileState 2>> "$log_file")" || return 2 while IFS= read -r line ; do key="${line%%=*}" value="${line#*=}" [[ "$line" == *=* ]] || return 2 case "$key" in LoadState) [[ -z "$load_state" ]] || return 2 load_state="$value" ;; ActiveState) [[ -z "$active_state" ]] || return 2 active_state="$value" ;; UnitFileState) [[ -z "$enabled_state" ]] || return 2 enabled_state="$value" ;; *) return 2 ;; esac done <<< "$output" [[ "$load_state" == "loaded" ]] || return 2 case "$active_state" in active|inactive) ;; *) return 2 ;; esac case "$enabled_state" in enabled|enabled-runtime|disabled) ;; *) return 2 ;; esac SRS_SERVICE_ACTIVE_STATE="$active_state" SRS_SERVICE_ENABLED_STATE="$enabled_state" return 0 } # Returns: 0 = active/enabled, 1 = inactive/disabled, 2 = query error. service_active() { read_srs_service_state "$1" || return 2 [[ "$SRS_SERVICE_ACTIVE_STATE" == "active" ]] } service_enabled_state() { read_srs_service_state "$1" || return 2 printf '%s\n' "$SRS_SERVICE_ENABLED_STATE" } service_enabled() { read_srs_service_state "$1" || return 2 case "$SRS_SERVICE_ENABLED_STATE" in enabled|enabled-runtime) return 0 ;; disabled) return 1 ;; *) return 2 ;; esac } postsrsd_srs_domain() { awk ' /^[[:space:]]*#/ { next } /^[[:space:]]*srs-domain[[:space:]]*=/ { line=$0 sub(/^[^=]*=[[:space:]]*/, "", line) gsub(/^[[:space:]]+|[[:space:]]+$/, "", line) gsub(/^"|"$/, "", line) print line exit } ' "$POSTSRSD_CONF_FILE" } srs_alias_tool() { local action="$1" python3 "$SRS_ALIAS_HELPER" "$action" \ --main-cf "$POSTFIX_MAIN_CF" \ --domain "$_SRS_DOMAIN" \ --reverse-map "socketmap:inet:${SRS_REVERSE_SOCKET_HOST}:${SRS_REVERSE_SOCKET_PORT}:reverse" } state_main_cf_matches() { local parameter="$1" local was_set="$2" local expected_value="$3" local current_value="" if "$was_set" ; then if ! main_cf_parameter_is_set "$parameter" ; then return 1 fi current_value="$(main_cf_explicit_value "$parameter")" [[ "$current_value" == "$expected_value" ]] return fi ! main_cf_parameter_is_set "$parameter" } state_master_option_matches() { local service="$1" local option="$2" local was_set="$3" local expected_value="$4" local current_value="" current_value="$(master_service_option_value "$service" "$option")" if "$was_set" ; then [[ "$current_value" == "$expected_value" ]] return fi [[ -z "$current_value" ]] } csv_add_value() { local current="$1" local add="$2" local item local result="" local found=false IFS=',' read -r -a _items <<< "$current" for item in "${_items[@]}" ; do item="${item#"${item%%[![:space:]]*}"}" item="${item%"${item##*[![:space:]]}"}" [[ -z "$item" ]] && continue if [[ "$item" == "$add" ]]; then found=true fi if [[ -z "$result" ]]; then result="$item" else result="${result},${item}" fi done if ! $found ; then if [[ -z "$result" ]]; then result="$add" else result="${result},${add}" fi fi printf '%s\n' "$result" } csv_remove_value() { local current="$1" local remove="$2" local item local result="" IFS=',' read -r -a _items <<< "$current" for item in "${_items[@]}" ; do item="${item#"${item%%[![:space:]]*}"}" item="${item%"${item##*[![:space:]]}"}" [[ -z "$item" ]] && continue [[ "$item" == "$remove" ]] && continue if [[ -z "$result" ]]; then result="$item" else result="${result},${item}" fi done printf '%s\n' "$result" } update_master_service_option() { local service="$1" local option="$2" local value="$3" python3 - \ "$POSTFIX_MASTER_CF" \ "$service" \ "$option" \ "$value" <<'PY_EOF' import os import re import stat import sys import tempfile path, service, option, value = sys.argv[1:5] with open(path, "r", encoding="utf-8") as f: lines = f.readlines() service_index = None for idx, line in enumerate(lines): if not line.strip() or line.lstrip().startswith("#"): continue fields = line.split() if len(fields) >= 2 and fields[0] == service and fields[1] == "inet": service_index = idx break if service_index is None: raise SystemExit(2) end_index = len(lines) for idx in range(service_index + 1, len(lines)): line = lines[idx] if line.strip() == "": end_index = idx break if line[0].isspace(): continue if line.lstrip().startswith("#"): end_index = idx break end_index = idx break option_re = re.compile( r"^(?P\s*)-o\s+" + re.escape(option) + r"=.*(?:\n)?$" ) option_index = None option_indent = " " for idx in range(service_index + 1, end_index): match = option_re.match(lines[idx]) if match: option_index = idx option_indent = match.group("indent") or " " break if value: new_line = f"{option_indent}-o {option}={value}\n" if option_index is not None: lines[option_index] = new_line else: lines.insert(end_index, new_line) else: if option_index is not None: del lines[option_index] st = os.stat(path) directory = os.path.dirname(path) fd, tmp_path = tempfile.mkstemp(prefix=".master.cf.srs.", dir=directory) try: with os.fdopen(fd, "w", encoding="utf-8") as f: f.writelines(lines) os.chmod(tmp_path, stat.S_IMODE(st.st_mode)) os.chown(tmp_path, st.st_uid, st.st_gid) os.replace(tmp_path, path) finally: if os.path.exists(tmp_path): os.unlink(tmp_path) PY_EOF } remove_managed_block() { local file="$1" local begin_marker="$2" local end_marker="$3" python3 - \ "$file" \ "$begin_marker" \ "$end_marker" <<'PY_EOF' import os import stat import sys import tempfile path, begin_marker, end_marker = sys.argv[1:4] with open(path, "r", encoding="utf-8") as f: lines = f.readlines() begin_matches = [ idx for idx, line in enumerate(lines) if line.rstrip("\n") == begin_marker ] end_matches = [ idx for idx, line in enumerate(lines) if line.rstrip("\n") == end_marker ] if len(begin_matches) != 1 or len(end_matches) != 1: raise SystemExit(2) begin_idx = begin_matches[0] end_idx = end_matches[0] if begin_idx >= end_idx: raise SystemExit(3) new_lines = lines[:begin_idx] + lines[end_idx + 1:] # activate-srs.sh appends exactly one separator line before the # managed block. Remove only that separator (also when the block was at # EOF), preserving every blank line that existed before activation. if begin_idx > 0 and lines[begin_idx - 1] == "\n": del new_lines[begin_idx - 1] st = os.stat(path) directory = os.path.dirname(path) fd, tmp_path = tempfile.mkstemp(prefix=".srs-managed.", dir=directory) try: with os.fdopen(fd, "w", encoding="utf-8") as f: f.writelines(new_lines) os.chmod(tmp_path, stat.S_IMODE(st.st_mode)) os.chown(tmp_path, st.st_uid, st.st_gid) os.replace(tmp_path, path) finally: if os.path.exists(tmp_path): os.unlink(tmp_path) PY_EOF } restore_main_cf_parameter() { local parameter="$1" local was_set="$2" local value="$3" python3 - \ "$POSTFIX_MAIN_CF" \ "$parameter" \ "$was_set" \ "$value" <<'PY_EOF' import os import re import stat import sys import tempfile path, parameter, was_set, value = sys.argv[1:5] with open(path, "r", encoding="utf-8") as f: lines = f.readlines() pattern = re.compile( r"^[ \t]*" + re.escape(parameter) + r"[ \t]*=" ) new_lines = [] skip_continuation = False for line in lines: if skip_continuation: if line.startswith((" ", "\t")) and not line.lstrip().startswith("#"): continue skip_continuation = False if line.lstrip().startswith("#"): new_lines.append(line) continue if pattern.match(line): skip_continuation = True continue new_lines.append(line) if was_set == "true": if new_lines and new_lines[-1].strip() != "": new_lines.append("\n") new_lines.append(f"{parameter} = {value}\n") st = os.stat(path) directory = os.path.dirname(path) fd, tmp_path = tempfile.mkstemp(prefix=".main.cf.srs.", dir=directory) try: with os.fdopen(fd, "w", encoding="utf-8") as f: f.writelines(new_lines) os.chmod(tmp_path, stat.S_IMODE(st.st_mode)) os.chown(tmp_path, st.st_uid, st.st_gid) os.replace(tmp_path, path) finally: if os.path.exists(tmp_path): os.unlink(tmp_path) PY_EOF } remove_legacy_srs_forward_service() { python3 - "$POSTFIX_MASTER_CF" <<'PY_EOF' import os import stat import sys import tempfile path = sys.argv[1] with open(path, "r", encoding="utf-8") as f: lines = f.readlines() start = None for idx, line in enumerate(lines): if not line.strip() or line.lstrip().startswith("#"): continue fields = line.split() if len(fields) >= 2 and fields[0] == "srs-forward" and fields[1] == "unix": if start is not None: raise SystemExit(2) start = idx if start is None: raise SystemExit(3) end = len(lines) for idx in range(start + 1, len(lines)): line = lines[idx] if line.strip() == "": end = idx break if line[0].isspace(): continue if line.lstrip().startswith("#"): end = idx break end = idx break block = "".join(lines[start:end]) required = [ "flags=Rq null_sender= user=nobody", "argv=/usr/local/sbin/srs-forward.py", "${sender}", "${recipient}", "${original_recipient}", "${queue_id}", "envid=${envid}", ] for token in required: if token not in block: raise SystemExit(4) new_lines = lines[:start] + lines[end:] while ( start > 0 and start < len(new_lines) and new_lines[start - 1].strip() == "" and new_lines[start].strip() == "" ): del new_lines[start] st = os.stat(path) directory = os.path.dirname(path) fd, tmp_path = tempfile.mkstemp(prefix=".master.cf.srs.", dir=directory) try: with os.fdopen(fd, "w", encoding="utf-8") as f: f.writelines(new_lines) os.chmod(tmp_path, stat.S_IMODE(st.st_mode)) os.chown(tmp_path, st.st_uid, st.st_gid) os.replace(tmp_path, path) finally: if os.path.exists(tmp_path): os.unlink(tmp_path) PY_EOF } systemd_dropins_match_expected() { [[ -r "$POSTFIX_SRS_DROPIN_FILE" ]] || return 1 [[ -r "$SRS_DELIVERY_DROPIN_FILE" ]] || return 1 grep -Fxq \ "Wants=${SRS_REVERSE_MAP_SERVICE} ${SRS_DELIVERY_SERVICE}" \ "$POSTFIX_SRS_DROPIN_FILE" || return 1 grep -Fxq \ "After=${SRS_REVERSE_MAP_SERVICE} ${SRS_DELIVERY_SERVICE}" \ "$POSTFIX_SRS_DROPIN_FILE" || return 1 grep -Fxq \ "Wants=${POSTSRSD_SERVICE}" \ "$SRS_DELIVERY_DROPIN_FILE" || return 1 grep -Fxq \ "After=${POSTSRSD_SERVICE}" \ "$SRS_DELIVERY_DROPIN_FILE" || return 1 return 0 } snapshot_systemd_dropins() { if [[ -f "$POSTFIX_SRS_DROPIN_FILE" ]]; then POSTFIX_SRS_DROPIN_WAS_PRESENT=true cp -a "$POSTFIX_SRS_DROPIN_FILE" \ "${DEACTIVATION_BACKUP_DIR}/postfix.service-srs.conf" || return 1 fi if [[ -f "$SRS_DELIVERY_DROPIN_FILE" ]]; then SRS_DELIVERY_DROPIN_WAS_PRESENT=true cp -a "$SRS_DELIVERY_DROPIN_FILE" \ "${DEACTIVATION_BACKUP_DIR}/postfix@srs-delivery.service-srs.conf" || return 1 fi return 0 } remove_srs_systemd_dropins() { rm -f \ "$POSTFIX_SRS_DROPIN_FILE" \ "$SRS_DELIVERY_DROPIN_FILE" || return 1 systemctl daemon-reload || return 1 return 0 } restore_systemd_dropins() { local restore_failed=false if $POSTFIX_SRS_DROPIN_WAS_PRESENT ; then mkdir -p "$POSTFIX_SRS_DROPIN_DIR" || restore_failed=true cp -a \ "${DEACTIVATION_BACKUP_DIR}/postfix.service-srs.conf" \ "$POSTFIX_SRS_DROPIN_FILE" || restore_failed=true else rm -f "$POSTFIX_SRS_DROPIN_FILE" || restore_failed=true fi if $SRS_DELIVERY_DROPIN_WAS_PRESENT ; then mkdir -p "$SRS_DELIVERY_DROPIN_DIR" || restore_failed=true cp -a \ "${DEACTIVATION_BACKUP_DIR}/postfix@srs-delivery.service-srs.conf" \ "$SRS_DELIVERY_DROPIN_FILE" || restore_failed=true else rm -f "$SRS_DELIVERY_DROPIN_FILE" || restore_failed=true fi systemctl daemon-reload || restore_failed=true ! $restore_failed } snapshot_service_states() { local service local -A snapshot_active=() snapshot_enabled=() for service in \ "$POSTSRSD_SERVICE" \ "$SRS_REVERSE_MAP_SERVICE" \ "$SRS_DELIVERY_SERVICE" do if ! read_srs_service_state "$service" ; then warn "Cannot determine a supported systemd state for '${service}'." return 1 fi # The caller has classified this installation as ENABLED. Reject # a state transition since that check rather than capturing an # unexpectedly stopped/disabled service as the rollback baseline. if [[ "$SRS_SERVICE_ACTIVE_STATE" != "active" || "$SRS_SERVICE_ENABLED_STATE" == "disabled" ]]; then warn "SRS service '${service}' changed state before deactivation." return 1 fi snapshot_active["$service"]="true" snapshot_enabled["$service"]="$SRS_SERVICE_ENABLED_STATE" done # Only publish a complete, validated snapshot. for service in \ "$POSTSRSD_SERVICE" \ "$SRS_REVERSE_MAP_SERVICE" \ "$SRS_DELIVERY_SERVICE" do SERVICE_WAS_ACTIVE["$service"]="${snapshot_active[$service]}" SERVICE_WAS_ENABLED["$service"]="${snapshot_enabled[$service]}" done return 0 } restore_service_states() { local service local restore_failed=false local enabled_state active_state # Validate the entire snapshot before changing *any* unit. for service in \ "$POSTSRSD_SERVICE" \ "$SRS_REVERSE_MAP_SERVICE" \ "$SRS_DELIVERY_SERVICE" do enabled_state="${SERVICE_WAS_ENABLED[$service]:-}" active_state="${SERVICE_WAS_ACTIVE[$service]:-}" case "$enabled_state" in enabled|enabled-runtime|disabled) ;; *) warn "Invalid saved enable state for '${service}': '${enabled_state}'."; return 1 ;; esac case "$active_state" in true|false) ;; *) warn "Invalid saved active state for '${service}': '${active_state}'."; return 1 ;; esac done for service in \ "$POSTSRSD_SERVICE" \ "$SRS_REVERSE_MAP_SERVICE" \ "$SRS_DELIVERY_SERVICE" do enabled_state="${SERVICE_WAS_ENABLED[$service]}" case "$enabled_state" in enabled) systemctl enable "$service" > /dev/null 2>> "$log_file" || restore_failed=true ;; enabled-runtime) systemctl enable --runtime "$service" > /dev/null 2>> "$log_file" || restore_failed=true ;; disabled) systemctl disable "$service" > /dev/null 2>> "$log_file" || restore_failed=true ;; esac if [[ "${SERVICE_WAS_ACTIVE[$service]}" == "true" ]]; then systemctl start "$service" > /dev/null 2>> "$log_file" || restore_failed=true else systemctl stop "$service" > /dev/null 2>> "$log_file" || restore_failed=true fi done ! $restore_failed } srs_delivery_queue_is_empty() { local queue_data queue_data="$( postqueue -c "$POSTFIX_SRS_DELIVERY_DIR" -j 2>> "$log_file" )" || return 2 [[ -z "${queue_data//[[:space:]]/}" ]] } # ---------- # Deactivation phase 1: wait for old routing work to finish. # The old qmgr process(es) must exit after the main Postfix reload. # Only then can we assume that no further delivery requests using the # former default_transport=srs-forward will be scheduled by that qmgr. # Also wait for all remaining srs-forward pipe workers, including idle # ones, before removing the service from master.cf. # ---------- capture_srs_old_queue_managers() { local master_pid pid ppid comm process_list process_list="$(ps -eo pid=,ppid=,comm=)" || return 1 master_pid="$(systemctl show "$POSTFIX_SERVICE" -p MainPID --value 2>> "$log_file")" || return 1 [[ "$master_pid" =~ ^[1-9][0-9]*$ ]] || return 1 kill -0 "$master_pid" 2>> "$log_file" || return 1 SRS_OLD_QMGR_PIDS=() while read -r pid ppid comm ; do if [[ "$ppid" == "$master_pid" && "$comm" == 'qmgr' ]]; then SRS_OLD_QMGR_PIDS+=("$pid") fi done <<< "$process_list" # Fail closed if the expected running queue manager cannot be identified. (( ${#SRS_OLD_QMGR_PIDS[@]} > 0 )) } srs_old_queue_manager_is_running() { local pid comm for pid in "${SRS_OLD_QMGR_PIDS[@]}" ; do [[ -d "/proc/$pid" ]] || continue if ! comm="$(ps -p "$pid" -o comm= 2>/dev/null)" ; then # The process may have exited between the two checks. [[ -d "/proc/$pid" ]] && return 2 continue fi # An unexpected process identity must not count as success. [[ "$comm" == 'qmgr' ]] || return 2 return 0 done return 1 } srs_forward_worker_is_running() { local process_list process_list="$(ps -eo comm=,args=)" || return 2 printf '%s\n' "$process_list" | awk ' $1 == "pipe" && /srs-forward/ { found=1 } $1 ~ /^python([0-9.]*)?$/ && /\/usr\/local\/sbin\/srs-forward[.]py/ { found=1 } END { exit(found ? 0 : 1) } ' } wait_for_srs_forward_drain() { local deadline=$((SECONDS + 180)) local _workers_rc _queue_rc _qmgr_rc while (( SECONDS < deadline )); do srs_old_queue_manager_is_running _qmgr_rc=$? case "$_qmgr_rc" in 0) sleep 2; continue ;; 1) ;; *) return 2 ;; esac srs_forward_worker_is_running _workers_rc=$? case "$_workers_rc" in 0) sleep 2; continue ;; 1) ;; *) return 2 ;; esac srs_delivery_queue_is_empty _queue_rc=$? case "$_queue_rc" in 0) ;; 1) sleep 2; continue ;; *) return 2 ;; esac # A second sample helps avoid treating a transient empty queue # as quiescence; it is not a substitute for waiting for old qmgr. sleep 2 srs_old_queue_manager_is_running _qmgr_rc=$? case "$_qmgr_rc" in 0) continue ;; 1) ;; *) return 2 ;; esac srs_forward_worker_is_running _workers_rc=$? case "$_workers_rc" in 0) continue ;; 1) ;; *) return 2 ;; esac srs_delivery_queue_is_empty _queue_rc=$? case "$_queue_rc" in 0) return 0 ;; 1) ;; *) return 2 ;; esac done return 1 } rollback_deactivation() { local rollback_failed=false if ! $DEACTIVATION_CHANGES_STARTED ; then return 0 fi blank_line warn "SRS deactivation failed. Trying to restore the previous state." echononl "Restore '${POSTFIX_MAIN_CF}'.." if cp -a "${DEACTIVATION_BACKUP_DIR}/main.cf" "$POSTFIX_MAIN_CF" > "$log_file" 2>&1 ; then echo_ok else echo_failed rollback_failed=true fi echononl "Restore '${POSTFIX_MASTER_CF}'.." if cp -a "${DEACTIVATION_BACKUP_DIR}/master.cf" "$POSTFIX_MASTER_CF" > "$log_file" 2>&1 ; then echo_ok else echo_failed rollback_failed=true fi echononl "Restore previous systemd SRS drop-ins.." if restore_systemd_dropins ; then echo_ok else echo_failed rollback_failed=true fi echononl "Restore previous SRS service states.." if restore_service_states ; then echo_ok else echo_failed rollback_failed=true fi echononl "Check restored Postfix configuration.." if postfix check > "$log_file" 2>&1 ; then echo_ok else echo_failed rollback_failed=true fi if systemctl is-active --quiet "$POSTFIX_SERVICE" ; then echononl "Reload restored Postfix configuration.." if systemctl reload "$POSTFIX_SERVICE" > "$log_file" 2>&1 ; then echo_ok else echo_failed rollback_failed=true fi else echononl "Check primary Postfix service '${POSTFIX_SERVICE}' is active.." echo_failed warn "Primary Postfix is inactive; restored configuration could not be reloaded." rollback_failed=true fi if $rollback_failed ; then error "Automatic rollback could not fully restore the previous state. Manual intervention is required." return 1 fi info "Previous enabled SRS state was successfully restored." return 0 } fatal_with_deactivation_rollback() { local message="$1" if $DEACTIVATION_CHANGES_STARTED ; then if rollback_deactivation ; then DEACTIVATION_CHANGES_STARTED=false fatal "${message} The previous state was successfully restored." else fatal "${message} Automatic rollback could not fully restore the previous state." fi fi fatal "$message" } handle_signal() { trap - SIGHUP SIGINT SIGTERM warn "Deactivation interrupted by signal." if $DEACTIVATION_CHANGES_STARTED ; then rollback_deactivation fi clean_up 1 } # ---------- # Header # ---------- blank_line echo -e "\033[1m----------\033[m" echo -e "\033[32m\033[1mRunning script \033[m\033[1m${script_name}\033[32m .. \033[m" echo -e "\033[1m----------\033[m" blank_line # ---------- # Basic checks # ---------- check_root check_required_commands \ awk \ cp \ cut \ grep \ hostname \ mkdir \ mktemp \ mv \ postfix \ postqueue \ python3 \ realpath \ rm \ sed \ ss \ stat \ systemctl load_srs_config load_srs_state [[ -f "$SRS_ALIAS_HELPER" && ! -L "$SRS_ALIAS_HELPER" ]] || fatal "Missing regular SRS alias helper: ${SRS_ALIAS_HELPER}" # ---------- # Expected active master.cf values # ---------- smtp_pre_value="" amavis_pre_value="" if $_PRE_SRS_SMTP_RECEIVE_OVERRIDE_OPTIONS_SET ; then smtp_pre_value="$_PRE_SRS_SMTP_RECEIVE_OVERRIDE_OPTIONS" fi if $_PRE_SRS_AMAVIS_RECEIVE_OVERRIDE_OPTIONS_SET ; then amavis_pre_value="$_PRE_SRS_AMAVIS_RECEIVE_OVERRIDE_OPTIONS" fi smtp_active_value="$(csv_add_value "$smtp_pre_value" "no_address_mappings")" amavis_active_value="$(csv_remove_value "$amavis_pre_value" "no_address_mappings")" # ---------- # Check installation # ---------- blank_line echo -e "\033[37m\033[1mCheck selective SRS installation..\033[m" blank_line installation_ok=true for _file in \ "$POSTFIX_MAIN_CF" \ "$POSTFIX_MASTER_CF" \ "$POSTSRSD_CONF_FILE" \ "$POSTSRSD_SECRET_FILE" \ "${POSTFIX_SRS_DELIVERY_DIR}/main.cf" \ "${POSTFIX_SRS_DELIVERY_DIR}/master.cf" \ "${POSTFIX_SRS_DELIVERY_DIR}/transport" \ "${POSTFIX_SRS_DELIVERY_DIR}/transport.db" do echononl "Check file '${_file}'.." if [[ -r "$_file" ]]; then echo_ok else echo_failed installation_ok=false fi done for _script in \ "$SRS_ALIAS_DECISION_SCRIPT" \ "$SRS_FORWARD_SCRIPT" \ "$SRS_REVERSE_MAP_SCRIPT" \ "$SRS_REVERSE_POLICY_SCRIPT" do echononl "Check executable '${_script}'.." if [[ -x "$_script" ]]; then echo_ok else echo_failed installation_ok=false fi done echononl "Check configured PostSRSd SRS domain.." if [[ "$(postsrsd_srs_domain)" == "$_SRS_DOMAIN" ]]; then echo_ok else echo_failed installation_ok=false fi # ---------- # Check services and listeners # ---------- blank_line echo -e "\033[37m\033[1mCheck selective SRS runtime..\033[m" blank_line services_exist=true services_active=true services_enabled=true services_inactive=true services_disabled=true for _service in \ "$POSTSRSD_SERVICE" \ "$SRS_REVERSE_MAP_SERVICE" \ "$SRS_DELIVERY_SERVICE" do echononl "Check systemd service '${_service}' exists.." if service_exists "$_service" ; then echo_ok else echo_failed services_exist=false services_active=false services_enabled=false continue fi echononl "Check systemd service '${_service}' is active.." service_active "$_service" _service_rc=$? case "$_service_rc" in 0) echo_ok; services_inactive=false ;; 1) echo_inactive; services_active=false ;; *) fatal "Cannot read valid active state for '${_service}'. No changes were made." ;; esac service_enabled "$_service" _service_rc=$? case "$_service_rc" in 0) echononl "Check systemd service '${_service}' enable state (enabled).." echo_ok services_disabled=false ;; 1) echononl "Check systemd service '${_service}' enable state (disabled).." echo_ok services_enabled=false ;; *) fatal "Cannot read valid enable state for '${_service}'. No changes were made." ;; esac done listeners_active=true listeners_inactive=true while IFS='|' read -r _label _host _port ; do port_is_listening "$_host" "$_port" _port_rc=$? case "$_port_rc" in 0) echononl "Check ${_label} state on ${_host}:${_port} (listening).." echo_ok listeners_inactive=false ;; 1) echononl "Check ${_label} state on ${_host}:${_port} (inactive).." echo_ok listeners_active=false ;; *) fatal "Cannot inspect listener ${_label} on ${_host}:${_port}. No changes were made." ;; esac done <}" echo " PRE-SRS AmaViS receive_override_options.......: ${_PRE_SRS_AMAVIS_RECEIVE_OVERRIDE_OPTIONS:-}" echo "" echo " SRS configuration and binaries will remain installed." echo " ${SRS_STATE_FILE} will NOT be deleted." echo "" if ! ask_yes_no "einverstanden" "no" ; then warn "Deactivation cancelled by user. Nothing was changed." clean_up 0 fi # ---------- # Require a running primary Postfix before making any changes # ---------- echononl "Check primary Postfix service '${POSTFIX_SERVICE}' is active.." if systemctl is-active --quiet "$POSTFIX_SERVICE" ; then echo_ok else echo_failed fatal "Primary Postfix service '${POSTFIX_SERVICE}' is inactive. Deactivation was not attempted." fi # ---------- # Prepare rollback # ---------- echononl "Snapshot previous SRS service states.." if snapshot_service_states ; then echo_ok else echo_failed fatal "Could not snapshot all SRS systemd service states. Deactivation was not attempted." fi DEACTIVATION_BACKUP_DIR="/root/postfix-srs-deactivation-${backup_date}" echononl "Create deactivation backup directory.." if mkdir -m 0700 "$DEACTIVATION_BACKUP_DIR" > "$log_file" 2>&1 ; then echo_ok printf ' Directory: %s\n' "$DEACTIVATION_BACKUP_DIR" else echo_failed fatal "$(cat "$log_file")" fi echononl "Backup '${POSTFIX_MAIN_CF}'.." if cp -a "$POSTFIX_MAIN_CF" "${DEACTIVATION_BACKUP_DIR}/main.cf" > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal "$(cat "$log_file")" fi echononl "Backup '${POSTFIX_MASTER_CF}'.." if cp -a "$POSTFIX_MASTER_CF" "${DEACTIVATION_BACKUP_DIR}/master.cf" > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal "$(cat "$log_file")" fi echononl "Backup systemd SRS drop-ins.." if snapshot_systemd_dropins > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal "$(cat "$log_file")" fi DEACTIVATION_CHANGES_STARTED=true # ---------- # Restore main.cf # ---------- blank_line echo -e "\033[37m\033[1mRestore pre-SRS Postfix configuration..\033[m" blank_line echononl "Remove managed SRS block from '${POSTFIX_MAIN_CF}'.." if remove_managed_block \ "$POSTFIX_MAIN_CF" \ "$SRS_MAIN_CF_BEGIN_MARKER" \ "$SRS_MAIN_CF_END_MARKER" > "$log_file" 2>&1 then echo_ok else echo_failed fatal_with_deactivation_rollback "Could not remove managed SRS block from '${POSTFIX_MAIN_CF}'." fi echononl "Restore pre-SRS 'default_transport'.." if restore_main_cf_parameter \ default_transport \ "$_PRE_SRS_DEFAULT_TRANSPORT_SET" \ "$_PRE_SRS_DEFAULT_TRANSPORT" > "$log_file" 2>&1 then echo_ok else echo_failed fatal_with_deactivation_rollback "Could not restore pre-SRS 'default_transport'." fi echononl "Restore pre-SRS 'recipient_canonical_maps'.." if restore_main_cf_parameter \ recipient_canonical_maps \ "$_PRE_SRS_RECIPIENT_CANONICAL_MAPS_SET" \ "$_PRE_SRS_RECIPIENT_CANONICAL_MAPS" > "$log_file" 2>&1 then echo_ok else echo_failed fatal_with_deactivation_rollback "Could not restore pre-SRS 'recipient_canonical_maps'." fi echononl "Restore pre-SRS 'srs-forward_destination_recipient_limit'.." if restore_main_cf_parameter \ srs-forward_destination_recipient_limit \ "$_PRE_SRS_SRS_FORWARD_DESTINATION_RECIPIENT_LIMIT_SET" \ "$_PRE_SRS_SRS_FORWARD_DESTINATION_RECIPIENT_LIMIT" > "$log_file" 2>&1 then echo_ok else echo_failed fatal_with_deactivation_rollback "Could not restore pre-SRS recipient limit." fi # ---------- # Remove only SRS-owned virtual alias list items; preserve later admin edits # ---------- if [[ "$srs_alias_status" == 'managed' ]]; then echononl "Remove tracked SRS virtual alias items.." if srs_alias_tool remove > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_deactivation_rollback "Cannot safely remove SRS alias items: $(cat "$log_file")" fi fi # ---------- # Restore smtpd receive override settings for normal Postfix routing. # Keep srs-forward service until old queue-manager work is drained. # ---------- echononl "Restore pre-SRS port 25 receive_override_options.." if update_master_service_option \ smtp \ receive_override_options \ "$_PRE_SRS_SMTP_RECEIVE_OVERRIDE_OPTIONS" > "$log_file" 2>&1 then if ! $_PRE_SRS_SMTP_RECEIVE_OVERRIDE_OPTIONS_SET ; then # An empty value means remove the option. update_master_service_option smtp receive_override_options "" > "$log_file" 2>&1 || { echo_failed fatal_with_deactivation_rollback "Could not remove port 25 receive_override_options." } fi echo_ok else echo_failed fatal_with_deactivation_rollback "Could not restore port 25 receive_override_options." fi echononl "Restore pre-SRS AmaViS receive_override_options.." if $_PRE_SRS_AMAVIS_RECEIVE_OVERRIDE_OPTIONS_SET ; then _amavis_restore="$_PRE_SRS_AMAVIS_RECEIVE_OVERRIDE_OPTIONS" else _amavis_restore="" fi if update_master_service_option \ localhost:10025 \ receive_override_options \ "$_amavis_restore" > "$log_file" 2>&1 then echo_ok else echo_failed fatal_with_deactivation_rollback "Could not restore AmaViS receive_override_options." fi # ---------- # Phase 1: leave srs-forward in master.cf until old routing work # has finished. Removing it before the reload could terminate # running pipe workers abruptly. # ---------- # ---------- # Validate and reload main Postfix # ---------- echononl "Check restored Postfix configuration.." if postfix check > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_deactivation_rollback "Postfix configuration check failed. $(cat "$log_file")" fi echononl "Capture active Postfix queue manager before reload.." if capture_srs_old_queue_managers ; then echo_ok else echo_failed fatal_with_deactivation_rollback "Could not identify the old Postfix queue manager. Refusing unsafe deactivation." fi echononl "Reload main Postfix service '${POSTFIX_SERVICE}'.." if systemctl reload "$POSTFIX_SERVICE" > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_deactivation_rollback "Could not reload '${POSTFIX_SERVICE}'. $(cat "$log_file")" fi # ---------- # Phase 1: drain all old routing work before removing the pipe service. # ---------- echononl "Wait for old SRS queue-manager and pipe work to drain (max. 180s).." if wait_for_srs_forward_drain ; then echo_ok else echo_failed fatal_with_deactivation_rollback \ "Old SRS work or SRS delivery queue has not drained safely; SRS remains enabled after rollback." fi # ---------- # Phase 2: remove the now-unused pipe service and SRS dependencies. # ---------- # ---------- # Restore master.cf # ---------- if [[ "$master_block_status" == "present" ]]; then echononl "Remove managed SRS block from '${POSTFIX_MASTER_CF}'.." if remove_managed_block \ "$POSTFIX_MASTER_CF" \ "$SRS_MASTER_CF_BEGIN_MARKER" \ "$SRS_MASTER_CF_END_MARKER" > "$log_file" 2>&1 then echo_ok else echo_failed fatal_with_deactivation_rollback "Could not remove managed SRS block from '${POSTFIX_MASTER_CF}'." fi else echononl "Remove legacy unmarked 'srs-forward' service.." if remove_legacy_srs_forward_service > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_deactivation_rollback "Could not safely remove legacy unmarked 'srs-forward' service." fi fi # ---------- # Remove systemd SRS dependencies # ---------- echononl "Remove systemd SRS dependency drop-ins.." if remove_srs_systemd_dropins > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_deactivation_rollback "Could not remove systemd SRS dependency drop-ins. $(cat "$log_file")" fi echononl "Check final pre-SRS Postfix configuration.." if postfix check > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_deactivation_rollback "Final Postfix configuration check failed. $(cat "$log_file")" fi echononl "Reload main Postfix after removing SRS pipe service.." if systemctl reload "$POSTFIX_SERVICE" > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_deactivation_rollback "Final Postfix reload failed. $(cat "$log_file")" fi echononl "Verify SRS delivery queue remains empty before service shutdown.." if srs_delivery_queue_is_empty ; then echo_ok else echo_failed fatal_with_deactivation_rollback \ "SRS delivery queue is nonempty or unreadable after final reload. Refusing shutdown." fi # ---------- # Stop and disable SRS services # ---------- blank_line echo -e "\033[37m\033[1mStop selective SRS runtime services..\033[m" blank_line for _service in \ "$SRS_DELIVERY_SERVICE" \ "$SRS_REVERSE_MAP_SERVICE" \ "$POSTSRSD_SERVICE" do echononl "Stop systemd service '${_service}'.." if systemctl stop "$_service" > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_deactivation_rollback "Could not stop systemd service '${_service}'. $(cat "$log_file")" fi echononl "Disable systemd service '${_service}'.." if systemctl disable "$_service" > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_deactivation_rollback "Could not disable systemd service '${_service}'. $(cat "$log_file")" fi done # ---------- # Final verification # ---------- blank_line echo -e "\033[37m\033[1mVerify disabled selective SRS configuration..\033[m" blank_line verification_ok=true echononl "Verify managed main.cf block is absent.." check_managed_block_markers \ "$POSTFIX_MAIN_CF" \ "$SRS_MAIN_CF_BEGIN_MARKER" \ "$SRS_MAIN_CF_END_MARKER" _marker_rc=$? if [[ "$_marker_rc" -eq 1 ]]; then echo_ok else echo_failed verification_ok=false fi echononl "Verify managed master.cf block is absent.." check_managed_block_markers \ "$POSTFIX_MASTER_CF" \ "$SRS_MASTER_CF_BEGIN_MARKER" \ "$SRS_MASTER_CF_END_MARKER" _marker_rc=$? if [[ "$_marker_rc" -eq 1 ]]; then echo_ok else echo_failed verification_ok=false fi echononl "Verify 'srs-forward' service is absent.." master_srs_forward_present _master_rc=$? if [[ "$_master_rc" -eq 1 ]]; then echo_ok else echo_failed verification_ok=false fi echononl "Verify pre-SRS main.cf state.." if state_main_cf_matches \ default_transport \ "$_PRE_SRS_DEFAULT_TRANSPORT_SET" \ "$_PRE_SRS_DEFAULT_TRANSPORT" && state_main_cf_matches \ recipient_canonical_maps \ "$_PRE_SRS_RECIPIENT_CANONICAL_MAPS_SET" \ "$_PRE_SRS_RECIPIENT_CANONICAL_MAPS" && state_main_cf_matches \ srs-forward_destination_recipient_limit \ "$_PRE_SRS_SRS_FORWARD_DESTINATION_RECIPIENT_LIMIT_SET" \ "$_PRE_SRS_SRS_FORWARD_DESTINATION_RECIPIENT_LIMIT" then echo_ok else echo_failed verification_ok=false fi echononl "Verify SRS virtual alias items are absent.." if [[ "$(srs_alias_tool status 2>"$log_file")" == 'absent' ]]; then echo_ok else echo_failed verification_ok=false fi echononl "Verify pre-SRS master.cf state.." if state_master_option_matches \ smtp \ receive_override_options \ "$_PRE_SRS_SMTP_RECEIVE_OVERRIDE_OPTIONS_SET" \ "$_PRE_SRS_SMTP_RECEIVE_OVERRIDE_OPTIONS" && state_master_option_matches \ localhost:10025 \ receive_override_options \ "$_PRE_SRS_AMAVIS_RECEIVE_OVERRIDE_OPTIONS_SET" \ "$_PRE_SRS_AMAVIS_RECEIVE_OVERRIDE_OPTIONS" then echo_ok else echo_failed verification_ok=false fi echononl "Verify systemd SRS dependency drop-ins are absent.." if [[ ! -e "$POSTFIX_SRS_DROPIN_FILE" && ! -L "$POSTFIX_SRS_DROPIN_FILE" && ! -e "$SRS_DELIVERY_DROPIN_FILE" && ! -L "$SRS_DELIVERY_DROPIN_FILE" ]]; then echo_ok else echo_failed verification_ok=false fi for _service in \ "$POSTSRSD_SERVICE" \ "$SRS_REVERSE_MAP_SERVICE" \ "$SRS_DELIVERY_SERVICE" do echononl "Verify systemd service '${_service}' is inactive.." service_active "$_service" _service_rc=$? if [[ "$_service_rc" -eq 1 ]]; then echo_ok else echo_failed verification_ok=false fi echononl "Verify systemd service '${_service}' is disabled.." service_enabled "$_service" _service_rc=$? if [[ "$_service_rc" -eq 1 ]]; then echo_ok else echo_failed verification_ok=false fi done for _endpoint in \ "PostSRSd socketmap|${POSTSRSD_SOCKET_HOST}|${POSTSRSD_SOCKET_PORT}" \ "SRS reverse socketmap|${SRS_REVERSE_SOCKET_HOST}|${SRS_REVERSE_SOCKET_PORT}" \ "SRS reinjection listener|${SRS_REINJECT_HOST}|${SRS_REINJECT_PORT}" \ "SRS reverse listener|${SRS_REVERSE_HOST}|${SRS_REVERSE_PORT}" do IFS='|' read -r _label _host _port <<< "$_endpoint" echononl "Verify ${_label} is inactive.." port_is_listening "$_host" "$_port" _port_rc=$? if [[ "$_port_rc" -eq 1 ]]; then echo_ok else echo_failed verification_ok=false fi done echononl "Final Postfix configuration check.." if postfix check > "$log_file" 2>&1 ; then echo_ok else echo_failed verification_ok=false fi if ! $verification_ok ; then fatal_with_deactivation_rollback "Final selective SRS deactivation verification failed." fi # ---------- # Success # ---------- DEACTIVATION_CHANGES_STARTED=false blank_line info "Selective SRS was successfully deactivated." echo " Current selective SRS state....................: DISABLED" echo " Persistent state retained......................: ${SRS_STATE_FILE}" echo " Deactivation backup............................: ${DEACTIVATION_BACKUP_DIR}" echo "" clean_up 0