270 lines
11 KiB
Python
270 lines
11 KiB
Python
#!/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_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=True,
|
||
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 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"
|
||
write_user_pdf(per_pdf, acc, cfg.SMTP_PORT, cfg.POP3_PORT)
|
||
log(f" ⤷ PDF: {per_pdf}")
|
||
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
combined = output_dir / f"zugangsdaten_gesamt_{timestamp}.pdf"
|
||
write_combined_pdf(combined, accounts, cfg.SMTP_PORT, cfg.POP3_PORT)
|
||
log(f"✓ Gesamt-PDF: {combined}")
|
||
|
||
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")
|
||
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")
|
||
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()
|