#!/usr/bin/env python3 """CLI / Orchestrierung: Plesk-Mail → Kerio-User+POP3-Sammler → Nextcloud-User → PDF.""" import argparse import csv import sys from datetime import datetime from pathlib import Path from typing import Callable, List, Tuple from config import Config from models import Account, AccountResult, StepResult from pdf import ( write_combined_minimal_pdf, write_combined_pdf, write_user_minimal_pdf, write_user_pdf, ) def _detect_delimiter(path: Path) -> str: with open(path, "r", encoding="utf-8-sig") as f: sample = f.read(4096) if sample.count(";") > sample.count(","): return ";" return "," def parse_csv(path) -> List[Account]: path = Path(path) delim = _detect_delimiter(path) accounts: List[Account] = [] with open(path, "r", encoding="utf-8-sig", newline="") as f: reader = csv.DictReader(f, delimiter=delim) for i, raw in enumerate(reader, 2): # row 1 = header row = {(k or "").strip().lower(): (v or "").strip() for k, v in raw.items()} try: quota_raw = row.get("nextcloudspeicher", "") quota = int(quota_raw) if quota_raw else None accounts.append(Account( name=row["name"], vorname=row["vorname"], emailadresse=row["emailadresse"], pleskhost=row["pleskhost"], keriohost=row["keriohost"], nextcloudhost=row["nextcloudhost"], kerioemailkennwort=row["kerioemailkennwort"], pleskemailkennwort=row["pleskemailkennwort"], nextcloudgruppe=row.get("nextcloudgruppe", ""), nextcloudspeicher=quota, nextcloudkennwort=row["nextcloudkennwort"], )) except KeyError as e: raise ValueError(f"CSV Zeile {i}: Spalte fehlt → {e}") from e except ValueError as e: raise ValueError(f"CSV Zeile {i}: ungültiger Wert → {e}") from e return accounts def _make_plesk_client(account: Account, cfg: Config): """Liefert (client, error_class). client=None → Manual-Modus.""" backend = cfg.PLESK_BACKEND if backend == "manual": return None, None if backend == "ssh": from clients.plesk_ssh import PleskSshClient, PleskSshError return PleskSshClient( host=account.pleskhost, ssh_port=cfg.PLESK_SSH_PORT, ssh_user=cfg.PLESK_SSH_USER, ssh_password=cfg.PLESK_SSH_PASSWORD or None, ssh_key=cfg.PLESK_SSH_KEY or None, ssh_key_passphrase=cfg.PLESK_SSH_KEY_PASSPHRASE or None, use_sudo=cfg.PLESK_SSH_USE_SUDO, ), PleskSshError if backend == "api": from clients.plesk import PleskClient, PleskError return PleskClient( host=account.pleskhost, api_key=cfg.PLESK_API_KEY or None, user=cfg.PLESK_USER or None, password=cfg.PLESK_PASSWORD or None, port=cfg.PLESK_PORT, verify=cfg.VERIFY_TLS, ), PleskError raise ValueError(f"Unbekanntes PLESK_BACKEND={backend!r} (erlaubt: manual|api|ssh)") def _deploy_one(account: Account, cfg: Config, log: Callable[[str], None]) -> AccountResult: # Importe lokal, damit die GUI ohne Netzwerk-Modulladen startet. from clients.kerio import KerioClient, KerioError from clients.nextcloud import NextcloudClient, NextcloudError result = AccountResult(account=account) # 1) Plesk if cfg.PLESK_BACKEND == "manual": log(f" → Plesk: MANUELL anzulegen ({account.emailadresse} @ {account.pleskhost})") result.steps.append(StepResult( "Plesk", "manuell", f"Bitte im Plesk-Webinterface anlegen: {account.emailadresse}, " f"Passwort wie unten – Tool fährt mit Kerio/Nextcloud fort.", )) else: log(f" → Plesk Mail anlegen ({cfg.PLESK_BACKEND}): " f"{account.emailadresse} @ {account.pleskhost}") try: plesk, plesk_error_cls = _make_plesk_client(account, cfg) try: if plesk.mail_exists(account.emailadresse): result.steps.append(StepResult( "Plesk", "übersprungen", "Mail existiert bereits")) log(" · existiert bereits, übersprungen") else: plesk.create_mail(account.emailadresse, account.pleskemailkennwort) result.steps.append(StepResult("Plesk", "angelegt")) log(" ✓ angelegt") finally: close = getattr(plesk, "close", None) if callable(close): close() except Exception as e: result.steps.append(StepResult("Plesk", "Fehler", str(e))) log(f" ✗ FEHLER: {e}") # Wir machen mit Kerio/Nextcloud trotzdem weiter – die sind unabhängig. # 2) Kerio log(f" → Kerio User+POP3-Sammler: {account.emailadresse} @ {account.keriohost}") try: kerio = KerioClient( host=account.keriohost, user=cfg.KERIO_USER, password=cfg.KERIO_PASSWORD, port=cfg.KERIO_PORT, verify=cfg.VERIFY_TLS, ) try: if kerio.user_exists(account.emailadresse): result.steps.append(StepResult("Kerio", "übersprungen", "User existiert bereits")) log(" · existiert bereits, POP3-Sammler nicht neu angelegt") else: uid = kerio.create_user( account.emailadresse, account.kerioemailkennwort, account.vollname, ) kerio.add_pop3_collection( kerio_user_id=uid, server=account.pleskhost, login_name=account.emailadresse, password=account.pleskemailkennwort, port=cfg.POP3_PORT, ssl=cfg.POP3_SSL, leave_days=cfg.POP3_KEEP_DAYS, ) result.steps.append(StepResult("Kerio", "angelegt", "inkl. POP3-Sammler")) log(" ✓ angelegt + POP3-Sammler") finally: kerio.logout() except Exception as e: result.steps.append(StepResult("Kerio", "Fehler", str(e))) log(f" ✗ FEHLER: {e}") # 3) Nextcloud log(f" → Nextcloud User: {account.nextcloud_username} @ {account.nextcloudhost}") try: nc = NextcloudClient( host=account.nextcloudhost, admin_user=cfg.NEXTCLOUD_USER, admin_password=cfg.NEXTCLOUD_PASSWORD, verify=cfg.VERIFY_TLS, ) if nc.user_exists(account.nextcloud_username): result.steps.append(StepResult("Nextcloud", "übersprungen", "User existiert bereits")) log(" · existiert bereits, übersprungen") else: if account.nextcloudgruppe: nc.ensure_group(account.nextcloudgruppe) nc.create_user( userid=account.nextcloud_username, password=account.nextcloudkennwort, email=account.emailadresse, display_name=account.vollname, group=account.nextcloudgruppe or None, quota_gb=account.nextcloudspeicher, ) result.steps.append(StepResult("Nextcloud", "angelegt")) log(" ✓ angelegt") except Exception as e: result.steps.append(StepResult("Nextcloud", "Fehler", str(e))) log(f" ✗ FEHLER: {e}") return result def _write_admin_report(path: Path, results: List[AccountResult], cfg: Config, timestamp: str) -> None: lines = [ f"Deploy-Report {timestamp}", f"Plesk-Backend: {cfg.PLESK_BACKEND}", "=" * 70, "", ] for r in results: a = r.account lines.append(f"{a.vollname} <{a.emailadresse}>") for s in r.steps: marker = {"angelegt": "✓", "übersprungen": "·", "manuell": "⚠", "Fehler": "✗"}.get(s.status, "?") detail = f" – {s.detail}" if s.detail else "" lines.append(f" {marker} {s.service:10s} {s.status}{detail}") lines.append("") if cfg.PLESK_BACKEND == "manual": lines += [ "=" * 70, "PLESK MANUELL ANZULEGEN (Shared-Host – kein API/SSH):", " → im Plesk-Webinterface jede Mailadresse oben mit dem", " in der CSV vergebenen Plesk-Mail-Passwort einrichten,", " sonst kann der Kerio-POP3-Sammler die Mails nicht abholen.", ] path.write_text("\n".join(lines), encoding="utf-8") def pdf_only(accounts: List[Account], cfg: Config, output_dir, log: Callable[[str], None] = print) -> Path: """Nur PDFs schreiben – KEIN Plesk/Kerio/Nextcloud-Aufruf. Praktisch wenn die Konten schon angelegt sind oder die CSV nochmal durchgejagt werden soll, um die PDFs neu zu generieren. """ output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) for i, acc in enumerate(accounts, 1): log(f"[{i}/{len(accounts)}] {acc.vollname} <{acc.emailadresse}>") safe = "".join(c if c.isalnum() or c in "-_." else "_" for c in acc.emailadresse) per_pdf = output_dir / f"zugangsdaten_{safe}.pdf" per_min = output_dir / f"zugangsdaten_minimal_{safe}.pdf" write_user_pdf(per_pdf, acc, cfg.SMTP_PORT, cfg.POP3_PORT, cfg.POP3_SSL) write_user_minimal_pdf(per_min, acc, cfg.SMTP_PORT, cfg.IMAP_PORT) log(f" ⤷ PDF: {per_pdf}") log(f" ⤷ PDF (minimal): {per_min}") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") combined = output_dir / f"zugangsdaten_gesamt_{timestamp}.pdf" combined_min = output_dir / f"zugangsdaten_gesamt_minimal_{timestamp}.pdf" write_combined_pdf(combined, accounts, cfg.SMTP_PORT, cfg.POP3_PORT, cfg.POP3_SSL) write_combined_minimal_pdf(combined_min, accounts, cfg.SMTP_PORT, cfg.IMAP_PORT) log(f"✓ Gesamt-PDF: {combined}") log(f"✓ Gesamt-PDF (minimal): {combined_min}") return combined def run_deploy(accounts: List[Account], cfg: Config, output_dir, log: Callable[[str], None] = print ) -> Tuple[List[AccountResult], Path]: output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) results: List[AccountResult] = [] for i, acc in enumerate(accounts, 1): log(f"[{i}/{len(accounts)}] {acc.vollname} <{acc.emailadresse}>") result = _deploy_one(acc, cfg, log) results.append(result) safe = "".join(c if c.isalnum() or c in "-_." else "_" for c in acc.emailadresse) per_pdf = output_dir / f"zugangsdaten_{safe}.pdf" per_min = output_dir / f"zugangsdaten_minimal_{safe}.pdf" write_user_pdf(per_pdf, acc, cfg.SMTP_PORT, cfg.POP3_PORT, cfg.POP3_SSL) write_user_minimal_pdf(per_min, acc, cfg.SMTP_PORT, cfg.IMAP_PORT) log(f" ⤷ PDF: {per_pdf}") log(f" ⤷ PDF (minimal): {per_min}") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") combined = output_dir / f"zugangsdaten_gesamt_{timestamp}.pdf" combined_min = output_dir / f"zugangsdaten_gesamt_minimal_{timestamp}.pdf" write_combined_pdf(combined, accounts, cfg.SMTP_PORT, cfg.POP3_PORT, cfg.POP3_SSL) write_combined_minimal_pdf(combined_min, accounts, cfg.SMTP_PORT, cfg.IMAP_PORT) log(f"✓ Gesamt-PDF: {combined}") log(f"✓ Gesamt-PDF (minimal): {combined_min}") report = output_dir / f"_admin_report_{timestamp}.txt" _write_admin_report(report, results, cfg, timestamp) log(f"✓ Admin-Report (nicht für Kunden!): {report}") return results, combined def main(): p = argparse.ArgumentParser( description="Email/Cloud-Accounts auf Plesk + Kerio + Nextcloud deployen.", ) p.add_argument("--csv", help="Pfad zur CSV-Datei (CLI-Modus)") p.add_argument("--output", default="./output", help="Verzeichnis für PDFs (default ./output)") p.add_argument("--gui", action="store_true", help="GUI starten") p.add_argument("--pdf-only", action="store_true", help="Nur PDFs aus der CSV erzeugen, keine Anlage in Plesk/Kerio/Nextcloud") args = p.parse_args() if args.gui or not args.csv: from gui import launch launch() return cfg = Config() try: accounts = parse_csv(args.csv) except (FileNotFoundError, ValueError) as e: print(f"CSV-Fehler: {e}", file=sys.stderr) sys.exit(2) print(f"{len(accounts)} Account(s) eingelesen.\n") if args.pdf_only: combined = pdf_only(accounts, cfg, args.output) print(f"\nFertig (nur PDF). Gesamt-PDF: {combined}") sys.exit(0) results, combined = run_deploy(accounts, cfg, args.output) failed = sum(1 for r in results if r.has_errors) print(f"\nFertig. {len(results) - failed}/{len(results)} ohne Fehler. Gesamt-PDF: {combined}") sys.exit(1 if failed else 0) if __name__ == "__main__": main()