Extend selective SRS management and add installer tests
This commit is contained in:
+82
-1
@@ -191,6 +191,45 @@ postsrsd_srs_domain() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upgrade an already running instance without touching binaries/systemd units.
|
||||||
|
# A new snapshot is taken on every attempt and restored on any failure.
|
||||||
|
upgrade_existing_srs_aliases() {
|
||||||
|
local action="$1" folder snapshot
|
||||||
|
folder="/root/postfix-srs-alias-upgrade-${backup_date}"
|
||||||
|
snapshot="${folder}/main.cf"
|
||||||
|
mkdir -m 0700 "$folder" || fatal "Cannot create alias upgrade backup directory."
|
||||||
|
cp -a "$POSTFIX_MAIN_CF" "$snapshot" || fatal "Cannot snapshot main.cf for alias upgrade."
|
||||||
|
echononl "Track incoming SRS bounce aliases (${action}).."
|
||||||
|
if srs_alias_tool "$action" > "$log_file" 2>&1 &&
|
||||||
|
postfix check >> "$log_file" 2>&1 &&
|
||||||
|
[[ "$(srs_alias_tool status 2>>"$log_file")" == 'managed' ]] &&
|
||||||
|
{ [[ "$action" == 'adopt' ]] || systemctl reload "$POSTFIX_SERVICE" >> "$log_file" 2>&1; }
|
||||||
|
then
|
||||||
|
echo_ok
|
||||||
|
info "SRS inbound alias settings now managed. Backup: ${snapshot}"
|
||||||
|
clean_up 0
|
||||||
|
fi
|
||||||
|
echo_failed
|
||||||
|
error "Alias upgrade failed: $(cat "$log_file")"
|
||||||
|
# A pure adoption changes only a comment. Never reload Postfix on
|
||||||
|
# either the success or rollback path for that operation.
|
||||||
|
if cp -a "$snapshot" "$POSTFIX_MAIN_CF" &&
|
||||||
|
postfix check > "$log_file" 2>&1 &&
|
||||||
|
{ [[ "$action" == 'adopt' ]] || systemctl reload "$POSTFIX_SERVICE" >> "$log_file" 2>&1; }
|
||||||
|
then
|
||||||
|
fatal "Alias upgrade failed; previous main.cf restored successfully."
|
||||||
|
fi
|
||||||
|
fatal "Alias upgrade failed; automatic restore incomplete. Manual review: ${snapshot}"
|
||||||
|
}
|
||||||
|
|
||||||
state_main_cf_matches() {
|
state_main_cf_matches() {
|
||||||
local parameter="$1"
|
local parameter="$1"
|
||||||
local was_set="$2"
|
local was_set="$2"
|
||||||
@@ -747,6 +786,8 @@ check_required_commands \
|
|||||||
|
|
||||||
load_srs_config
|
load_srs_config
|
||||||
load_srs_state
|
load_srs_state
|
||||||
|
[[ -f "$SRS_ALIAS_HELPER" && ! -L "$SRS_ALIAS_HELPER" ]] ||
|
||||||
|
fatal "Missing regular SRS alias helper: ${SRS_ALIAS_HELPER}"
|
||||||
|
|
||||||
smtp_pre_value=""
|
smtp_pre_value=""
|
||||||
amavis_pre_value=""
|
amavis_pre_value=""
|
||||||
@@ -1255,9 +1296,31 @@ blank_line
|
|||||||
# Handle detected state
|
# Handle detected state
|
||||||
# ----------
|
# ----------
|
||||||
|
|
||||||
|
# A running v1 installation can be upgraded without reinstalling PostSRSd.
|
||||||
|
srs_alias_status="$(srs_alias_tool status 2>"$log_file")" ||
|
||||||
|
fatal "Unsafe virtual alias configuration: $(cat "$log_file")"
|
||||||
|
|
||||||
if [[ "$srs_state" == "enabled" ]]; then
|
if [[ "$srs_state" == "enabled" ]]; then
|
||||||
info "Selective SRS is already enabled. Nothing to do."
|
case "$srs_alias_status" in
|
||||||
|
managed)
|
||||||
|
info "Selective SRS with inbound bounce reception is already enabled. Nothing to do."
|
||||||
clean_up 0
|
clean_up 0
|
||||||
|
;;
|
||||||
|
absent|legacy)
|
||||||
|
warn "SRS is enabled but bounce alias ownership is not tracked (${srs_alias_status})."
|
||||||
|
info "Only virtual alias parameters will be updated; PostSRSd binaries and services remain untouched."
|
||||||
|
if ! ask_yes_no "Upgrade incoming SRS alias management" "no"; then
|
||||||
|
info "Upgrade cancelled without changes."
|
||||||
|
clean_up 0
|
||||||
|
fi
|
||||||
|
if [[ "$srs_alias_status" == 'legacy' ]]; then
|
||||||
|
upgrade_existing_srs_aliases adopt
|
||||||
|
else
|
||||||
|
upgrade_existing_srs_aliases add
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*) fatal "Unexpected SRS alias status: ${srs_alias_status}" ;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$srs_state" == "inconsistent" ]]; then
|
if [[ "$srs_state" == "inconsistent" ]]; then
|
||||||
@@ -1268,6 +1331,8 @@ fi
|
|||||||
if ! $activation_ready ; then
|
if ! $activation_ready ; then
|
||||||
fatal "Selective SRS is disabled, but the installation is incomplete. Activation is not possible."
|
fatal "Selective SRS is disabled, but the installation is incomplete. Activation is not possible."
|
||||||
fi
|
fi
|
||||||
|
[[ "$srs_alias_status" == 'absent' ]] ||
|
||||||
|
fatal "SRS disabled but alias entries are not absent. Refusing activation."
|
||||||
|
|
||||||
|
|
||||||
# ----------
|
# ----------
|
||||||
@@ -1362,6 +1427,14 @@ else
|
|||||||
fatal_with_activation_rollback "Could not add managed SRS block to '${POSTFIX_MASTER_CF}'."
|
fatal_with_activation_rollback "Could not add managed SRS block to '${POSTFIX_MASTER_CF}'."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echononl "Add incoming SRS virtual alias domain and reverse map.."
|
||||||
|
if srs_alias_tool add > "$log_file" 2>&1 ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
fatal_with_activation_rollback "Could not add tracked virtual alias items: $(cat "$log_file")"
|
||||||
|
fi
|
||||||
|
|
||||||
echononl "Set port 25 receive_override_options.."
|
echononl "Set port 25 receive_override_options.."
|
||||||
if update_master_service_option \
|
if update_master_service_option \
|
||||||
smtp \
|
smtp \
|
||||||
@@ -1527,6 +1600,14 @@ else
|
|||||||
verification_ok=false
|
verification_ok=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echononl "Verify incoming SRS alias ownership.."
|
||||||
|
if [[ "$(srs_alias_tool status 2>"$log_file")" == 'managed' ]]; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
verification_ok=false
|
||||||
|
fi
|
||||||
|
|
||||||
echononl "Verify 'srs-forward' pipe service.."
|
echononl "Verify 'srs-forward' pipe service.."
|
||||||
if master_srs_forward_present ; then
|
if master_srs_forward_present ; then
|
||||||
echo_ok
|
echo_ok
|
||||||
|
|||||||
@@ -203,6 +203,14 @@ postsrsd_srs_domain() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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() {
|
state_main_cf_matches() {
|
||||||
local parameter="$1"
|
local parameter="$1"
|
||||||
local was_set="$2"
|
local was_set="$2"
|
||||||
@@ -919,6 +927,8 @@ check_required_commands \
|
|||||||
|
|
||||||
load_srs_config
|
load_srs_config
|
||||||
load_srs_state
|
load_srs_state
|
||||||
|
[[ -f "$SRS_ALIAS_HELPER" && ! -L "$SRS_ALIAS_HELPER" ]] ||
|
||||||
|
fatal "Missing regular SRS alias helper: ${SRS_ALIAS_HELPER}"
|
||||||
|
|
||||||
|
|
||||||
# ----------
|
# ----------
|
||||||
@@ -1412,7 +1422,12 @@ blank_line
|
|||||||
# Handle detected state
|
# Handle detected state
|
||||||
# ----------
|
# ----------
|
||||||
|
|
||||||
|
srs_alias_status="$(srs_alias_tool status 2>"$log_file")" ||
|
||||||
|
fatal "Unsafe virtual alias configuration: $(cat "$log_file")"
|
||||||
|
|
||||||
if [[ "$srs_state" == "disabled" ]]; then
|
if [[ "$srs_state" == "disabled" ]]; then
|
||||||
|
[[ "$srs_alias_status" == 'absent' ]] ||
|
||||||
|
fatal "SRS marked disabled but virtual alias items remain (${srs_alias_status})."
|
||||||
info "Selective SRS is already disabled. Nothing to do."
|
info "Selective SRS is already disabled. Nothing to do."
|
||||||
clean_up 0
|
clean_up 0
|
||||||
fi
|
fi
|
||||||
@@ -1425,6 +1440,11 @@ fi
|
|||||||
if ! $installation_ok || ! $services_exist ; then
|
if ! $installation_ok || ! $services_exist ; then
|
||||||
fatal "Selective SRS is enabled, but the installation is incomplete. Deactivation was not attempted."
|
fatal "Selective SRS is enabled, but the installation is incomplete. Deactivation was not attempted."
|
||||||
fi
|
fi
|
||||||
|
if [[ "$srs_alias_status" == 'legacy' ]]; then
|
||||||
|
fatal "SRS virtual alias settings are not tracked. Run the revised activate-srs.sh once to adopt them first."
|
||||||
|
fi
|
||||||
|
[[ "$srs_alias_status" == 'managed' || "$srs_alias_status" == 'absent' ]] ||
|
||||||
|
fatal "Unsafe SRS alias status: ${srs_alias_status}"
|
||||||
|
|
||||||
|
|
||||||
# ----------
|
# ----------
|
||||||
@@ -1583,6 +1603,19 @@ else
|
|||||||
fi
|
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 master.cf
|
# Restore master.cf
|
||||||
# ----------
|
# ----------
|
||||||
@@ -1782,6 +1815,14 @@ else
|
|||||||
verification_ok=false
|
verification_ok=false
|
||||||
fi
|
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.."
|
echononl "Verify pre-SRS master.cf state.."
|
||||||
if state_master_option_matches \
|
if state_master_option_matches \
|
||||||
smtp \
|
smtp \
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# See /usr/share/postfix/main.cf.dist for a commented, more complete version
|
||||||
|
|
||||||
|
# See http://www.postfix.org/COMPATIBILITY_README.html
|
||||||
|
compatibility_level = 3.9
|
||||||
|
|
||||||
|
# Which domain that locally-originated mail appears to come from.
|
||||||
|
# Debian policy suggests to read this value from /etc/mailname.
|
||||||
|
#XX needs a review in postinst&config
|
||||||
|
#myorigin = /etc/mailname
|
||||||
|
#myorigin = $mydomain
|
||||||
|
myorigin = @MYORIGIN@
|
||||||
|
|
||||||
|
# Text that follows the 220 code in the SMTP server's greeting banner.
|
||||||
|
# You MUST specify $myhostname at the start due to an RFC requirement.
|
||||||
|
smtpd_banner = $myhostname ESMTP $mail_name (Debian)
|
||||||
|
|
||||||
|
# IP protocols to use: ipv4, ipv6, or all
|
||||||
|
# (set this explicitly so `post-install upgrade-configuration' wont complain)
|
||||||
|
inet_protocols = all
|
||||||
|
|
||||||
|
# List of "trusted" SMTP clients (maptype:mapname allowed) that have more
|
||||||
|
# privileges than "strangers". If mynetworks is not specified (the default),
|
||||||
|
# mynetworks_style is used to compute its value.
|
||||||
|
#mynetworks_style = class
|
||||||
|
#mynetworks_style = subnet
|
||||||
|
mynetworks_style = host
|
||||||
|
#
|
||||||
|
mynetworks = 127.0.0.0/8
|
||||||
|
|
||||||
|
# Uncomment the next line to generate "delayed mail" warnings
|
||||||
|
#delay_warning_time = 4h
|
||||||
|
|
||||||
|
# List of domains (maptype:mapname allowed) that this machine considers
|
||||||
|
# itself the final destination for.
|
||||||
|
mydestination = @MYDESTINATION@
|
||||||
|
|
||||||
|
# Maximum size of a user mailbox
|
||||||
|
mailbox_size_limit = 51200000
|
||||||
|
|
||||||
|
# Optional external command to use instead of mailbox delivery. If set,
|
||||||
|
# you must set up an alias to forward root mail to a real user.
|
||||||
|
#mailbox_command = /usr/bin/procmail
|
||||||
|
#mailbox_command = /usr/bin/procmail -a "$EXTENSION"
|
||||||
|
mailbox_command =
|
||||||
|
|
||||||
|
# List of alias maps to use to lookup local addresses.
|
||||||
|
# Per Debian Policy it should be /etc/aliases.
|
||||||
|
alias_maps = $alias_database, nis:mail.aliases
|
||||||
|
|
||||||
|
# List of alias maps to make indexes on, when running newaliases.
|
||||||
|
alias_database = hash:/etc/aliases
|
||||||
|
|
||||||
|
# Notify (or not) local biff service when new mail arrives.
|
||||||
|
# Rarely used these days.
|
||||||
|
biff = no
|
||||||
|
|
||||||
|
# Separator between user name and address extension (user+foo@domain)
|
||||||
|
#recipient_delimiter = +
|
||||||
|
recipient_delimiter =
|
||||||
|
|
||||||
|
# A host to send "other" mail to
|
||||||
|
#relayhost = $mydomain
|
||||||
|
#relayhost = [gateway.example.com]
|
||||||
|
#relayhost = [ip.add.re.ss]:port
|
||||||
|
#relayhost = uucphost
|
||||||
|
relayhost =
|
||||||
|
|
||||||
|
# Where to look for Cyrus SASL configuration files. Upstream default is unset
|
||||||
|
# (use compiled-in SASL library default), Debian Policy says it should be
|
||||||
|
# /etc/postfix/sasl.
|
||||||
|
cyrus_sasl_config_path = /etc/postfix/sasl
|
||||||
|
|
||||||
|
authorized_submit_users = root
|
||||||
|
data_directory = /var/lib/postfix-srs-delivery
|
||||||
|
html_directory = /usr/share/doc/postfix/html
|
||||||
|
master_service_disable =
|
||||||
|
queue_directory = /var/spool/postfix-srs-delivery
|
||||||
|
readme_directory = /usr/share/doc/postfix
|
||||||
|
multi_instance_group = srs-delivery
|
||||||
|
multi_instance_name = postfix-srs-delivery
|
||||||
|
inet_interfaces = loopback-only
|
||||||
|
default_transport = smtp
|
||||||
|
local_transport = error:SRS-LOCAL-DELIVERY-BLOCKED
|
||||||
|
mailbox_transport = error:SRS-MAILBOX-DELIVERY-BLOCKED
|
||||||
|
relay_transport = error:SRS-RELAY-DELIVERY-BLOCKED
|
||||||
|
virtual_transport = srs-lmtp:unix:/var/spool/postfix/private/dovecot-lmtp
|
||||||
|
virtual_alias_maps = @VIRTUAL_ALIAS_MAPS@
|
||||||
|
virtual_alias_domains = @VIRTUAL_ALIAS_DOMAINS@
|
||||||
|
virtual_mailbox_maps = @VIRTUAL_MAILBOX_MAPS@
|
||||||
|
virtual_mailbox_domains = @VIRTUAL_MAILBOX_DOMAINS@
|
||||||
|
relay_domains = @RELAY_DOMAINS@
|
||||||
|
transport_maps = btree:/etc/postfix-srs/delivery/transport
|
||||||
|
mydomain = @MYDOMAIN@
|
||||||
|
myhostname = @MYHOSTNAME@
|
||||||
|
2bounce_notice_recipient = postmaster
|
||||||
|
bounce_notice_recipient = postmaster
|
||||||
|
delay_notice_recipient = postmaster
|
||||||
|
error_notice_recipient = postmaster
|
||||||
|
notify_classes =
|
||||||
|
smtp_bind_address = @SMTP_BIND_ADDRESS@
|
||||||
|
smtp_bind_address6 = @SMTP_BIND_ADDRESS6@
|
||||||
|
smtp_dns_support_level = @SMTP_DNS_SUPPORT_LEVEL@
|
||||||
|
smtp_host_lookup = @SMTP_HOST_LOOKUP@
|
||||||
|
smtp_tls_CAfile = @SMTP_TLS_CAFILE@
|
||||||
|
smtp_tls_mandatory_protocols = @SMTP_TLS_MANDATORY_PROTOCOLS@
|
||||||
|
smtp_tls_protocols = @SMTP_TLS_PROTOCOLS@
|
||||||
|
smtp_tls_security_level = @SMTP_TLS_SECURITY_LEVEL@
|
||||||
|
config_directory = /etc/postfix-srs/delivery
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
#
|
||||||
|
# Postfix master process configuration file. For details on the format
|
||||||
|
# of the file, see the master(5) manual page (command: "man 5 master" or
|
||||||
|
# on-line: https://www.postfix.org/master.5.html).
|
||||||
|
#
|
||||||
|
# Do not forget to execute "postfix reload" after editing this file.
|
||||||
|
#
|
||||||
|
# ==========================================================================
|
||||||
|
# service type private unpriv chroot wakeup maxproc command + args
|
||||||
|
# (yes) (yes) (no) (never) (100)
|
||||||
|
# ==========================================================================
|
||||||
|
#smtp inet n - y - - smtpd
|
||||||
|
#smtp inet n - y - 1 postscreen
|
||||||
|
#smtpd pass - - y - - smtpd
|
||||||
|
#dnsblog unix - - y - 0 dnsblog
|
||||||
|
#tlsproxy unix - - y - 0 tlsproxy
|
||||||
|
# Choose one: enable submission for loopback clients only, or for any client.
|
||||||
|
#127.0.0.1:submission inet n - y - - smtpd
|
||||||
|
#submission inet n - y - - smtpd
|
||||||
|
# -o syslog_name=postfix/submission
|
||||||
|
# -o smtpd_forbid_unauth_pipelining=no
|
||||||
|
# -o smtpd_tls_security_level=encrypt
|
||||||
|
# -o smtpd_sasl_auth_enable=yes
|
||||||
|
# -o smtpd_tls_auth_only=yes
|
||||||
|
# -o local_header_rewrite_clients=static:all
|
||||||
|
# -o smtpd_hide_client_session=yes
|
||||||
|
# -o smtpd_reject_unlisted_recipient=no
|
||||||
|
# Instead of specifying complex smtpd_<xxx>_restrictions here,
|
||||||
|
# specify "smtpd_<xxx>_restrictions=$mua_<xxx>_restrictions"
|
||||||
|
# here, and specify mua_<xxx>_restrictions in main.cf (where
|
||||||
|
# "<xxx>" is "client", "helo", "sender", "relay", or "recipient").
|
||||||
|
# -o smtpd_client_restrictions=
|
||||||
|
# -o smtpd_helo_restrictions=
|
||||||
|
# -o smtpd_sender_restrictions=
|
||||||
|
# -o smtpd_relay_restrictions=
|
||||||
|
# -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
|
||||||
|
# -o milter_macro_daemon_name=ORIGINATING
|
||||||
|
# Choose one: enable submissions for loopback clients only, or for any client.
|
||||||
|
#127.0.0.1:submissions inet n - y - - smtpd
|
||||||
|
#submissions inet n - y - - smtpd
|
||||||
|
# -o syslog_name=postfix/submissions
|
||||||
|
# -o smtpd_forbid_unauth_pipelining=no
|
||||||
|
# -o smtpd_tls_wrappermode=yes
|
||||||
|
# -o smtpd_sasl_auth_enable=yes
|
||||||
|
# -o local_header_rewrite_clients=static:all
|
||||||
|
# -o smtpd_hide_client_session=yes
|
||||||
|
# -o smtpd_reject_unlisted_recipient=no
|
||||||
|
# Instead of specifying complex smtpd_<xxx>_restrictions here,
|
||||||
|
# specify "smtpd_<xxx>_restrictions=$mua_<xxx>_restrictions"
|
||||||
|
# here, and specify mua_<xxx>_restrictions in main.cf (where
|
||||||
|
# "<xxx>" is "client", "helo", "sender", "relay", or "recipient").
|
||||||
|
# -o smtpd_client_restrictions=
|
||||||
|
# -o smtpd_helo_restrictions=
|
||||||
|
# -o smtpd_sender_restrictions=
|
||||||
|
# -o smtpd_relay_restrictions=
|
||||||
|
# -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
|
||||||
|
# -o milter_macro_daemon_name=ORIGINATING
|
||||||
|
#628 inet n - y - - qmqpd
|
||||||
|
pickup unix n - y 60 1 pickup
|
||||||
|
cleanup unix n - y - 0 cleanup
|
||||||
|
qmgr unix n - n 300 1 qmgr
|
||||||
|
#qmgr unix n - n 300 1 oqmgr
|
||||||
|
tlsmgr unix - - y 1000? 1 tlsmgr
|
||||||
|
rewrite unix - - y - - trivial-rewrite
|
||||||
|
bounce unix - - y - 0 bounce
|
||||||
|
defer unix - - y - 0 bounce
|
||||||
|
trace unix - - y - 0 bounce
|
||||||
|
verify unix - - y - 1 verify
|
||||||
|
flush unix n - y 1000? 0 flush
|
||||||
|
proxymap unix - - n - - proxymap
|
||||||
|
proxywrite unix - - n - 1 proxymap
|
||||||
|
#smtp unix - - y - - smtp
|
||||||
|
#relay unix - - y - - smtp
|
||||||
|
# -o syslog_name=${multi_instance_name?{$multi_instance_name}:{postfix}}/$service_name
|
||||||
|
# -o smtp_helo_timeout=5 -o smtp_connect_timeout=5
|
||||||
|
showq unix n - y - - showq
|
||||||
|
error unix - - y - - error
|
||||||
|
retry unix - - y - - error
|
||||||
|
discard unix - - y - - discard
|
||||||
|
#local unix - n n - - local
|
||||||
|
#virtual unix - n n - - virtual
|
||||||
|
#lmtp unix - - y - - lmtp
|
||||||
|
anvil unix - - y - 1 anvil
|
||||||
|
scache unix - - y - 1 scache
|
||||||
|
postlog unix-dgram n - n - 1 postlogd
|
||||||
|
#
|
||||||
|
# ====================================================================
|
||||||
|
# Interfaces to non-Postfix software. Be sure to examine the manual
|
||||||
|
# pages of the non-Postfix software to find out what options it wants.
|
||||||
|
#
|
||||||
|
# Many of the following services use the Postfix pipe(8) delivery
|
||||||
|
# agent. See the pipe(8) man page for information about ${recipient}
|
||||||
|
# and other message envelope options.
|
||||||
|
# ====================================================================
|
||||||
|
#
|
||||||
|
# maildrop. See the Postfix MAILDROP_README file for details.
|
||||||
|
# Also specify in main.cf: maildrop_destination_recipient_limit=1
|
||||||
|
#
|
||||||
|
#maildrop unix - n n - - pipe
|
||||||
|
# flags=DRXhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
|
||||||
|
#
|
||||||
|
# ====================================================================
|
||||||
|
#
|
||||||
|
# Recent Cyrus versions can use the existing "lmtp" master.cf entry.
|
||||||
|
#
|
||||||
|
# Specify in cyrus.conf:
|
||||||
|
# lmtp cmd="lmtpd -a" listen="localhost:lmtp" proto=tcp4
|
||||||
|
#
|
||||||
|
# Specify in main.cf one or more of the following:
|
||||||
|
# mailbox_transport = lmtp:inet:localhost
|
||||||
|
# virtual_transport = lmtp:inet:localhost
|
||||||
|
#
|
||||||
|
# ====================================================================
|
||||||
|
#
|
||||||
|
# Cyrus 2.1.5 (Amos Gouaux)
|
||||||
|
# Also specify in main.cf: cyrus_destination_recipient_limit=1
|
||||||
|
#
|
||||||
|
#cyrus unix - n n - - pipe
|
||||||
|
# flags=DRX user=cyrus argv=/cyrus/bin/deliver -e -r ${sender} -m ${extension} ${user}
|
||||||
|
#
|
||||||
|
# ====================================================================
|
||||||
|
#
|
||||||
|
# Old example of delivery via Cyrus.
|
||||||
|
#
|
||||||
|
#old-cyrus unix - n n - - pipe
|
||||||
|
# flags=R user=cyrus argv=/cyrus/bin/deliver -e -m ${extension} ${user}
|
||||||
|
#
|
||||||
|
# ====================================================================
|
||||||
|
#
|
||||||
|
# See the Postfix UUCP_README file for configuration details.
|
||||||
|
#
|
||||||
|
#uucp unix - n n - - pipe
|
||||||
|
# flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient)
|
||||||
|
#
|
||||||
|
# ====================================================================
|
||||||
|
#
|
||||||
|
# Other external delivery methods.
|
||||||
|
#
|
||||||
|
#ifmail unix - n n - - pipe
|
||||||
|
# flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r $nexthop ($recipient)
|
||||||
|
#
|
||||||
|
#bsmtp unix - n n - - pipe
|
||||||
|
# flags=Fq. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -f $sender $nexthop $recipient
|
||||||
|
#
|
||||||
|
#scalemail-backend unix - n n - 2 pipe
|
||||||
|
# flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store
|
||||||
|
# ${nexthop} ${user} ${extension}
|
||||||
|
#
|
||||||
|
#mailman unix - n n - - pipe
|
||||||
|
# flags=FRX user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py
|
||||||
|
# ${nexthop} ${user}
|
||||||
|
|
||||||
|
# SRS reinjection - loopback only, no alias expansion
|
||||||
|
127.0.0.1:10028 inet n - y - - smtpd
|
||||||
|
-o syslog_name=postfix-srs-delivery/reinject
|
||||||
|
-o receive_override_options=no_address_mappings,no_unknown_recipient_checks
|
||||||
|
-o smtpd_recipient_restrictions=permit_mynetworks,reject
|
||||||
|
-o mynetworks=127.0.0.0/8
|
||||||
|
|
||||||
|
# SRS reverse cleanup - envelope recipients only, no virtual alias expansion
|
||||||
|
srs-reverse-cleanup unix n - y - 0 cleanup
|
||||||
|
-o syslog_name=postfix-srs-delivery/reverse-cleanup
|
||||||
|
-o recipient_canonical_maps=socketmap:inet:127.0.0.1:10003:reverse
|
||||||
|
-o recipient_canonical_classes=envelope_recipient
|
||||||
|
-o virtual_alias_maps=
|
||||||
|
-o canonical_maps=
|
||||||
|
-o sender_canonical_maps=
|
||||||
|
|
||||||
|
# SRS reverse listener - loopback only
|
||||||
|
|
||||||
|
# SRS reverse recipient verification policy
|
||||||
|
127.0.0.1:10029 inet n - y - - smtpd
|
||||||
|
-o syslog_name=postfix-srs-delivery/reverse-smtpd
|
||||||
|
-o cleanup_service_name=srs-reverse-cleanup
|
||||||
|
-o content_filter=
|
||||||
|
-o smtpd_milters=
|
||||||
|
-o mynetworks=127.0.0.0/8
|
||||||
|
-o smtpd_relay_restrictions=permit_mynetworks,reject
|
||||||
|
-o smtpd_recipient_restrictions=check_policy_service,unix:private/srs-reverse-policy,reject
|
||||||
|
-o smtpd_reject_unlisted_recipient=no
|
||||||
|
srs-reverse-policy unix - n n - 0 spawn
|
||||||
|
user=nobody argv=/usr/local/sbin/srs-reverse-policy.py
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# LMTP delivery to productive Dovecot
|
||||||
|
# =============================================================
|
||||||
|
srs-lmtp unix - - n - - lmtp
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Outbound SMTP delivery
|
||||||
|
# =============================================================
|
||||||
|
smtp unix - - n - - smtp
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Internal SMTP transport for SRS reverse processing
|
||||||
|
# =============================================================
|
||||||
|
srs-reverse-smtp unix - - n - - smtp
|
||||||
|
-o smtp_bind_address=127.0.0.1
|
||||||
|
-o smtp_bind_address6=::1
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=PostSRSd SRS Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/postsrsd/sbin/postsrsd -C /etc/postsrsd-srs/postsrsd.conf
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
domains = {}
|
||||||
|
srs-domain = "@SRS_DOMAIN@"
|
||||||
|
socketmap = inet:127.0.0.1:10003
|
||||||
|
secrets-file = "/etc/postsrsd-srs/secret"
|
||||||
|
milter-rewrite-local = off
|
||||||
|
original-envelope = embedded
|
||||||
|
debug = off
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Domain-restricted SRS reverse socketmap
|
||||||
|
After=network.target postsrsd-srs.service
|
||||||
|
Wants=postsrsd-srs.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=nobody
|
||||||
|
Group=nogroup
|
||||||
|
ExecStart=/usr/bin/python3 /usr/local/sbin/srs-reverse-map.py
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# SRS-Bounces zurück an den produktiven Postfix
|
||||||
|
@SRS_DOMAIN@ srs-reverse-smtp:[127.0.0.1]:10029
|
||||||
Regular → Executable
+3
@@ -34,6 +34,9 @@ POSTFIX_MASTER_CF="${POSTFIX_CONF_DIR}/master.cf"
|
|||||||
POSTFIX_SRS_DIR="/etc/postfix-srs"
|
POSTFIX_SRS_DIR="/etc/postfix-srs"
|
||||||
POSTFIX_SRS_DELIVERY_DIR="${POSTFIX_SRS_DIR}/delivery"
|
POSTFIX_SRS_DELIVERY_DIR="${POSTFIX_SRS_DIR}/delivery"
|
||||||
SRS_STATE_FILE="${POSTFIX_SRS_DIR}/state.conf"
|
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_DIR="/etc/postsrsd-srs"
|
||||||
POSTSRSD_CONF_FILE="${POSTSRSD_CONF_DIR}/postsrsd.conf"
|
POSTSRSD_CONF_FILE="${POSTSRSD_CONF_DIR}/postsrsd.conf"
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Read-only, fail-closed public-DNS preflight for the approved SRS DNS profile.
|
||||||
|
|
||||||
|
Checks the authoritative nameservers, bypassing local hosts/systemd-resolved.
|
||||||
|
This deliberately validates only the approved single-host DNS profile:
|
||||||
|
exactly one MX pointing to this host and one SPF record `v=spf1 mx -all`.
|
||||||
|
It is NOT a general-purpose SPF evaluator.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import ipaddress
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# A public resolver is used only to discover the delegated authoritative NS and
|
||||||
|
# their numeric IPs. The actual TXT/MX/A/AAAA values are read from each NS.
|
||||||
|
BOOTSTRAP_RESOLVERS = ('1.1.1.1', '9.9.9.9')
|
||||||
|
DNS_TIMEOUT = 5
|
||||||
|
|
||||||
|
|
||||||
|
def dig(server, name, kind, authoritative=False):
|
||||||
|
command = ['dig', '@' + server, '+time=2', '+tries=1', '+noall', '+comments', '+answer']
|
||||||
|
if authoritative:
|
||||||
|
command.append('+norecurse')
|
||||||
|
command.extend([name, kind])
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(command, capture_output=True, text=True, timeout=DNS_TIMEOUT, check=False)
|
||||||
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||||
|
raise ValueError(f'DNS request failed ({server}, {name}, {kind}): {exc}') from exc
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise ValueError(f'DNS request failed ({server}, {name}, {kind}): {proc.stderr.strip() or proc.returncode}')
|
||||||
|
header = re.search(r'HEADER.*?status:\s*(\w+)', proc.stdout)
|
||||||
|
if not header or header.group(1) != 'NOERROR':
|
||||||
|
raise ValueError(f'DNS returned {header.group(1) if header else "no status"}: {server} {name} {kind}')
|
||||||
|
flags = re.search(r'^;; flags:\s*(.*?);\s*QUERY:', proc.stdout, re.M)
|
||||||
|
if authoritative and (not flags or 'aa' not in flags.group(1).split()):
|
||||||
|
raise ValueError(f'Authoritative response missing AA flag: {server} {name} {kind}')
|
||||||
|
rows = []
|
||||||
|
for line in proc.stdout.splitlines():
|
||||||
|
parts = line.split(None, 4)
|
||||||
|
if len(parts) != 5 or parts[2].upper() != 'IN':
|
||||||
|
continue
|
||||||
|
owner, _ttl, _cls, rr_type, data = parts
|
||||||
|
if rr_type.upper() == kind.upper() and owner.rstrip('.').lower() == name.rstrip('.').lower():
|
||||||
|
rows.append(data.strip())
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def public_lookup(name, kind):
|
||||||
|
failures = []
|
||||||
|
for server in BOOTSTRAP_RESOLVERS:
|
||||||
|
try:
|
||||||
|
return dig(server, name, kind)
|
||||||
|
except ValueError as exc:
|
||||||
|
failures.append(str(exc))
|
||||||
|
raise ValueError('Public NS bootstrap failed: ' + ' | '.join(failures))
|
||||||
|
|
||||||
|
|
||||||
|
def discover_nameservers(domain):
|
||||||
|
labels = domain.rstrip('.').split('.')
|
||||||
|
# Pick closest delegated zone. A zone without its own delegation has no NS.
|
||||||
|
for i in range(len(labels) - 1):
|
||||||
|
zone = '.'.join(labels[i:])
|
||||||
|
ns = public_lookup(zone, 'NS')
|
||||||
|
if ns:
|
||||||
|
names = sorted({n.rstrip('.').lower() for n in ns})
|
||||||
|
return zone, names
|
||||||
|
raise ValueError(f'Cannot discover authoritative nameservers for {domain}')
|
||||||
|
|
||||||
|
|
||||||
|
def addr_of_ns(ns):
|
||||||
|
for kind in ('A', 'AAAA'):
|
||||||
|
for value in public_lookup(ns, kind):
|
||||||
|
try:
|
||||||
|
return str(ipaddress.ip_address(value))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
raise ValueError(f'Cannot resolve nameserver {ns} to a public IP address')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_txt(value):
|
||||||
|
# DNS may split a single TXT resource record into multiple quoted strings.
|
||||||
|
parts = re.findall(r'"((?:\\.|[^"\\])*)"', value)
|
||||||
|
if not parts:
|
||||||
|
raise ValueError('Unexpected TXT presentation format: ' + value)
|
||||||
|
return ''.join(parts).replace('\\"', '"')
|
||||||
|
|
||||||
|
|
||||||
|
def check_server(ns_name, ns_ip, domain, hostname, bind4, bind6):
|
||||||
|
errors = []
|
||||||
|
def query(name, kind):
|
||||||
|
try:
|
||||||
|
return dig(ns_ip, name, kind, authoritative=True)
|
||||||
|
except ValueError as exc:
|
||||||
|
errors.append(str(exc))
|
||||||
|
return []
|
||||||
|
|
||||||
|
spf_records = []
|
||||||
|
for raw in query(domain, 'TXT'):
|
||||||
|
try:
|
||||||
|
text = parse_txt(raw)
|
||||||
|
if text.lower().startswith('v=spf1'):
|
||||||
|
spf_records.append(text)
|
||||||
|
except ValueError as exc:
|
||||||
|
errors.append(str(exc))
|
||||||
|
if len(spf_records) != 1 or spf_records[0].lower().split() != ['v=spf1', 'mx', '-all']:
|
||||||
|
errors.append('SPF is not the approved single-host profile: v=spf1 mx -all')
|
||||||
|
|
||||||
|
mx = query(domain, 'MX')
|
||||||
|
if len(mx) != 1:
|
||||||
|
errors.append(f'Expected exactly one MX pointing to {hostname}; found {mx}')
|
||||||
|
else:
|
||||||
|
parts = mx[0].split()
|
||||||
|
if len(parts) != 2 or not parts[0].isdigit() or parts[1].rstrip('.').lower() != hostname:
|
||||||
|
errors.append(f'MX is not the expected mailserver {hostname}: {mx[0]}')
|
||||||
|
|
||||||
|
for kind, configured in [('A', bind4), ('AAAA', bind6)]:
|
||||||
|
answers = query(hostname, kind)
|
||||||
|
normalized = []
|
||||||
|
for entry in answers:
|
||||||
|
try:
|
||||||
|
normalized.append(str(ipaddress.ip_address(entry)))
|
||||||
|
except ValueError:
|
||||||
|
errors.append(f'Invalid {kind} address returned for {hostname}: {entry}')
|
||||||
|
if not configured:
|
||||||
|
errors.append(f'Postfix smtp_bind_address{("6" if kind == "AAAA" else "")} is empty; cannot verify outbound IP')
|
||||||
|
elif configured not in normalized:
|
||||||
|
errors.append(f'Postfix outbound {kind} address {configured} is not published for {hostname}: {normalized}')
|
||||||
|
|
||||||
|
print(f' Authoritative NS {ns_name} ({ns_ip}): ' + ('OK' if not errors else 'FAILED'))
|
||||||
|
for err in errors:
|
||||||
|
print(f' [ error ] {err}')
|
||||||
|
return not errors
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--domain', required=True)
|
||||||
|
ap.add_argument('--hostname', required=True)
|
||||||
|
ap.add_argument('--ipv4', required=True)
|
||||||
|
ap.add_argument('--ipv6', required=True)
|
||||||
|
args = ap.parse_args()
|
||||||
|
domain = args.domain.lower().rstrip('.')
|
||||||
|
hostname = args.hostname.lower().rstrip('.')
|
||||||
|
if not shutil.which('dig'):
|
||||||
|
print(' [ error ] dig is required to verify public DNS')
|
||||||
|
return 1
|
||||||
|
try:
|
||||||
|
bind4 = str(ipaddress.ip_address(args.ipv4)) if args.ipv4 else ''
|
||||||
|
bind6 = str(ipaddress.ip_address(args.ipv6)) if args.ipv6 else ''
|
||||||
|
zone, nameservers = discover_nameservers(domain)
|
||||||
|
print(f' DNS delegation zone................: {zone}')
|
||||||
|
print(f' Expected MX host...................: {hostname}')
|
||||||
|
print(f' Expected SMTP IPv4.................: {bind4 or "unknown"}')
|
||||||
|
print(f' Expected SMTP IPv6.................: {bind6 or "unknown"}')
|
||||||
|
if len(nameservers) > 8:
|
||||||
|
raise ValueError('More than eight authoritative nameservers; manual review required')
|
||||||
|
ok = True
|
||||||
|
for ns in nameservers:
|
||||||
|
try:
|
||||||
|
ip = addr_of_ns(ns)
|
||||||
|
if not check_server(ns, ip, domain, hostname, bind4, bind6):
|
||||||
|
ok = False
|
||||||
|
except ValueError as exc:
|
||||||
|
print(f' Authoritative NS {ns}: FAILED')
|
||||||
|
print(f' [ error ] {exc}')
|
||||||
|
ok = False
|
||||||
|
print(' Public SRS DNS preflight...........: ' + ('OK' if ok else 'FAILED'))
|
||||||
|
return 0 if ok else 1
|
||||||
|
except ValueError as exc:
|
||||||
|
print(f' [ error ] {exc}')
|
||||||
|
print(' Public SRS DNS preflight...........: FAILED')
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
Executable
+223
@@ -0,0 +1,223 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Track narrowly-scoped SRS virtual alias additions in a Postfix main.cf.
|
||||||
|
|
||||||
|
This helper never restores an old main.cf; it only adds/removes its exact two
|
||||||
|
list items. An ownership marker is mandatory before removal. This is a
|
||||||
|
review draft: do not use on a live mail server until the complete integration
|
||||||
|
and rollback have been tested.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import stat
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
MARKER_PREFIX = '# OOPEN-SRS-ALIAS-OWNERSHIP v1 '
|
||||||
|
PARAMS = ('virtual_alias_domains', 'virtual_alias_maps')
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def read_entries(lines, param):
|
||||||
|
"""Return [(first_line, last_line_exclusive, value_lines)] for a parameter.
|
||||||
|
|
||||||
|
Continuation lines must be indented, active, and not comments. A comment
|
||||||
|
ends the immediately preceding value, leaving its original text untouched.
|
||||||
|
"""
|
||||||
|
pattern = re.compile(r'^\s*' + re.escape(param) + r'\s*=', re.I)
|
||||||
|
found = []
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if not pattern.match(line) or line.lstrip().startswith('#'):
|
||||||
|
continue
|
||||||
|
j = i + 1
|
||||||
|
while j < len(lines) and lines[j].startswith((' ', '\t')) and lines[j].strip() and not lines[j].lstrip().startswith('#'):
|
||||||
|
j += 1
|
||||||
|
first_value = line.split('=', 1)[1].strip()
|
||||||
|
values = [first_value] + [item.strip() for item in lines[i + 1:j]]
|
||||||
|
found.append((i, j, values))
|
||||||
|
if len(found) > 1:
|
||||||
|
raise ConfigError(f'Duplicate active parameter {param}')
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def current_tokens(lines, param):
|
||||||
|
entries = read_entries(lines, param)
|
||||||
|
if not entries:
|
||||||
|
return (False, [])
|
||||||
|
values = entries[0][2]
|
||||||
|
return (True, [part for part in re.split(r'[\s,]+', ' '.join(values)) if part])
|
||||||
|
|
||||||
|
|
||||||
|
def make_marker(domain, reverse_map, orig):
|
||||||
|
return (MARKER_PREFIX + f'domain={domain} map={reverse_map} '
|
||||||
|
f'domains_was_set={int(orig[0])} maps_was_set={int(orig[1])}\n')
|
||||||
|
|
||||||
|
|
||||||
|
def marker_state(lines, domain, reverse_map):
|
||||||
|
markers = [line for line in lines if line.startswith('# OOPEN-SRS-ALIAS-OWNERSHIP')]
|
||||||
|
if not markers:
|
||||||
|
return None
|
||||||
|
if len(markers) != 1:
|
||||||
|
raise ConfigError('Multiple SRS alias ownership markers')
|
||||||
|
marker = markers[0]
|
||||||
|
expected = re.compile(r'^' + re.escape(MARKER_PREFIX) +
|
||||||
|
r'domain=' + re.escape(domain) + r' map=' + re.escape(reverse_map) +
|
||||||
|
r' domains_was_set=([01]) maps_was_set=([01])\n$')
|
||||||
|
match = expected.fullmatch(marker)
|
||||||
|
if not match:
|
||||||
|
raise ConfigError('Foreign or malformed SRS alias ownership marker')
|
||||||
|
return tuple(item == '1' for item in match.groups())
|
||||||
|
|
||||||
|
|
||||||
|
def inspect(lines, domain, reverse_map):
|
||||||
|
is_set_domains, domains = current_tokens(lines, PARAMS[0])
|
||||||
|
is_set_maps, maps = current_tokens(lines, PARAMS[1])
|
||||||
|
matches_domain = sum(token.lower() == domain.lower() for token in domains)
|
||||||
|
matches_map = sum(token == reverse_map for token in maps)
|
||||||
|
if matches_domain > 1 or matches_map > 1:
|
||||||
|
raise ConfigError('Duplicate SRS domain or reverse map item')
|
||||||
|
ownership = marker_state(lines, domain, reverse_map)
|
||||||
|
if ownership is not None:
|
||||||
|
if matches_domain != 1 or matches_map != 1:
|
||||||
|
raise ConfigError('Ownership marker present but SRS tokens are incomplete')
|
||||||
|
return 'managed', ownership
|
||||||
|
if matches_domain and matches_map:
|
||||||
|
return 'legacy', None
|
||||||
|
if not matches_domain and not matches_map:
|
||||||
|
return 'absent', (is_set_domains, is_set_maps)
|
||||||
|
raise ConfigError('Partial/unmanaged SRS alias configuration')
|
||||||
|
|
||||||
|
|
||||||
|
def append_token(lines, param, token):
|
||||||
|
entries = read_entries(lines, param)
|
||||||
|
if not entries:
|
||||||
|
if lines and not lines[-1].endswith('\n'):
|
||||||
|
lines[-1] += '\n'
|
||||||
|
lines.append(f'{param} = {token}\n')
|
||||||
|
return
|
||||||
|
start, end, values = entries[0]
|
||||||
|
if token in [part for part in re.split(r'[\s,]+', ' '.join(values)) if part]:
|
||||||
|
raise ConfigError(f'SRS token already present in {param}')
|
||||||
|
if end > start + 1:
|
||||||
|
lines.insert(end, f' {token}\n')
|
||||||
|
else:
|
||||||
|
line = lines[start].rstrip('\r\n')
|
||||||
|
lines[start] = line.rstrip() + ' ' + token + '\n'
|
||||||
|
|
||||||
|
|
||||||
|
def remove_token(lines, param, token, was_set):
|
||||||
|
entries = read_entries(lines, param)
|
||||||
|
if len(entries) != 1:
|
||||||
|
raise ConfigError(f'Cannot safely remove token from {param}: missing parameter')
|
||||||
|
first, end, _ = entries[0]
|
||||||
|
indices = []
|
||||||
|
for idx in range(first, end):
|
||||||
|
value = lines[idx].split('=', 1)[1] if idx == first else lines[idx]
|
||||||
|
parts = [p for p in re.split(r'[\s,]+', value.strip()) if p]
|
||||||
|
if token in parts:
|
||||||
|
indices.append(idx)
|
||||||
|
if len(indices) != 1:
|
||||||
|
raise ConfigError(f'Expected exactly one SRS token in {param}')
|
||||||
|
idx = indices[0]
|
||||||
|
line = lines[idx]
|
||||||
|
if idx == first:
|
||||||
|
before, value = line.split('=', 1)
|
||||||
|
remaining = [p for p in re.split(r'[\s,]+', value.strip()) if p != token]
|
||||||
|
if not was_set and end == first + 1 and not remaining:
|
||||||
|
del lines[first]
|
||||||
|
else:
|
||||||
|
lines[idx] = before.rstrip() + ' = ' + ' '.join(remaining) + '\n'
|
||||||
|
else:
|
||||||
|
remaining = [p for p in re.split(r'[\s,]+', line.strip()) if p != token]
|
||||||
|
if remaining:
|
||||||
|
lines[idx] = ' ' + ' '.join(remaining) + '\n'
|
||||||
|
else:
|
||||||
|
del lines[idx]
|
||||||
|
if not was_set and not current_tokens(lines, param)[1]:
|
||||||
|
entries = read_entries(lines, param)
|
||||||
|
if entries:
|
||||||
|
start, end, _ = entries[0]
|
||||||
|
del lines[start:end]
|
||||||
|
|
||||||
|
|
||||||
|
def write_atomic(path, content):
|
||||||
|
st = os.stat(path)
|
||||||
|
if not stat.S_ISREG(st.st_mode):
|
||||||
|
raise ConfigError('main.cf is not a regular file')
|
||||||
|
parent = os.path.dirname(os.path.abspath(path))
|
||||||
|
fd, temp_path = tempfile.mkstemp(prefix='.main.cf.srs-alias.', dir=parent)
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, 'w', encoding='utf-8', newline='') as f:
|
||||||
|
f.writelines(content)
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
os.chmod(temp_path, stat.S_IMODE(st.st_mode))
|
||||||
|
os.chown(temp_path, st.st_uid, st.st_gid)
|
||||||
|
os.replace(temp_path, path)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.unlink(temp_path)
|
||||||
|
|
||||||
|
|
||||||
|
def run(args):
|
||||||
|
path = Path(args.main_cf)
|
||||||
|
lines = path.read_text(encoding='utf-8').splitlines(keepends=True)
|
||||||
|
state, extra = inspect(lines, args.domain, args.reverse_map)
|
||||||
|
if args.mode == 'status':
|
||||||
|
print(state)
|
||||||
|
return
|
||||||
|
if args.mode == 'add':
|
||||||
|
if state != 'absent':
|
||||||
|
raise ConfigError('Add refused: expected absent state, got ' + state)
|
||||||
|
# Add in reverse order so insertion indexes remain consistent.
|
||||||
|
append_token(lines, PARAMS[1], args.reverse_map)
|
||||||
|
append_token(lines, PARAMS[0], args.domain)
|
||||||
|
lines.append(make_marker(args.domain, args.reverse_map, extra))
|
||||||
|
elif args.mode == 'adopt':
|
||||||
|
if state != 'legacy':
|
||||||
|
raise ConfigError('Adoption refused: expected legacy state, got ' + state)
|
||||||
|
if lines and not lines[-1].endswith('\n'):
|
||||||
|
lines[-1] += '\n'
|
||||||
|
lines.append(make_marker(args.domain, args.reverse_map, (True, True)))
|
||||||
|
elif args.mode == 'remove':
|
||||||
|
if state != 'managed':
|
||||||
|
raise ConfigError('Removal refused: expected managed state, got ' + state)
|
||||||
|
was_domains, was_maps = extra
|
||||||
|
remove_token(lines, PARAMS[1], args.reverse_map, was_maps)
|
||||||
|
remove_token(lines, PARAMS[0], args.domain, was_domains)
|
||||||
|
lines[:] = [line for line in lines if not line.startswith(MARKER_PREFIX)]
|
||||||
|
else:
|
||||||
|
raise ConfigError('Unknown mode')
|
||||||
|
expected = 'absent' if args.mode == 'remove' else 'managed'
|
||||||
|
check, _ = inspect(lines, args.domain, args.reverse_map)
|
||||||
|
if check != expected:
|
||||||
|
raise ConfigError(f'Post-modification state unexpected: {check}')
|
||||||
|
write_atomic(path, lines)
|
||||||
|
print(check)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('mode', choices=['status', 'add', 'adopt', 'remove'])
|
||||||
|
parser.add_argument('--main-cf', required=True)
|
||||||
|
parser.add_argument('--domain', required=True)
|
||||||
|
parser.add_argument('--reverse-map', required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not re.fullmatch(r'[A-Za-z0-9.-]+', args.domain):
|
||||||
|
parser.error('Unsafe domain')
|
||||||
|
if not re.fullmatch(r'socketmap:inet:127\.0\.0\.1:[0-9]{1,5}:reverse', args.reverse_map):
|
||||||
|
parser.error('Unexpected reverse map')
|
||||||
|
try:
|
||||||
|
run(args)
|
||||||
|
except (ConfigError, OSError, UnicodeError) as exc:
|
||||||
|
print(f'SRS alias configuration error: {exc}', file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Executable
+1153
File diff suppressed because it is too large
Load Diff
Executable
+303
@@ -0,0 +1,303 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Offline, path-relocated installer contract tests. All external commands mocked.
|
||||||
|
Do not equate these tests with genuine Postfix/PostSRSd integration.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import os, subprocess, tempfile, re, sys, shutil, signal, types
|
||||||
|
|
||||||
|
SOURCE = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
def putexe(file, content):
|
||||||
|
file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
file.write_text('#!/usr/bin/env bash\n' + content)
|
||||||
|
file.chmod(0o755)
|
||||||
|
|
||||||
|
def setup(root):
|
||||||
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
|
repo = root / 'repo'
|
||||||
|
repo.mkdir()
|
||||||
|
# Copy only public SRS source files needed by the test. Never copy the
|
||||||
|
# complete mailserver repository, .git, credentials, local conf/srs.conf,
|
||||||
|
# or unrelated applications into an executable test fixture.
|
||||||
|
if not SOURCE.is_dir():
|
||||||
|
raise RuntimeError(f'SOURCE must be a repository directory: {SOURCE}')
|
||||||
|
required = [
|
||||||
|
'install-update-srs.sh',
|
||||||
|
'helpers/srs-common.sh',
|
||||||
|
'helpers/srs-dns-check.py',
|
||||||
|
'helpers/srs-virtual-alias.py',
|
||||||
|
'files/srs/srs-alias-decision.py',
|
||||||
|
'files/srs/srs-forward.py',
|
||||||
|
'files/srs/srs-reverse-map.py',
|
||||||
|
'files/srs/srs-reverse-policy.py',
|
||||||
|
]
|
||||||
|
templates = SOURCE/'files/srs/templates'
|
||||||
|
required += [str(f.relative_to(SOURCE)) for f in templates.iterdir() if f.is_file() and not f.is_symlink()]
|
||||||
|
for rel in required:
|
||||||
|
src = SOURCE/rel
|
||||||
|
if not src.is_file() or src.is_symlink():
|
||||||
|
raise RuntimeError(f'Missing or symlinked source file: {src}')
|
||||||
|
dst = repo/rel
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
for path in (repo/'install-update-srs.sh', repo/'helpers/srs-common.sh',
|
||||||
|
*list((repo/'files/srs').glob('*.py')),
|
||||||
|
*list((repo/'files/srs/templates').glob('*'))):
|
||||||
|
s = path.read_text()
|
||||||
|
# Keep all managed install/config/queue paths in an unprivileged fixture tree.
|
||||||
|
# The postconf absolute command must also be relocated.
|
||||||
|
s = s.replace('/usr/sbin/postconf', str(root/'bin/postconf'))
|
||||||
|
for prefix in ['/etc/', '/usr/local/', '/var/', '/run/lock/', '/root/srs-installer-backups']:
|
||||||
|
new = str(root) + prefix
|
||||||
|
s = s.replace(prefix, new)
|
||||||
|
# prevent reading the real original root Postfix config in our fixtures
|
||||||
|
if path == repo/'helpers/srs-common.sh':
|
||||||
|
# The test runner uses a pipe for scripted confirmations;
|
||||||
|
# allow that only inside the copied, isolated helper fixture.
|
||||||
|
needle = 'if [[ -t 1 ]]; then'
|
||||||
|
if s.count(needle) != 1:
|
||||||
|
raise RuntimeError('Unexpected terminal gate in helper')
|
||||||
|
s = s.replace(needle, 'if true; then', 1)
|
||||||
|
path.write_text(s)
|
||||||
|
(repo/'install-update-srs.sh').chmod(0o755)
|
||||||
|
for p in ['etc/postfix','usr/local/sbin','run/lock','var/spool','var/lib','etc/systemd/system','flags']:
|
||||||
|
(root/p).mkdir(parents=True, exist_ok=True)
|
||||||
|
(root/'etc/postfix/main.cf').write_text('myhostname = mail.example.org\n')
|
||||||
|
(root/'etc/postfix/master.cf').write_text('smtp inet n - y - - smtpd\nlocalhost:10025 inet n - y - - smtpd\n -o receive_override_options=no_unknown_recipient_checks\n')
|
||||||
|
(repo/'conf').mkdir(exist_ok=True)
|
||||||
|
# All expected lookup paths are extracted from the (relocated) reference source.
|
||||||
|
alias = (repo/'files/srs/srs-alias-decision.py').read_text()
|
||||||
|
for typ,name in set(re.findall(r'"(pgsql|btree):([^" ]+)"', alias)):
|
||||||
|
f = Path(name + ('.db' if typ == 'btree' else ''))
|
||||||
|
f.parent.mkdir(parents=True,exist_ok=True)
|
||||||
|
f.touch()
|
||||||
|
bin = root/'bin'
|
||||||
|
bin.mkdir()
|
||||||
|
putexe(bin/'postconf', '''
|
||||||
|
name="${@: -1}"
|
||||||
|
# e.mx.oopen.de has two pre-existing unrelated warnings in main.cf.
|
||||||
|
# They are emitted on every postconf call while the requested value is valid.
|
||||||
|
echo "postconf: warning: ${SRS_TEST_ROOT}/etc/postfix/main.cf: unused parameter: policyd-spf_time_limit=3600" >&2
|
||||||
|
# The warning path matches the source main.cf in the relocated fixture.
|
||||||
|
# Existing warnings must be distinguished from queried-param errors.
|
||||||
|
if [[ -f "${SRS_TEST_ROOT}/flags/unknown_parameter_warning" ]]; then
|
||||||
|
echo "postconf: warning: smtp_tls_cafile: unknown parameter" >&2
|
||||||
|
fi
|
||||||
|
case "$name" in
|
||||||
|
mail_owner) echo postfix;;
|
||||||
|
myhostname) echo mail.example.org;;
|
||||||
|
mydomain) echo example.org;;
|
||||||
|
myorigin) echo /etc/mailname;;
|
||||||
|
mydestination) echo 'mail.example.org localhost';;
|
||||||
|
relay_domains) echo '\u0024mydestination';;
|
||||||
|
virtual_alias_maps) echo 'pgsql:some-alias';;
|
||||||
|
virtual_alias_domains) echo 'btree:some-domain';;
|
||||||
|
virtual_mailbox_maps) echo 'pgsql:some-mailbox';;
|
||||||
|
virtual_mailbox_domains) echo 'pgsql:some-domain';;
|
||||||
|
smtp_bind_address) echo 192.0.2.22;;
|
||||||
|
smtp_bind_address6) echo 2001:db8::22;;
|
||||||
|
smtp_dns_support_level) echo dnssec;;
|
||||||
|
smtp_host_lookup) echo dns;;
|
||||||
|
smtp_tls_CAfile) echo /etc/ssl/certs/ca-certificates.crt;;
|
||||||
|
smtp_tls_mandatory_protocols|smtp_tls_protocols) echo '>=TLSv1.2';;
|
||||||
|
smtp_tls_security_level) echo dane;;
|
||||||
|
*) echo "unknown $name" >&2; exit 1;;
|
||||||
|
esac
|
||||||
|
''')
|
||||||
|
putexe(bin/'id', '''
|
||||||
|
if [[ "$1" == -u && "$2" == postfix ]]; then echo 123; exit 0; fi
|
||||||
|
if [[ "$1" == -g && "$2" == postfix ]]; then echo 123; exit 0; fi
|
||||||
|
exec /usr/bin/id "$@"
|
||||||
|
''')
|
||||||
|
putexe(bin/'systemctl', '''
|
||||||
|
if [[ "$1" == is-active ]]; then exit 3; fi
|
||||||
|
exit 0
|
||||||
|
''')
|
||||||
|
putexe(bin/'git', '''
|
||||||
|
if [[ "$1" == clone ]]; then
|
||||||
|
mkdir -p "${@: -1}"; exit 0
|
||||||
|
fi
|
||||||
|
exec /usr/bin/git "$@"
|
||||||
|
''')
|
||||||
|
putexe(bin/'cmake', '''
|
||||||
|
if [[ "$1" == --version ]]; then echo 'cmake version 3.31.6'; exit 0; fi
|
||||||
|
if [[ "$1" == -S ]]; then
|
||||||
|
for ((i=1; i<=$#; i++)); do
|
||||||
|
arg="${!i}"
|
||||||
|
if [[ "$arg" == -B ]]; then n=$((i+1)); build="${!n}"; fi
|
||||||
|
if [[ "$arg" == -DCMAKE_INSTALL_PREFIX=* ]]; then prefix="${arg#*=}"; fi
|
||||||
|
done
|
||||||
|
mkdir -p "$build"; printf '%s\\n' "$prefix" > "$build/prefix"; exit 0
|
||||||
|
fi
|
||||||
|
if [[ "$1" == --build ]]; then exit 0; fi
|
||||||
|
if [[ "$1" == --install ]]; then
|
||||||
|
prefix="$(cat "$2/prefix")"
|
||||||
|
mkdir -p "${DESTDIR}${prefix}/sbin"
|
||||||
|
printf '#!/bin/sh\\nexit 0\\n' > "${DESTDIR}${prefix}/sbin/postsrsd"
|
||||||
|
chmod +x "${DESTDIR}${prefix}/sbin/postsrsd"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 9
|
||||||
|
''')
|
||||||
|
putexe(bin/'ctest','exit 0\n')
|
||||||
|
putexe(bin/'dig','exit 0\n')
|
||||||
|
putexe(bin/'postmap','''
|
||||||
|
case "$1" in
|
||||||
|
btree:*) dest="${1#btree:}"; touch "${dest}.db";;
|
||||||
|
*) exit 1;;
|
||||||
|
esac
|
||||||
|
if [[ -f "${SRS_TEST_ROOT}/flags/fail_postmap" ]]; then exit 1; fi
|
||||||
|
exit 0
|
||||||
|
''')
|
||||||
|
putexe(bin/'postfix','''
|
||||||
|
if [[ "$*" == *check* ]]; then
|
||||||
|
mkdir -p "${SRS_TEST_ROOT}/var/spool/postfix-srs-delivery/private" "${SRS_TEST_ROOT}/var/lib/postfix-srs-delivery"
|
||||||
|
if [[ -f "${SRS_TEST_ROOT}/flags/fail_postfix" ]]; then echo 'forced postfix check error' >&2; exit 1; fi
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
''')
|
||||||
|
(repo/'helpers/srs-dns-check.py').write_text('print(" Simulated authoritative DNS: OK")\n')
|
||||||
|
return repo
|
||||||
|
|
||||||
|
def run(root, repo, *, fail=None, version=None):
|
||||||
|
if fail: (root/'flags'/('fail_'+fail)).touch()
|
||||||
|
argv = [str(repo/'install-update-srs.sh'), '--install']
|
||||||
|
if version: argv.extend(['--version', version])
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.update({'PATH':str(root/'bin') + ':' + env['PATH'], 'SRS_TEST_ROOT':str(root), 'LC_ALL':'C'})
|
||||||
|
# No real PTY is needed: the terminal requirement is disabled only in the
|
||||||
|
# relocated helper copy, never in the actual repository.
|
||||||
|
# File-backed capture prevents subprocess pipe hangs from descendant
|
||||||
|
# processes retaining output fds. A timeout kills the entire fixture group.
|
||||||
|
with tempfile.TemporaryFile(mode='w+t') as out:
|
||||||
|
proc = subprocess.Popen(argv, stdin=subprocess.PIPE, stdout=out,
|
||||||
|
stderr=subprocess.STDOUT, text=True, env=env,
|
||||||
|
start_new_session=True)
|
||||||
|
try:
|
||||||
|
proc.communicate(input='srs.example.org\nyes\nyes\n', timeout=20)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
os.killpg(proc.pid, signal.SIGKILL)
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
raise RuntimeError('Mock installer timed out')
|
||||||
|
out.seek(0)
|
||||||
|
return types.SimpleNamespace(returncode=proc.returncode, stdout=out.read())
|
||||||
|
|
||||||
|
def assert_ok(condition, msg):
|
||||||
|
if not condition: raise AssertionError(msg)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix='oopen-srs-test-') as tmp:
|
||||||
|
base=Path(tmp)
|
||||||
|
os.chmod(base, 0o2700) # Test SGID inheritance without granting group access.
|
||||||
|
root=base/'initial'
|
||||||
|
repo=setup(root)
|
||||||
|
result=run(root,repo)
|
||||||
|
print('initial install', result.returncode)
|
||||||
|
if result.returncode: print(result.stdout[-2500:])
|
||||||
|
assert_ok(result.returncode == 0,'initial installation failed')
|
||||||
|
assert_ok((root/'usr/local/postsrsd').is_symlink(), 'missing symlink')
|
||||||
|
assert_ok((root/'usr/local/sbin/srs-forward.py').exists(), 'missing srs forward')
|
||||||
|
assert_ok((root/'etc/postsrsd-srs/secret').stat().st_mode & 0o777 == 0o600, 'secret mode')
|
||||||
|
assert_ok('smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt' in (root/'etc/postfix-srs/delivery/main.cf').read_text(),'rendered TLS CA')
|
||||||
|
print(" secret dir mode:", oct((root/"etc/postsrsd-srs").stat().st_mode & 0o777), "uid", (root/"etc/postsrsd-srs").stat().st_uid)
|
||||||
|
print(" stat check:", subprocess.check_output(["stat", "-c", "%u:%g:%a", str(root/"etc/postsrsd-srs")], text=True).strip())
|
||||||
|
print(' initial install: state, secret, config, symlink, CAfile verified')
|
||||||
|
original_secret=(root/'etc/postsrsd-srs/secret').read_bytes()
|
||||||
|
original_forward=(root/'usr/local/sbin/srs-forward.py').read_bytes()
|
||||||
|
original_link=os.readlink(root/'usr/local/postsrsd')
|
||||||
|
result=run(root,repo)
|
||||||
|
print('maintenance update', result.returncode)
|
||||||
|
if result.returncode: print(result.stdout[-2500:])
|
||||||
|
assert_ok(result.returncode == 0,'maintenance update failed')
|
||||||
|
assert_ok((root/'etc/postsrsd-srs/secret').read_bytes()==original_secret,'secret changed')
|
||||||
|
assert_ok(os.readlink(root/'usr/local/postsrsd')==original_link,'link changed')
|
||||||
|
print(' maintenance update: original secret and binary retained')
|
||||||
|
# Force late failure, after Postfix configuration and all units have been deployed.
|
||||||
|
source=repo/'files/srs/srs-forward.py'
|
||||||
|
source.write_text(source.read_text()+'\n# injected test change\n')
|
||||||
|
result=run(root,repo,fail='postfix')
|
||||||
|
print('failed maintenance rollback', result.returncode)
|
||||||
|
if result.returncode==0: print(result.stdout[-2500:])
|
||||||
|
assert_ok(result.returncode != 0,'injected failure not detected')
|
||||||
|
assert_ok((root/'usr/local/sbin/srs-forward.py').read_bytes()==original_forward,'rollback did not restore file')
|
||||||
|
assert_ok((root/'etc/postsrsd-srs/secret').read_bytes()==original_secret,'rollback changed secret')
|
||||||
|
assert_ok(os.readlink(root/'usr/local/postsrsd')==original_link,'rollback changed symlink')
|
||||||
|
print(' failure rollback: file restored; secret and symlink preserved')
|
||||||
|
(root/'flags/fail_postfix').unlink()
|
||||||
|
result=run(root,repo,version='2.4.2')
|
||||||
|
print('version upgrade', result.returncode)
|
||||||
|
if result.returncode: print(result.stdout[-2500:])
|
||||||
|
assert_ok(result.returncode == 0,'version upgrade failed')
|
||||||
|
assert_ok((root/'usr/local/postsrsd-2.4.1').exists(),'prior prefix missing')
|
||||||
|
assert_ok(os.readlink(root/'usr/local/postsrsd')=='postsrsd-2.4.2','new link incorrect')
|
||||||
|
print(' version upgrade: previous prefix retained; link updated')
|
||||||
|
(root/'flags/fail_postfix').touch()
|
||||||
|
result=run(root,repo,version='2.4.3')
|
||||||
|
print('failed version upgrade rollback', result.returncode)
|
||||||
|
assert_ok(result.returncode != 0,'injected failure not detected')
|
||||||
|
assert_ok(os.readlink(root/'usr/local/postsrsd')=='postsrsd-2.4.2','previous version link not restored')
|
||||||
|
assert_ok(not (root/'usr/local/postsrsd-2.4.3').exists(),'failed version prefix not removed')
|
||||||
|
print(' failed version upgrade: symlink restored, new prefix removed')
|
||||||
|
|
||||||
|
# Active SRS configuration must prohibit any maintenance update before writes.
|
||||||
|
(root/'flags/fail_postfix').unlink()
|
||||||
|
active_marker = '# BEGIN OOPEN-SRS-MANAGED'
|
||||||
|
existing_main=(root/'etc/postfix/main.cf')
|
||||||
|
original_main=existing_main.read_text()
|
||||||
|
existing_main.write_text(original_main + active_marker + '\n')
|
||||||
|
before=(root/'usr/local/sbin/srs-forward.py').read_bytes()
|
||||||
|
result=run(root,repo,version='2.4.2')
|
||||||
|
print('active integration guard', result.returncode)
|
||||||
|
assert_ok(result.returncode != 0,'active SRS was not refused')
|
||||||
|
assert_ok((root/'usr/local/sbin/srs-forward.py').read_bytes() == before, 'active check changed scripts')
|
||||||
|
existing_main.write_text(original_main)
|
||||||
|
print(' active integration guard: blocked before file changes')
|
||||||
|
|
||||||
|
# Independent initial installation with late failure. New SRS files must be removed.
|
||||||
|
root=base/'failed-first'
|
||||||
|
repo=setup(root)
|
||||||
|
result=run(root,repo,fail='postfix')
|
||||||
|
print('failed first install', result.returncode)
|
||||||
|
assert_ok(result.returncode != 0,'injected first-install failure not detected')
|
||||||
|
assert_ok(not (root/'usr/local/postsrsd').exists(), 'first-install rollback left symlink')
|
||||||
|
assert_ok(not (root/'usr/local/postsrsd-2.4.1').exists(), 'first-install rollback left prefix')
|
||||||
|
assert_ok(not (root/'etc/postsrsd-srs/secret').exists(), 'first-install rollback left secret')
|
||||||
|
assert_ok(not (root/'etc/postfix-srs/delivery/main.cf').exists(), 'first-install rollback left config')
|
||||||
|
assert_ok((root/'var/spool/postfix-srs-delivery/private').is_dir(), 'test Postfix runtime queue unexpectedly deleted')
|
||||||
|
print(' failed first install: tracked files removed; Postfix runtime directory safely preserved')
|
||||||
|
|
||||||
|
# Fail at postmap, before systemd and Postfix checks.
|
||||||
|
root=base/'failed-map'
|
||||||
|
repo=setup(root)
|
||||||
|
result=run(root,repo,fail='postmap')
|
||||||
|
print('failed postmap rollback', result.returncode)
|
||||||
|
assert_ok(result.returncode != 0, 'injected postmap failure not detected')
|
||||||
|
assert_ok(not (root/'etc/postfix-srs/delivery/transport.db').exists(),'failed postmap index remains')
|
||||||
|
assert_ok(not (root/'etc/postsrsd-srs/secret').exists(),'failed postmap secret remains')
|
||||||
|
assert_ok(not (root/'usr/local/postsrsd').exists(),'failed postmap link remains')
|
||||||
|
print(' failed postmap: generated index and secret removed')
|
||||||
|
|
||||||
|
# Unexpected warnings must never be silently swallowed.
|
||||||
|
root=base/'unexpected-warning'
|
||||||
|
repo=setup(root)
|
||||||
|
(root/'flags/unknown_parameter_warning').touch()
|
||||||
|
result=run(root, repo)
|
||||||
|
print('unexpected postconf warning', result.returncode)
|
||||||
|
assert_ok(result.returncode != 0, 'unexpected postconf warning accepted')
|
||||||
|
assert_ok(not (root/'usr/local/postsrsd').exists(), 'unexpected warning allowed installation')
|
||||||
|
print(' unrelated existing warnings tolerated, queried-parameter warning rejected')
|
||||||
|
|
||||||
|
# A template referencing an unknown Postfix parameter must fail closed.
|
||||||
|
root=base/'bad-template-parameter'
|
||||||
|
repo=setup(root)
|
||||||
|
template=root/'repo/files/srs/templates/delivery-main.cf.in'
|
||||||
|
contents=template.read_text()
|
||||||
|
assert_ok('smtp_tls_CAfile = @SMTP_TLS_CAFILE@' in contents, 'missing expected template parameter')
|
||||||
|
template.write_text(contents.replace('smtp_tls_CAfile = @SMTP_TLS_CAFILE@',
|
||||||
|
'smtp_tls_cafile = @SMTP_TLS_CAFILE@'))
|
||||||
|
result=run(root, repo)
|
||||||
|
print('invalid template parameter', result.returncode)
|
||||||
|
assert_ok(result.returncode != 0, 'unknown template Postfix parameter accepted')
|
||||||
|
assert_ok(not (root/'usr/local/postsrsd').exists(), 'invalid template allowed installation')
|
||||||
|
print(' unknown requested Postfix parameter rejected')
|
||||||
|
|
||||||
|
print('ALL OFFLINE MOCK CONTRACT TESTS PASSED')
|
||||||
Reference in New Issue
Block a user