From ba21a3aa7de96bed81707783703f29a4d913683b Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 7 Aug 2026 13:56:41 +0200 Subject: [PATCH] Trial-Setup-Script entfernt, funktionierende Trial-URL dokumentiert Das Script hat den Erstkonfigurationsassistenten zwar zuverlaessig uebersprungen, aber nicht das eigentliche Ziel erreicht: der Schalter ConfigWizardDone gibt nur Administrationskonsole und Admin-API frei und aktiviert keinen Testzeitraum. Ohne Lizenzschluessel bleiben IMAP, POP3 und Webmail gesperrt, damit taugt der Weg nicht fuer echten Mailbetrieb. An seiner Stelle steht jetzt ein README-Abschnitt mit der Adresse, die tatsaechlich funktioniert. Der Link im Assistenten zeigt noch auf kerio.com; GFI hat auf gfi.ai umgestellt, und alle alten Download- und Trial-Adressen leiten auf https://gfi.ai/products-and-solutions/email-and-messaging-solutions/kerioconnect/free-trial Dazu zwei Punkte zur Fehlersuche, falls ein Schluessel abgelehnt wird: error.log unterscheidet zwischen ungueltig und abgelaufen ("License is expired"), und registration.kerio.com ist nicht mehr aufloesbar, waehrend secure.kerio.com es noch ist. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 76 ++++----- kerio_trial_setup.py | 360 ------------------------------------------- 2 files changed, 41 insertions(+), 395 deletions(-) delete mode 100755 kerio_trial_setup.py diff --git a/README.md b/README.md index 627336c..853f49c 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,11 @@ Werkzeuge für Kerio Connect auf der Kommandozeile. | Datei | Zweck | | --- | --- | | [`kerio_rename_user.py`](kerio_rename_user.py) | Benutzer umbenennen — Login-Name **und** Mailbox-Verzeichnis im Store | -| [`kerio_trial_setup.py`](kerio_trial_setup.py) | Am Erstkonfigurationsassistenten vorbei, wenn dessen Lizenzschritt klemmt | | [`kerio_users_config.py`](kerio_users_config.py) | Lesen und Ändern von `users.cfg` | | [`kerio_common.py`](kerio_common.py) | Gemeinsame Bausteine: Admin-API-Client, Dienststeuerung | -Beide Scripts laufen **lokal auf dem Kerio-Server als root**, brauchen nur die -Python-Standardbibliothek und bieten `--dry-run`. +Das Script läuft **lokal auf dem Kerio-Server als root**, braucht nur die +Python-Standardbibliothek und bietet `--dry-run`. Getestet gegen **Kerio Connect 10.0.9 patch 2** auf Debian 12. @@ -147,48 +146,55 @@ legt anschließend in der Administrationskonsole einen Alias an. --- -# Erstkonfigurationsassistent überspringen +# Testlizenz besorgen -## Warum +Der Erstkonfigurationsassistent verlangt im Schritt „Registrierte Testaktivierung" +eine Testlizenznummer samt CAPTCHA. Der Link „Lizenznummer für Testversion +erhalten" im Assistenten zeigt noch auf die alte `kerio.com`-Adresse und läuft +ins Leere: GFI hat die Domain auf **`gfi.ai`** umgestellt. -Der Assistent verlangt im Schritt „Registrierte Testaktivierung" eine -Testlizenznummer samt CAPTCHA. Der dokumentierte Link „Im unregistrierten Modus -aktivieren" fehlt je nach Version oder ist im Fenster abgeschnitten, und die -verlinkte Registrierungsseite ist seit der Übernahme durch GFI teilweise tot — -man kommt im Assistenten nicht weiter. +Die funktionierende Adresse — dort die E-Mail-Adresse eintragen, die Nummer kommt +per Mail: -Kerio kennt dafür den Schalter `ConfigWizardDone` in `mailserver.cfg`. Steht er -auf `1`, öffnet die Administrationskonsole den Assistenten nicht mehr. +**** -## Was das bringt — und was nicht +Die alten Adressen `www.gfi.com/.../kerio-connect/download` und +`gfi.ai/.../kerio-connect/download` leiten inzwischen alle dorthin um. -Danach sind **Administrationskonsole und Admin-API voll nutzbar**, und dieses -Repo lässt sich damit testen. +Die Nummer lässt sich entweder im Assistenten eintragen oder später aus der +laufenden Instanz heraus: **Dashboard → „Become a registered trial user"**. +Beides braucht ausgehendes HTTPS auf Port 443. -**Der Mailbetrieb bleibt aber gesperrt.** Ohne Lizenzschlüssel weisen IMAP, POP3 -und Webmail jede Verbindung ab (`Server license expired` beim Client, -`Product name does not match` im Log), und `error.log` meldet beim Start -`No license key found`. Der Schalter überspringt nur den Dialog — er aktiviert -keinen Testzeitraum. +## Ohne Lizenz geht kein Mailbetrieb -Für echten Mailbetrieb führt kein Weg an einer Trial-Lizenznummer vorbei: -**Dashboard → „Become a registered trial user"**, oder auf der GFI-Downloadseite -„Try free for 30 days". Braucht ausgehendes HTTPS auf Port 443. +Der Schalter `ConfigWizardDone` in `mailserver.cfg` überspringt zwar den +Assistenten und gibt Administrationskonsole und Admin-API frei — er aktiviert +aber **keinen Testzeitraum**. Ohne Lizenzschlüssel weisen IMAP, POP3 und Webmail +jede Verbindung ab (`Server license expired` beim Client, `Product name does not +match` im Log), und `error.log` meldet beim Start `No license key found`. Eine +API zum Aktivieren gibt es nicht. -## Benutzung +Wer den Assistenten neu durchlaufen will, setzt `ConfigWizardDone` zurück auf `0` +oder löscht `mailserver.cfg` ganz — Kerio legt beim nächsten Start eine frische +an. Achtung: die Domänendefinition steht in dieser Datei, die Benutzer in +`users.cfg` sind danach verwaist. -```bash -./kerio_trial_setup.py --status # Zustand anzeigen (kein root nötig) -sudo ./kerio_trial_setup.py --skip-wizard # Assistenten überspringen -sudo ./kerio_trial_setup.py --restore # Backup zurückspielen +**`mailserver.cfg` und `users.cfg` dürfen nur bei gestopptem Dienst bearbeitet +werden.** Kerio schreibt seine Konfiguration beim Beenden aus dem Speicher +zurück und überschreibt sonst jede Änderung am laufenden Dienst. + +## Wenn der Schlüssel abgelehnt wird + +Lohnt ein Blick in `store/logs/error.log` — die Meldung dort unterscheidet +zwischen ungültig und abgelaufen: + +``` +registration.cpp: License error: Unable to set license (License is expired). ``` -## Der springende Punkt - -`mailserver.cfg` darf **nur bei gestopptem Dienst** bearbeitet werden. Kerio -schreibt seine Konfiguration beim Beenden aus dem Speicher zurück und würde eine -Änderung am laufenden Dienst wieder überschreiben. Das gilt für `users.cfg` -genauso — beide Scripts halten sich daran. +Und ein DNS-Test: `registration.kerio.com` ist nicht mehr auflösbar, +`secure.kerio.com` schon. Ein Assistent, der noch den alten Host kontaktiert, +kann die Nummer nicht validieren. --- @@ -197,5 +203,5 @@ genauso — beide Scripts halten sich daran. - [Administration API — Users](https://manuals.gfi.com/en/kerio/api/connect/admin/reference/interfacekerio_1_1jsonapi_1_1admin_1_1_users.html) - [Sample communication (Session.login, X-Token)](https://manuals.gfi.com/en/kerio/api/connect/admin/reference/sample_communication.html) - [Registering Kerio Connect (unregistered mode)](https://manuals.gfi.com/en/kerio/connect/content/registration-and-licenses/registering-kerio-connect-1134.html) -- [Initial Wizard Settings Are Not Saving (ConfigWizardDone)](https://support.kerioconnect.gfi.com/article/114858-initial-wizard-settings-are-not-saving-in-kerio-connect) +- [Kerio Connect Testversion (30 Tage)](https://gfi.ai/products-and-solutions/email-and-messaging-solutions/kerioconnect/free-trial) - [Modifying the mailserver.cfg](https://support.kerioconnect.gfi.com/en-us/article/114788-modifying-the-mailserver-cfg) diff --git a/kerio_trial_setup.py b/kerio_trial_setup.py deleted file mode 100755 index 3eab827..0000000 --- a/kerio_trial_setup.py +++ /dev/null @@ -1,360 +0,0 @@ -#!/usr/bin/env python3 -""" -Bringt eine frische Kerio-Connect-Installation am Erstkonfigurationsassistenten -vorbei, wenn dessen Lizenzierungsschritt nicht durchlaufbar ist. - -Hintergrund: Der Assistent verlangt im Schritt "Registrierte Testaktivierung" -eine Testlizenznummer samt CAPTCHA. Der Link "Im unregistrierten Modus -aktivieren" fehlt je nach Version oder ist im Fenster abgeschnitten, und die -verlinkte Registrierungsseite ist seit der Uebernahme durch GFI teilweise tot. - -Kerio kennt dafuer den Schalter ConfigWizardDone in mailserver.cfg. Steht er -auf 1, oeffnet die Administrationskonsole den Assistenten nicht mehr und der -Server laeuft 30 Tage ab Installation im unregistrierten Modus. - -Was im unregistrierten Modus fehlt: Antivirus-Updates, ActiveSync, Greylisting, -Hersteller-Support. Nach 30 Tagen stoppt die Engine. Benutzerverwaltung, -Administrationskonsole und Admin-API sind nicht eingeschraenkt. - -WICHTIG: mailserver.cfg darf nur bei gestopptem Dienst bearbeitet werden. -Kerio schreibt seine Konfiguration beim Beenden aus dem Speicher zurueck und -wuerde eine Aenderung am laufenden Dienst wieder ueberschreiben. Genau das -erledigt dieses Script in der richtigen Reihenfolge. - -Das Script muss LOKAL AUF DEM KERIO-SERVER ALS ROOT laufen. - -Beispiele: - - ./kerio_trial_setup.py --status # nur anzeigen - sudo ./kerio_trial_setup.py --skip-wizard # Assistenten ueberspringen - sudo ./kerio_trial_setup.py --restore # letztes Backup zuruecksetzen -""" - -from __future__ import annotations - -import argparse -import os -import re -import shutil -import sys -import time -from datetime import datetime, timedelta -from pathlib import Path - -from kerio_common import ( - DEFAULT_INSTALL_DIR, - DEFAULT_SERVICE, - AbortedError, - ServiceController, -) - -# Kerio deaktiviert die Engine 30 Tage nach der Installation. -TRIAL_DAYS = 30 - -BACKUP_SUFFIX = ".bak-trial-setup" - -# 0 -WIZARD_PATTERN = re.compile( - r'()([^<]*)()' -) - - -class ConfigFile: - """Liest und schreibt mailserver.cfg mit Backup.""" - - def __init__(self, path: Path) -> None: - self.path = path - if not path.is_file(): - raise AbortedError( - f"'{path}' existiert nicht. Installationsverzeichnis mit " - "--install-dir angeben." - ) - self.text = path.read_text(encoding="utf-8") - - # -- Lesen ------------------------------------------------------------- - - def wizard_done(self) -> str | None: - match = WIZARD_PATTERN.search(self.text) - return match.group(2).strip() if match else None - - def trial_id(self) -> str | None: - match = re.search( - r'([^<]*)', self.text) - return match.group(1).strip() if match else None - - def store_dir(self) -> str | None: - match = re.search( - r'([^<]*)', self.text) - return match.group(1).strip() if match else None - - # -- Schreiben --------------------------------------------------------- - - def set_wizard_done(self, value: str = "1") -> bool: - """Setzt ConfigWizardDone. Gibt False zurueck, wenn schon so.""" - match = WIZARD_PATTERN.search(self.text) - if match is None: - raise AbortedError( - "ConfigWizardDone steht nicht in der Konfiguration. " - "Andere Kerio-Version? Bitte die Datei von Hand pruefen." - ) - if match.group(2).strip() == value: - return False - self.text = WIZARD_PATTERN.sub(rf"\g<1>{value}\g<3>", self.text, count=1) - return True - - def backup(self) -> Path: - target = self.path.with_name(self.path.name + BACKUP_SUFFIX) - shutil.copy2(self.path, target) - return target - - def save(self) -> None: - # Ueber eine temporaere Datei im selben Verzeichnis, damit die - # Konfiguration nie halb geschrieben auf der Platte liegt. - temp = self.path.with_name(self.path.name + ".tmp-trial-setup") - temp.write_text(self.text, encoding="utf-8") - shutil.copystat(self.path, temp) - os.replace(temp, self.path) - - -def estimate_install_date(install_dir: Path, config: ConfigFile) -> datetime | None: - """Schaetzt den Installationszeitpunkt. - - Kerio fuehrt kein Installationsdatum in der Konfiguration, deshalb bleibt - nur der Umweg ueber Zeitstempel: - - 1. der Eintrag der Paketverwaltung - am zuverlaessigsten - 2. sonst der aelteste Zeitstempel unter den Store-Unterverzeichnissen, - die Kerio einmalig beim ersten Start anlegt - - Verzeichnisse wie store/ selbst taugen nicht: ihr Zeitstempel wandert - mit jeder Aenderung darin. Der Wert ist eine Schaetzung, kein Beweis. - """ - for package_file in ( - Path("/var/lib/dpkg/info/kerio-connect.list"), - Path("/var/lib/dpkg/info/kerio-connect:amd64.list"), - ): - if package_file.is_file(): - try: - return datetime.fromtimestamp(package_file.stat().st_mtime) - except OSError: - pass - - store = Path(config.store_dir() or (install_dir / "store")) - stamps = [] - for name in ("queue", "saas", "spamassassin", "logs"): - path = store / name - if path.is_dir(): - try: - stamps.append(path.stat().st_mtime) - except OSError: - continue - if stamps: - return datetime.fromtimestamp(min(stamps)) - return None - - -def print_status(config: ConfigFile, install_dir: Path, - service: ServiceController) -> None: - print("Status") - print("------") - print(f" Konfiguration : {config.path}") - - running = service.is_running() - state = {True: "laeuft", False: "gestoppt", None: "unbekannt"}[running] - print(f" Dienst : {service.describe()} -> {state}") - - wizard = config.wizard_done() - if wizard == "1": - wizard_text = "1 (Assistent wird uebersprungen)" - elif wizard == "0": - wizard_text = "0 (Assistent erscheint beim Oeffnen der Konsole)" - else: - wizard_text = f"{wizard!r} (unerwarteter Wert)" - print(f" ConfigWizard : {wizard_text}") - - trial = config.trial_id() - print(f" TrialID : {trial or '(leer - unregistrierter Modus)'}") - - license_dir = install_dir / "license" - if license_dir.is_dir(): - entries = [e for e in license_dir.iterdir() if not e.name.startswith(".")] - print(f" Lizenzdateien : {len(entries) or 'keine'}") - - installed = estimate_install_date(install_dir, config) - if installed: - expires = installed + timedelta(days=TRIAL_DAYS) - remaining = (expires - datetime.now()).days - print(f" Installiert : ~{installed:%Y-%m-%d %H:%M} (geschaetzt)") - if remaining >= 0: - print(f" Testzeitraum : laeuft ~{expires:%Y-%m-%d} ab " - f"(noch ~{remaining} Tage)") - else: - print(f" Testzeitraum : seit ~{expires:%Y-%m-%d} abgelaufen " - "- die Engine wird deaktiviert sein") - - -def skip_wizard(config: ConfigFile, service: ServiceController, - args: argparse.Namespace) -> int: - if config.wizard_done() == "1": - print("\nConfigWizardDone steht bereits auf 1 - nichts zu tun.") - return 0 - - service.preflight() - - print("\nGeplante Aenderung") - print("------------------") - print(f" {config.path}") - print(" ConfigWizardDone: 0 -> 1") - print(f" Backup nach : {config.path.name}{BACKUP_SUFFIX}") - print(f" Dienst : {service.describe()} (stoppen, aendern, starten)") - - if args.dry_run: - print("\n--dry-run: es wurde nichts geaendert.") - return 0 - - if not args.yes: - print("\nAchtung: der Kerio-Dienst wird dabei kurz gestoppt.") - if input("Jetzt durchfuehren? [j/N] ").strip().lower() not in ("j", "ja", "y", "yes"): - print("Abgebrochen.") - return 1 - - print() - was_running = service.is_running() - service.stop() - - try: - # Erst nach dem Stoppen neu einlesen: Kerio hat die Datei beim - # Beenden zurueckgeschrieben, unsere Kopie von vorhin ist veraltet. - fresh = ConfigFile(config.path) - backup = fresh.backup() - print(f" Backup: {backup}") - - if not fresh.set_wizard_done("1"): - print(" ConfigWizardDone stand nach dem Stoppen schon auf 1.") - else: - fresh.save() - print(" ConfigWizardDone auf 1 gesetzt.") - except (AbortedError, OSError) as exc: - print(f"\nFEHLER: {exc}", file=sys.stderr) - if was_running: - service.start() - return 2 - finally: - if was_running: - service.start() - - # Gegenlesen: haelt der Wert, nachdem Kerio wieder laeuft? - time.sleep(2) - check = ConfigFile(config.path) - if check.wizard_done() != "1": - print("\n WARNUNG: Der Wert steht nach dem Start nicht mehr auf 1. " - "Lief der Dienst beim Schreiben doch noch?", file=sys.stderr) - return 2 - - print("\nFertig. Die Administrationskonsole oeffnet den Assistenten nicht mehr.") - print("Kerio laeuft im unregistrierten Modus - 30 Tage ab Installation.") - return 0 - - -def restore(config_path: Path, service: ServiceController, - args: argparse.Namespace) -> int: - backup = config_path.with_name(config_path.name + BACKUP_SUFFIX) - if not backup.is_file(): - raise AbortedError(f"Kein Backup gefunden: {backup}") - - print(f"\nStellt '{backup}' nach '{config_path}' wieder her.") - if args.dry_run: - print("--dry-run: es wurde nichts geaendert.") - return 0 - if not args.yes: - if input("Jetzt zuruecksetzen? [j/N] ").strip().lower() not in ("j", "ja", "y", "yes"): - print("Abgebrochen.") - return 1 - - print() - was_running = service.is_running() - service.stop() - try: - shutil.copy2(backup, config_path) - print(f" Wiederhergestellt aus {backup}") - finally: - if was_running: - service.start() - return 0 - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Kerio Connect am Erstkonfigurationsassistenten vorbeibringen.", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=__doc__, - ) - parser.add_argument("--install-dir", default=DEFAULT_INSTALL_DIR, - help=f"Kerio-Installationsverzeichnis " - f"(Standard: {DEFAULT_INSTALL_DIR})") - parser.add_argument("--service-name", default=DEFAULT_SERVICE, - help=f"Name des Kerio-Dienstes (Standard: {DEFAULT_SERVICE})") - parser.add_argument("--service-manager", default="auto", - choices=("auto", "systemd", "initd", "none"), - help="Wie der Dienst gestoppt wird") - parser.add_argument("--service-wait", type=int, default=90, - help="Sekunden Wartezeit aufs Stoppen (Standard: 90)") - - action = parser.add_mutually_exclusive_group() - action.add_argument("--status", action="store_true", - help="Nur den Zustand anzeigen (Standard)") - action.add_argument("--skip-wizard", action="store_true", - help="ConfigWizardDone auf 1 setzen") - action.add_argument("--restore", action="store_true", - help="Konfiguration aus dem Backup zuruecksetzen") - - parser.add_argument("--dry-run", action="store_true", - help="Nur anzeigen, was passieren wuerde") - parser.add_argument("--yes", action="store_true", - help="Rueckfrage ueberspringen") - - args = parser.parse_args(argv) - - if (args.skip_wizard or args.restore) and not args.dry_run and os.geteuid() != 0: - parser.error( - "Zum Aendern der Konfiguration werden root-Rechte benoetigt. " - "Mit sudo starten (oder --dry-run zum Testen)." - ) - return args - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - install_dir = Path(args.install_dir) - config_path = install_dir / "mailserver.cfg" - - service = ServiceController( - name=args.service_name, - mode=args.service_manager, - wait_seconds=args.service_wait, - dry_run=args.dry_run, - ) - - try: - if args.restore: - return restore(config_path, service, args) - - config = ConfigFile(config_path) - print_status(config, install_dir, service) - - if args.skip_wizard: - return skip_wizard(config, service, args) - return 0 - except AbortedError as exc: - print(f"\nFEHLER: {exc}", file=sys.stderr) - return 2 - except PermissionError as exc: - print(f"\nFEHLER: Keine Berechtigung: {exc}\nMit sudo starten.", file=sys.stderr) - return 2 - except KeyboardInterrupt: - print("\nAbgebrochen.", file=sys.stderr) - return 130 - - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:]))