Files
kerio-connect-rename-user/tests/test_adopt.py
T
duffyduckandClaude Opus 5 da64a15486 --adopt: verwaistes Mailbox-Verzeichnis einem Benutzer zuordnen
Fuer den Fall, dass der alte Benutzer schon geloescht und der neue von
Hand angelegt wurde - dann liegt nur noch das Verzeichnis herum. users.cfg
bleibt unangetastet, es wandert allein das Verzeichnis.

Geprueft wird vorher, dass der Zielbenutzer existiert und der alte nicht
mehr; gibt es ihn noch, verweist das Script auf die normale Umbenennung.
Hat das Ziel schon ein Verzeichnis mit Mails, bricht es ab - mit --force
wird das bestehende als <name>.abgeloest-<zeitstempel> beiseitegeschoben
statt geloescht.

Die Zielpruefung arbeitet jetzt als Folge statt paarweise: bei der
Uebernahme raeumt der erste Schritt das Ziel, das der zweite belegt. Eine
isolierte Pruefung haette das als Konflikt gemeldet.

Dazu die Kontaktkarte: .personal traegt Namen und Adresse des vorherigen
Eigentuemers. Sie wird jetzt umgeschrieben statt entfernt - Kerio legt sie
naemlich nicht neu an, wenn sie fehlt, weder beim Neustart noch bei einer
Aenderung ueber die Admin-API. Das gilt auch fuer die normale Umbenennung,
wo bisher die alte Adresse stehenblieb.

Beides gegen den Testserver verifiziert: nach der Uebernahme sieht der
Zielbenutzer alle Mails samt Unterordnern und Flags, die Kontaktkarte
stimmt, users.cfg ist unveraendert und neue Mail wird zugestellt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 14:35:51 +02:00

169 lines
7.0 KiB
Python

