From 39b933b798f96c101a9890fd4c6125d3ce32fbfb Mon Sep 17 00:00:00 2001 From: Christoph Date: Tue, 20 Oct 2026 14:41:48 +0200 Subject: [PATCH] Archive discontinued selective SRS implementation --- .gitignore | 4 + README.md | 51 ++++ .../selective-srs-2026/README-SRS.md | 0 .../selective-srs-2026/README-install-SRS.md | 0 archive/selective-srs-2026/README.md | 170 +++++++++++ .../selective-srs-2026/activate-srs.sh | 0 .../selective-srs-2026/conf}/srs.conf.sample | 0 .../selective-srs-2026/deactivate-srs.sh | 0 .../files}/srs/srs-alias-decision.py | 0 .../files}/srs/srs-forward.py | 0 .../files}/srs/srs-reverse-map.py | 0 .../files}/srs/srs-reverse-policy.py | 0 .../files}/srs/templates/delivery-main.cf.in | 0 .../files}/srs/templates/delivery-master.cf | 0 .../files}/srs/templates/postsrsd-srs.service | 0 .../files}/srs/templates/postsrsd.conf.in | 0 .../srs/templates/srs-reverse-map.service | 0 .../files}/srs/templates/transport.in | 0 .../selective-srs-2026/helpers}/srs-common.sh | 0 .../helpers}/srs-dns-check.py | 0 .../helpers}/srs-virtual-alias.py | 0 .../selective-srs-2026/install-update-srs.sh | 0 .../tests/test_installer_mock.py | 263 ++++++++++++++++++ .../tests}/test_installer_mock_safe.py | 0 .../selective-srs-2026/tests}/test_srs_dns.py | 0 25 files changed, 488 insertions(+) create mode 100644 README.md rename README-SRS.md => archive/selective-srs-2026/README-SRS.md (100%) rename README-install-SRS.md => archive/selective-srs-2026/README-install-SRS.md (100%) create mode 100644 archive/selective-srs-2026/README.md rename activate-srs.sh => archive/selective-srs-2026/activate-srs.sh (100%) rename {conf => archive/selective-srs-2026/conf}/srs.conf.sample (100%) rename deactivate-srs.sh => archive/selective-srs-2026/deactivate-srs.sh (100%) rename {files => archive/selective-srs-2026/files}/srs/srs-alias-decision.py (100%) rename {files => archive/selective-srs-2026/files}/srs/srs-forward.py (100%) rename {files => archive/selective-srs-2026/files}/srs/srs-reverse-map.py (100%) rename {files => archive/selective-srs-2026/files}/srs/srs-reverse-policy.py (100%) rename {files => archive/selective-srs-2026/files}/srs/templates/delivery-main.cf.in (100%) rename {files => archive/selective-srs-2026/files}/srs/templates/delivery-master.cf (100%) rename {files => archive/selective-srs-2026/files}/srs/templates/postsrsd-srs.service (100%) rename {files => archive/selective-srs-2026/files}/srs/templates/postsrsd.conf.in (100%) rename {files => archive/selective-srs-2026/files}/srs/templates/srs-reverse-map.service (100%) rename {files => archive/selective-srs-2026/files}/srs/templates/transport.in (100%) rename {helpers => archive/selective-srs-2026/helpers}/srs-common.sh (100%) rename {helpers => archive/selective-srs-2026/helpers}/srs-dns-check.py (100%) rename {helpers => archive/selective-srs-2026/helpers}/srs-virtual-alias.py (100%) rename install-update-srs.sh => archive/selective-srs-2026/install-update-srs.sh (100%) create mode 100755 archive/selective-srs-2026/tests/test_installer_mock.py rename {tests => archive/selective-srs-2026/tests}/test_installer_mock_safe.py (100%) rename {tests => archive/selective-srs-2026/tests}/test_srs_dns.py (100%) diff --git a/.gitignore b/.gitignore index 473b6ff..3527ae9 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ log* # - roundcube /log/* roundcubemail-* + +# Python +__pycache__/ +*.py[cod] diff --git a/README.md b/README.md new file mode 100644 index 0000000..df5f4d6 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# Mailsystem + +Dieses Repository enthaelt Installations-, Update- und +Verwaltungsskripte sowie Konfigurationsvorlagen und +Dokumentationen fuer Linux-basierte Mailserver. + +Zu den unterstuetzten Komponenten gehoeren unter anderem: + +- Postfix +- Dovecot +- AmaViS +- SpamAssassin und ClamAV +- OpenDKIM und OpenDMARC +- PostfixAdmin +- Roundcube +- weitere Dienste und Hilfsprogramme + +## Installation und Dokumentation + +Die Installations- und Verwaltungsskripte befinden sich +ueberwiegend im Hauptverzeichnis. + +Ergaenzende Dateien liegen unter anderem in: + +- conf/ +- files/ +- helpers/ +- DOC/ + +Weitere Informationen enthalten die vorhandenen +README-Dateien, insbesondere README.install. + +## Archivierte Entwicklungen + +Abgeschlossene oder eingestellte Entwicklungsarbeiten +werden unter archive/ aufbewahrt. + +### Selective SRS (2026) + +Die im Oktober 2026 entwickelte selektive SRS-Loesung +wurde nach abschliessender Architekturpruefung +eingestellt und von den Mailservern entfernt. + +Der vollstaendige historische Entwicklungsstand ist +hier dokumentiert: + +[Selective SRS 2026](archive/selective-srs-2026/README.md) + +Die archivierten SRS-Skripte sind nicht fuer den +produktiven Einsatz freigegeben und duerfen nicht +unveraendert zur Installation verwendet werden. diff --git a/README-SRS.md b/archive/selective-srs-2026/README-SRS.md similarity index 100% rename from README-SRS.md rename to archive/selective-srs-2026/README-SRS.md diff --git a/README-install-SRS.md b/archive/selective-srs-2026/README-install-SRS.md similarity index 100% rename from README-install-SRS.md rename to archive/selective-srs-2026/README-install-SRS.md diff --git a/archive/selective-srs-2026/README.md b/archive/selective-srs-2026/README.md new file mode 100644 index 0000000..6702732 --- /dev/null +++ b/archive/selective-srs-2026/README.md @@ -0,0 +1,170 @@ +# Selective Sender Rewriting Scheme (SRS) + +## Projektstatus + +**Archiviert am 20. Oktober 2026.** + +Die Entwicklung wurde eingestellt. Diese Implementierung ist nicht +fuer den produktiven Einsatz freigegeben. + +Dieses Verzeichnis dient ausschliesslich der Archivierung und +Dokumentation des bisherigen Entwicklungsstands. + +Die enthaltenen Installations- und Verwaltungsskripte duerfen +nicht unveraendert fuer Neuinstallationen verwendet werden. + +## Urspruengliches Ziel + +Ziel war eine selektive SRS-Implementierung fuer Postfix. + +SRS sollte ausschliesslich bei externen Weiterleitungen zum +Einsatz kommen, die durch lokale virtuelle Mail-Aliase entstehen. + +Normale eingehende und ausgehende E-Mails sollten dagegen +unveraendert verarbeitet werden. + +Die Verwaltung virtueller Aliase erfolgt unter anderem ueber +PostfixAdmin und PostgreSQL. + +## Technische Implementierung + +Entwickelt wurde eine Loesung auf Basis von: + +- Postfix +- PostSRSd 2.4.1 +- einer zusaetzlichen Postfix-Instanz +- eigenen Python-Programmen zur Alias-Entscheidung, + Weiterleitung und Rueckuebersetzung +- Shell-Skripten fuer Installation, Aktivierung und + Deaktivierung + +Die Implementierung enthielt unter anderem: + +- selektive SRS-Entscheidungslogik +- SRS0-/SRS1-Umschreibung +- Verarbeitung von SRS-Ruecklaeufern +- persistente Speicherung des vorherigen Postfix-Zustands +- kontrollierte Aktivierung und Deaktivierung +- Sicherungs- und Rollback-Funktionen +- DNS- und SPF-Pruefungen +- Offline-Tests + +## Entwicklungs- und Testergebnis + +Die Implementierung wurde im Oktober 2026 entwickelt und +auf e.mx.oopen.de getestet. + +Dabei wurden unter anderem folgende Funktionen erfolgreich +geprueft: + +- Aktivierung und Deaktivierung +- externe Alias-Weiterleitungen mit SRS +- SPF-Pruefung weitergeleiteter Nachrichten +- Rueckuebersetzung von SRS-Ruecklaeufern +- verschiedene Fehler- und Rollback-Szenarien + +Die vorhandenen Tests belegen keine vollstaendige +Produktionsreife. + +Die Loesung wurde zeitweise auch auf mx.warenform.de +produktiv eingesetzt. + +## Grund fuer die Einstellung + +Bei der abschliessenden Architekturpruefung wurden +grundlegende betriebliche Risiken festgestellt. + +Insbesondere: + +1. Die Verwendung von + + default_transport = srs-forward + + fuehrte dazu, dass auch regulaere Postfix-Zustellungen + von der eigenen Python-SRS-Verarbeitung und der + zusaetzlichen Postfix-Instanz abhaengig waren. + +2. Die globale Einbindung der SRS-Reverse-Socketmap + in die Empfaengeradressverarbeitung erzeugte eine + weitere Abhaengigkeit von einem eigenen Dienst. + +3. Fehler in der SRS-Entscheidungslogik konnten auch + Nachrichten beeintraechtigen, fuer die ueberhaupt + keine SRS-Umschreibung erforderlich war. + +4. Der Betriebs- und Wartungsaufwand der Gesamtkonstruktion + war fuer die urspruenglich begrenzte Aufgabenstellung + zu hoch. + +Damit wurde die wichtigste betriebliche Anforderung +nicht erfuellt: + +Der regulaere Mailbetrieb muss auch bei einem Ausfall +der optionalen SRS-Verarbeitung unabhaengig weiterlaufen. + +Eine weitere Ausweitung der eigenen Fehlerbehandlung +wurde nicht als sinnvoll angesehen. + +## Entscheidung + +Am 20. Oktober 2026 wurde entschieden, diese +SRS-Implementierung nicht weiterzuentwickeln +und nicht fuer weitere produktive Installationen +zu verwenden. + +Stattdessen wurde der bisherige Postfix-Betrieb +ohne SRS wiederhergestellt. + +## Rueckbau + +Die SRS-Integration wurde am 20. Oktober 2026 auf +folgenden Systemen deaktiviert und entfernt: + +- e.mx.oopen.de +- mx.warenform.de + +Entfernt wurden insbesondere: + +- SRS-spezifische Postfix-Konfigurationseintraege +- die zusaetzliche Postfix-Instanz +- SRS-spezifische systemd-Units +- PostSRSd und die eigenen SRS-Python-Programme +- nicht mehr benoetigte SRS-Socket- und PID-Dateien + +Die Konfiguration und das jeweilige SRS-Secret +wurden vor der Entfernung lokal gesichert. + +Diese Sicherungen sind nicht Bestandteil des +Git-Repositories. + +## Inhalt des Archivs + +Die bisherigen Programme, Konfigurationsvorlagen, +Verwaltungsskripte, Tests und Dokumentationen +bleiben hier erhalten. + +Die urspruenglichen README-Dateien beschreiben +den damaligen Entwicklungsstand und sind als +historische Dokumentation zu verstehen. + +Da die Dateien in dieses Archivverzeichnis +verschoben wurden, stimmen einige relative Pfade +und Annahmen der Skripte nicht mehr mit der +urspruenglichen Repository-Struktur ueberein. + +Die Skripte und Tests sind daher nicht ohne +weitere Anpassungen ausfuehrbar. + +## Moegliche spaetere Neubewertung + +Eine erneute Beschaeftigung mit SRS sollte +nur erfolgen, wenn eine deutlich einfachere +und besser isolierte Architektur verfuegbar ist. + +Dabei muss insbesondere gewaehrleistet sein, +dass Fehler oder Ausfaelle der SRS-Verarbeitung +keine Auswirkungen auf den regulaeren +Postfix-Mailbetrieb haben. + +Eine unveraenderte Wiederaufnahme der hier +archivierten Implementierung wird nicht empfohlen. diff --git a/activate-srs.sh b/archive/selective-srs-2026/activate-srs.sh similarity index 100% rename from activate-srs.sh rename to archive/selective-srs-2026/activate-srs.sh diff --git a/conf/srs.conf.sample b/archive/selective-srs-2026/conf/srs.conf.sample similarity index 100% rename from conf/srs.conf.sample rename to archive/selective-srs-2026/conf/srs.conf.sample diff --git a/deactivate-srs.sh b/archive/selective-srs-2026/deactivate-srs.sh similarity index 100% rename from deactivate-srs.sh rename to archive/selective-srs-2026/deactivate-srs.sh diff --git a/files/srs/srs-alias-decision.py b/archive/selective-srs-2026/files/srs/srs-alias-decision.py similarity index 100% rename from files/srs/srs-alias-decision.py rename to archive/selective-srs-2026/files/srs/srs-alias-decision.py diff --git a/files/srs/srs-forward.py b/archive/selective-srs-2026/files/srs/srs-forward.py similarity index 100% rename from files/srs/srs-forward.py rename to archive/selective-srs-2026/files/srs/srs-forward.py diff --git a/files/srs/srs-reverse-map.py b/archive/selective-srs-2026/files/srs/srs-reverse-map.py similarity index 100% rename from files/srs/srs-reverse-map.py rename to archive/selective-srs-2026/files/srs/srs-reverse-map.py diff --git a/files/srs/srs-reverse-policy.py b/archive/selective-srs-2026/files/srs/srs-reverse-policy.py similarity index 100% rename from files/srs/srs-reverse-policy.py rename to archive/selective-srs-2026/files/srs/srs-reverse-policy.py diff --git a/files/srs/templates/delivery-main.cf.in b/archive/selective-srs-2026/files/srs/templates/delivery-main.cf.in similarity index 100% rename from files/srs/templates/delivery-main.cf.in rename to archive/selective-srs-2026/files/srs/templates/delivery-main.cf.in diff --git a/files/srs/templates/delivery-master.cf b/archive/selective-srs-2026/files/srs/templates/delivery-master.cf similarity index 100% rename from files/srs/templates/delivery-master.cf rename to archive/selective-srs-2026/files/srs/templates/delivery-master.cf diff --git a/files/srs/templates/postsrsd-srs.service b/archive/selective-srs-2026/files/srs/templates/postsrsd-srs.service similarity index 100% rename from files/srs/templates/postsrsd-srs.service rename to archive/selective-srs-2026/files/srs/templates/postsrsd-srs.service diff --git a/files/srs/templates/postsrsd.conf.in b/archive/selective-srs-2026/files/srs/templates/postsrsd.conf.in similarity index 100% rename from files/srs/templates/postsrsd.conf.in rename to archive/selective-srs-2026/files/srs/templates/postsrsd.conf.in diff --git a/files/srs/templates/srs-reverse-map.service b/archive/selective-srs-2026/files/srs/templates/srs-reverse-map.service similarity index 100% rename from files/srs/templates/srs-reverse-map.service rename to archive/selective-srs-2026/files/srs/templates/srs-reverse-map.service diff --git a/files/srs/templates/transport.in b/archive/selective-srs-2026/files/srs/templates/transport.in similarity index 100% rename from files/srs/templates/transport.in rename to archive/selective-srs-2026/files/srs/templates/transport.in diff --git a/helpers/srs-common.sh b/archive/selective-srs-2026/helpers/srs-common.sh similarity index 100% rename from helpers/srs-common.sh rename to archive/selective-srs-2026/helpers/srs-common.sh diff --git a/helpers/srs-dns-check.py b/archive/selective-srs-2026/helpers/srs-dns-check.py similarity index 100% rename from helpers/srs-dns-check.py rename to archive/selective-srs-2026/helpers/srs-dns-check.py diff --git a/helpers/srs-virtual-alias.py b/archive/selective-srs-2026/helpers/srs-virtual-alias.py similarity index 100% rename from helpers/srs-virtual-alias.py rename to archive/selective-srs-2026/helpers/srs-virtual-alias.py diff --git a/install-update-srs.sh b/archive/selective-srs-2026/install-update-srs.sh similarity index 100% rename from install-update-srs.sh rename to archive/selective-srs-2026/install-update-srs.sh diff --git a/archive/selective-srs-2026/tests/test_installer_mock.py b/archive/selective-srs-2026/tests/test_installer_mock.py new file mode 100755 index 0000000..2a61d87 --- /dev/null +++ b/archive/selective-srs-2026/tests/test_installer_mock.py @@ -0,0 +1,263 @@ +#!/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, zipfile, re, sys, shutil + +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() + if SOURCE.is_file(): + with zipfile.ZipFile(SOURCE) as z: + z.extractall(repo) + else: + for source_file in SOURCE.rglob('*'): + if source_file.is_file(): + dst = repo / source_file.relative_to(SOURCE) + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source_file, 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 + 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 +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/'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() + cmd = f"{repo/'install-update-srs.sh'} --install" + if version: cmd += f' --version {version}' + env = os.environ.copy() + env.update({'PATH':str(root/'bin') + ':' + env['PATH'], 'SRS_TEST_ROOT':str(root), 'LC_ALL':'C'}) + # Running inside util-linux script supplies a real PTY for init_srs_runtime. + result = subprocess.run(['script','-q','-e','-c',cmd,'/dev/null'], input='srs.example.org\nyes\nyes\n', + text=True, capture_output=True, timeout=35, env=env) + return result + +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, 0o2770) # Deliberately inherit SGID on fixture subdirectories. + 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') diff --git a/tests/test_installer_mock_safe.py b/archive/selective-srs-2026/tests/test_installer_mock_safe.py similarity index 100% rename from tests/test_installer_mock_safe.py rename to archive/selective-srs-2026/tests/test_installer_mock_safe.py diff --git a/tests/test_srs_dns.py b/archive/selective-srs-2026/tests/test_srs_dns.py similarity index 100% rename from tests/test_srs_dns.py rename to archive/selective-srs-2026/tests/test_srs_dns.py