--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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c4883d42bb
commit
da64a15486
@@ -0,0 +1,168 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user