"""Testet die Uebernahme eines verwaisten Mailbox-Verzeichnisses (--adopt)."""
import sys, tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from kerio_rename_user import (
plan_store_adoption, check_rename_targets, apply_store_renames,
count_mails, revert_store_renames, AbortedError,
)
STAMP = "20260807-120000"
ok = True
def check(label, cond):
global ok
print(f" {'ok ' if cond else 'FEHLER'} {label}")
if not cond:
ok = False
def build_store(with_target=True, target_mails=0):
"""Baut einen Store: verwaistes Verzeichnis + optional Ziel des neuen Benutzers."""
root = Path(tempfile.mkdtemp())
store = root / "store"
alt = store / "mail" / "firma.de" / "anna.mueller"
(alt / "INBOX" / "#msgs").mkdir(parents=True)
for i in (1, 2, 3):
(alt / "INBOX" / "#msgs" / f"0000000{i}.eml").write_text(f"Mail {i}")
(alt / ".personal").write_text("vCard von Anna Mueller")
(alt / "stats.usr").write_text("Statistik alt")
if with_target:
neu = store / "mail" / "firma.de" / "anna.schmidt"
(neu / "INBOX" / "#msgs").mkdir(parents=True)
(neu / ".personal").write_text("vCard von Anna Schmidt")
for i in range(target_mails):
(neu / "INBOX" / "#msgs" / f"9000000{i}.eml").write_text(f"Zielmail {i}")
(store / "mail" / "firma.de" / "fremder").mkdir(parents=True)
return store
print("== 1. Zielverzeichnis existiert, ist aber leer ==")
store = build_store(with_target=True, target_mails=0)
moves = plan_store_adoption(store, "firma.de", "anna.mueller", "anna.schmidt",
STAMP, force=False)
check("zwei Schritte geplant (beiseite + uebernehmen)", len(moves) == 2)
check("erst wird das Ziel beiseitegeschoben",
".abgeloest-" in moves[0][1].name and moves[0][0].name == "anna.schmidt")
check("dann rueckt die Quelle nach",
moves[1][0].name == "anna.mueller" and moves[1][1].name == "anna.schmidt")
check_rename_targets(moves)
print(" Sequenzpruefung akzeptiert die Abfolge")
done = apply_store_renames(moves)
ziel = store / "mail" / "firma.de" / "anna.schmidt"
check("3 Mails beim neuen Benutzer", count_mails(ziel) == 3)
check("alte Mail-Datei inhaltlich da",
(ziel / "INBOX" / "#msgs" / "00000001.eml").read_text() == "Mail 1")
check("abgeloestes Zielverzeichnis daneben",
(store / "mail" / "firma.de" / f"anna.schmidt.abgeloest-{STAMP}").is_dir())
check("Quellverzeichnis weg",
not (store / "mail" / "firma.de" / "anna.mueller").exists())
check("Fremder unberuehrt", (store / "mail" / "firma.de" / "fremder").is_dir())
print("\n== 2. Rollback nimmt beide Schritte zurueck ==")
revert_store_renames(done)
check("Quelle wieder da",
(store / "mail" / "firma.de" / "anna.mueller" / "INBOX" / "#msgs" /
"00000001.eml").read_text() == "Mail 1")
check("Ziel wieder am Platz",
(store / "mail" / "firma.de" / "anna.schmidt" / ".personal").read_text()
== "vCard von Anna Schmidt")
check("kein abgeloestes Verzeichnis mehr",
not (store / "mail" / "firma.de" / f"anna.schmidt.abgeloest-{STAMP}").exists())
print("\n== 3. Ziel enthaelt schon Mails -> Abbruch ohne --force ==")
store = build_store(with_target=True, target_mails=2)
try:
plan_store_adoption(store, "firma.de", "anna.mueller", "anna.schmidt",
STAMP, force=False)
check("haette abbrechen muessen", False)
except AbortedError as exc:
check("bricht ab und nennt die Anzahl", "2 Mail" in str(exc))
print("\n== 4. ... mit --force geht es, Mails bleiben erhalten ==")
moves = plan_store_adoption(store, "firma.de", "anna.mueller", "anna.schmidt",
STAMP, force=True)
check_rename_targets(moves)
apply_store_renames(moves)
abgeloest = store / "mail" / "firma.de" / f"anna.schmidt.abgeloest-{STAMP}"
check("die 2 Zielmails sind erhalten geblieben", count_mails(abgeloest) == 2)
check("die 3 uebernommenen Mails sind am Platz",
count_mails(store / "mail" / "firma.de" / "anna.schmidt") == 3)
print("\n== 5. Kein Zielverzeichnis vorhanden -> nur ein Schritt ==")
store = build_store(with_target=False)
moves = plan_store_adoption(store, "firma.de", "anna.mueller", "anna.schmidt",
STAMP, force=False)
check("genau ein Schritt", len(moves) == 1)
check_rename_targets(moves)
apply_store_renames(moves)
check("Mails angekommen",
count_mails(store / "mail" / "firma.de" / "anna.schmidt") == 3)
print("\n== 6. Quellverzeichnis gibt es nicht -> klare Meldung ==")
try:
plan_store_adoption(store, "firma.de", "gibtsnicht", "anna.schmidt",
STAMP, force=False)
check("haette abbrechen muessen", False)
except AbortedError as exc:
check("klare Fehlermeldung", "gibtsnicht" in str(exc))
print("\n== 7. Sequenzpruefung erkennt zwei Moves auf dasselbe Ziel ==")
store = build_store(with_target=False)
d = store / "mail" / "firma.de"
try:
check_rename_targets([(d / "anna.mueller", d / "ziel"),
(d / "fremder", d / "ziel")])
check("haette abbrechen muessen", False)
except AbortedError as exc:
check("Kollision erkannt", "Zwei Verschiebungen" in str(exc))
# ---------------------------------------------------------------------------
print("\n== 8. Kontaktkarte (.personal) umschreiben ==")
from kerio_rename_user import rewrite_personal_vcard
mb = Path(tempfile.mkdtemp())
VCARD = (b"Subject: Maxi Musterfrau\r\n"
b"Date: Fri, 7 Aug 2026 11:33:20 +0000\r\n"
b'Content-Type: text/vcard; charset="utf-8"\r\n'
b"\r\n"
b"BEGIN:VCARD\r\n"
b"VERSION:3.0\r\n"
b"EMAIL;TYPE=PREF,WORK:maxi.musterfrau@local.lan\r\n"
b"FN:Maxi Musterfrau\r\n"
b"X-JABBER:maxi.musterfrau@local.lan\r\n"
b"END:VCARD\r\n")
(mb / ".personal").write_bytes(VCARD)
changed = rewrite_personal_vcard(mb, "maxi.musterfrau@local.lan",
"anna.neu@local.lan", "Anna Neu")
out = (mb / ".personal").read_bytes()
check("meldet Aenderung", changed is True)
check("EMAIL umgeschrieben", b"EMAIL;TYPE=PREF,WORK:anna.neu@local.lan\r\n" in out)
check("X-JABBER umgeschrieben", b"X-JABBER:anna.neu@local.lan\r\n" in out)
check("FN umgeschrieben", b"FN:Anna Neu\r\n" in out)
check("Subject umgeschrieben", b"Subject: Anna Neu\r\n" in out)
check("keine alte Adresse mehr", b"maxi.musterfrau" not in out)
check("CRLF erhalten", out.count(b"\r\n") == VCARD.count(b"\r\n"))
check("VCARD-Struktur intakt",
out.startswith(b"Subject:") and out.rstrip().endswith(b"END:VCARD"))
print("\n== 9. Kontaktkarte fehlt -> kein Fehler ==")
leer = Path(tempfile.mkdtemp())
check("meldet 'nichts geaendert'",
rewrite_personal_vcard(leer, "a@b.c", "d@e.f", "X") is False)
print("\n== 10. Nichts zu ersetzen -> Datei bleibt unveraendert ==")
mb2 = Path(tempfile.mkdtemp())
(mb2 / ".personal").write_bytes(VCARD)
rewrite_personal_vcard(mb2, "fremder@local.lan", "wer@local.lan", None)
check("Datei unveraendert", (mb2 / ".personal").read_bytes() == VCARD)
print("\n" + ("ALLE TESTS BESTANDEN" if ok else "FEHLGESCHLAGEN"))
sys.exit(0 if ok else 1)