#!/usr/bin/env bash # ---------- # Selective SRS activation # # Activates selective SRS on a prepared mail server. # # The script is intentionally conservative: # # - ENABLED -> no changes, rc=0 # - DISABLED -> activate after explicit confirmation # - INCONSISTENT -> abort without changes # # Persistent pre-SRS state is read from: # # /etc/postfix-srs/state.conf # # That state file is never overwritten by this script. # ---------- # ---------- # 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 # ---------- # Activation runtime state # ---------- ACTIVATION_CHANGES_STARTED=false ACTIVATION_BACKUP_DIR="" POSTFIX_RELOADED=false POSTFIX_SRS_DROPIN_WAS_PRESENT=false SRS_DELIVERY_DROPIN_WAS_PRESENT=false declare -A SERVICE_WAS_ACTIVE declare -A SERVICE_WAS_ENABLED # ---------- # 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() { 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 } ' "$POSTFIX_MASTER_CF" } port_is_listening() { local host="$1" local port="$2" ss -ltnH 2>/dev/null | 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) } ' } service_exists() { systemctl cat "$1" > /dev/null 2>&1 } service_active() { systemctl is-active --quiet "$1" } service_enabled_state() { systemctl is-enabled "$1" 2>/dev/null || true } 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" } 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 } append_main_cf_managed_block() { local tmp tmp="$(mktemp "${POSTFIX_CONF_DIR}/.main.cf.srs.XXXXXX")" || return 1 if ! cp -a "$POSTFIX_MAIN_CF" "$tmp" ; then rm -f "$tmp" return 1 fi cat >> "$tmp" <> "$tmp" <<'EOF' # BEGIN OOPEN-SRS-MANAGED # ============================================================================ # Selective Sender Rewriting Scheme (SRS) # # This block is maintained by the SRS management scripts. # Do not rename or remove the BEGIN/END markers. # ============================================================================ srs-forward unix - n n - - pipe flags=Rq null_sender= user=nobody argv=/usr/local/sbin/srs-forward.py ${sender} ${recipient} ${original_recipient} ${queue_id} envid=${envid} # END OOPEN-SRS-MANAGED EOF if ! mv "$tmp" "$POSTFIX_MASTER_CF" ; then rm -f "$tmp" return 1 fi return 0 } snapshot_systemd_dropins() { if [[ -f "$POSTFIX_SRS_DROPIN_FILE" ]]; then POSTFIX_SRS_DROPIN_WAS_PRESENT=true cp -a "$POSTFIX_SRS_DROPIN_FILE" \ "${ACTIVATION_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" \ "${ACTIVATION_BACKUP_DIR}/postfix@srs-delivery.service-srs.conf" || return 1 fi return 0 } write_srs_systemd_dropins() { mkdir -p "$POSTFIX_SRS_DROPIN_DIR" "$SRS_DELIVERY_DROPIN_DIR" || return 1 cat > "$POSTFIX_SRS_DROPIN_FILE" < "$SRS_DELIVERY_DROPIN_FILE" < /dev/null 2>> "$log_file" || restore_failed=true else systemctl stop "$service" > /dev/null 2>> "$log_file" || restore_failed=true fi enabled_state="${SERVICE_WAS_ENABLED[$service]:-}" case "$enabled_state" in enabled|enabled-runtime) systemctl enable "$service" > /dev/null 2>> "$log_file" || restore_failed=true ;; disabled) systemctl disable "$service" > /dev/null 2>> "$log_file" || restore_failed=true ;; esac done ! $restore_failed } rollback_activation() { local rollback_failed=false if ! $ACTIVATION_CHANGES_STARTED ; then return 0 fi blank_line warn "SRS activation failed. Trying to restore the previous state." if [[ -n "$ACTIVATION_BACKUP_DIR" && -r "${ACTIVATION_BACKUP_DIR}/main.cf" && -r "${ACTIVATION_BACKUP_DIR}/master.cf" ]] then echononl "Restore '${POSTFIX_MAIN_CF}'.." if cp -a "${ACTIVATION_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 "${ACTIVATION_BACKUP_DIR}/master.cf" "$POSTFIX_MASTER_CF" > "$log_file" 2>&1 ; then echo_ok else echo_failed rollback_failed=true fi else error "Activation backup is incomplete. Cannot automatically restore Postfix configuration." 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 fi if $rollback_failed ; then error "Automatic rollback could not fully restore the previous state. Manual intervention is required." return 1 fi info "Previous state was successfully restored." return 0 } fatal_with_activation_rollback() { local message="$1" if $ACTIVATION_CHANGES_STARTED ; then if rollback_activation ; then ACTIVATION_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 "Activation interrupted by signal." if $ACTIVATION_CHANGES_STARTED ; then rollback_activation 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 \ rm \ python3 \ realpath \ sed \ ss \ stat \ systemctl load_srs_config load_srs_state 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 required installation files # ---------- 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 systemd services # ---------- blank_line echo -e "\033[37m\033[1mCheck selective SRS services..\033[m" blank_line services_exist=true services_active=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 continue fi if service_active "$_service" ; then echononl "Check systemd service '${_service}' runtime state (active).." echo_ok else echononl "Check systemd service '${_service}' runtime state (inactive).." echo_ok services_active=false fi done echononl "Check main Postfix service '${POSTFIX_SERVICE}' is active.." if service_active "$POSTFIX_SERVICE" ; then echo_ok else echo_failed services_active=false fi # ---------- # Check listeners # ---------- blank_line echo -e "\033[37m\033[1mCheck selective SRS listeners..\033[m" blank_line listeners_ok=true while IFS='|' read -r _label _host _port ; do if port_is_listening "$_host" "$_port" ; then echononl "Check ${_label} state on ${_host}:${_port} (listening).." echo_ok else echononl "Check ${_label} state on ${_host}:${_port} (inactive).." echo_ok listeners_ok=false fi done <}" echo "" if ! ask_yes_no "einverstanden" "no" ; then warn "Activation cancelled by user. Nothing was changed." clean_up 0 fi # ---------- # Prepare rollback # ---------- snapshot_service_states ACTIVATION_BACKUP_DIR="/root/postfix-srs-activation-${backup_date}" echononl "Create activation backup directory '${ACTIVATION_BACKUP_DIR}'.." if mkdir -m 0700 "$ACTIVATION_BACKUP_DIR" > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal "$(cat "$log_file")" fi echononl "Backup '${POSTFIX_MAIN_CF}'.." if cp -a "$POSTFIX_MAIN_CF" "${ACTIVATION_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" "${ACTIVATION_BACKUP_DIR}/master.cf" > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal "$(cat "$log_file")" fi echononl "Backup existing systemd SRS drop-ins.." if snapshot_systemd_dropins > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal "$(cat "$log_file")" fi ACTIVATION_CHANGES_STARTED=true # ---------- # Activate Postfix integration # ---------- blank_line echo -e "\033[37m\033[1mActivate selective SRS Postfix integration..\033[m" blank_line echononl "Add managed SRS block to '${POSTFIX_MAIN_CF}'.." if append_main_cf_managed_block > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_activation_rollback "Could not add managed SRS block to '${POSTFIX_MAIN_CF}'." fi echononl "Add managed SRS service block to '${POSTFIX_MASTER_CF}'.." if append_master_cf_managed_block > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_activation_rollback "Could not add managed SRS block to '${POSTFIX_MASTER_CF}'." fi echononl "Set port 25 receive_override_options.." if update_master_service_option \ smtp \ receive_override_options \ "$smtp_active_value" > "$log_file" 2>&1 then echo_ok else echo_failed fatal_with_activation_rollback "Could not update port 25 receive_override_options." fi echononl "Set AmaViS reinjection receive_override_options.." if update_master_service_option \ localhost:10025 \ receive_override_options \ "$amavis_active_value" > "$log_file" 2>&1 then echo_ok else echo_failed fatal_with_activation_rollback "Could not update AmaViS reinjection receive_override_options." fi echononl "Check Postfix configuration.." if postfix check > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_activation_rollback "Postfix configuration check failed. $(cat "$log_file")" fi # ---------- # Install systemd dependencies and start/enable SRS runtime services # ---------- blank_line echo -e "\033[37m\033[1mStart selective SRS runtime services..\033[m" blank_line echononl "Install systemd SRS dependency drop-ins.." if write_srs_systemd_dropins > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_activation_rollback "Could not install systemd SRS dependency drop-ins. $(cat "$log_file")" fi for _service in \ "$POSTSRSD_SERVICE" \ "$SRS_REVERSE_MAP_SERVICE" \ "$SRS_DELIVERY_SERVICE" do echononl "Enable systemd service '${_service}'.." if systemctl enable "$_service" > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_activation_rollback "Could not enable systemd service '${_service}'. $(cat "$log_file")" fi echononl "Start systemd service '${_service}'.." if systemctl start "$_service" > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal_with_activation_rollback "Could not start systemd service '${_service}'. $(cat "$log_file")" fi done # ---------- # Verify listeners before Postfix reload # ---------- 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} on ${_host}:${_port}.." if port_is_listening "$_host" "$_port" ; then echo_ok else echo_failed fatal_with_activation_rollback "${_label} is not listening on ${_host}:${_port}." fi done # ---------- # Reload main Postfix # ---------- blank_line echononl "Reload main Postfix service '${POSTFIX_SERVICE}'.." if systemctl reload "$POSTFIX_SERVICE" > "$log_file" 2>&1 ; then POSTFIX_RELOADED=true echo_ok else echo_failed fatal_with_activation_rollback "Could not reload '${POSTFIX_SERVICE}'. $(cat "$log_file")" fi # ---------- # Final verification # ---------- blank_line echo -e "\033[37m\033[1mVerify activated selective SRS configuration..\033[m" blank_line verification_ok=true echononl "Verify managed main.cf block.." if check_managed_block_markers \ "$POSTFIX_MAIN_CF" \ "$SRS_MAIN_CF_BEGIN_MARKER" \ "$SRS_MAIN_CF_END_MARKER" then echo_ok else echo_failed verification_ok=false fi echononl "Verify managed master.cf block.." if check_managed_block_markers \ "$POSTFIX_MASTER_CF" \ "$SRS_MASTER_CF_BEGIN_MARKER" \ "$SRS_MASTER_CF_END_MARKER" then echo_ok else echo_failed verification_ok=false fi echononl "Verify 'default_transport = srs-forward'.." if [[ "$(main_cf_explicit_value default_transport)" == "srs-forward" ]]; then echo_ok else echo_failed verification_ok=false fi echononl "Verify SRS recipient canonical map.." if [[ "$(main_cf_explicit_value recipient_canonical_maps)" == \ "socketmap:inet:${SRS_REVERSE_SOCKET_HOST}:${SRS_REVERSE_SOCKET_PORT}:reverse" ]] then echo_ok else echo_failed verification_ok=false fi echononl "Verify 'srs-forward' pipe service.." if master_srs_forward_present ; then echo_ok else echo_failed verification_ok=false fi echononl "Verify port 25 receive_override_options.." if [[ "$(master_service_option_value smtp receive_override_options)" == "$smtp_active_value" ]]; then echo_ok else echo_failed verification_ok=false fi echononl "Verify AmaViS reinjection receive_override_options.." if [[ "$(master_service_option_value localhost:10025 receive_override_options)" == "$amavis_active_value" ]]; 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 active.." if service_active "$_service" ; 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_activation_rollback "Final selective SRS verification failed." fi # ---------- # Success # ---------- ACTIVATION_CHANGES_STARTED=false blank_line info "Selective SRS was successfully activated." echo " Current selective SRS state....................: ENABLED" echo " Activation backup..............................: ${ACTIVATION_BACKUP_DIR}" echo "" clean_up 0