add manual/api/ssh Plesk-Backends, Minimal-PDF, --pdf-only und GUI-Verbesserungen

- PLESK_BACKEND={manual,api,ssh}: manual als Default für Shared Hosts,
  api unverändert, ssh ruft `plesk bin mail` per paramiko auf.
- POP3_PORT default 995, POP3_SSL mit Auto-Erkennung anhand Port.
- Kerio: User wird mit mayChangePassword=False angelegt.
- Zusätzliche Minimal-PDF (nur Email + Cloud) pro Konto + Sammel-Variante,
  IMAP-Port konfigurierbar.
- CLI-Flag --pdf-only und entsprechender GUI-Button "📄 Nur PDF".
- GUI: Lösch-Button "✕ löschen" sichtbarer, letzte Zeile löschbar.
- PDFs sind kunden-tauglich (kein Status-Block, kein ACHTUNG-Hinweis);
  Anlage-Status separat in _admin_report_<ts>.txt.
- README dokumentiert die Skip-Logik pro Dienst und ihre Caveats.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 12:15:39 +02:00
parent e951c839ca
commit dda5c746ce
6 changed files with 216 additions and 34 deletions
+49 -4
View File
@@ -9,7 +9,10 @@ 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
from pdf import (
write_combined_minimal_pdf, write_combined_pdf,
write_user_minimal_pdf, write_user_pdf,
)
def _detect_delimiter(path: Path) -> str:
@@ -141,7 +144,7 @@ def _deploy_one(account: Account, cfg: Config, log: Callable[[str], None]) -> Ac
server=account.pleskhost,
login_name=account.emailadresse,
password=account.pleskemailkennwort,
port=cfg.POP3_PORT, ssl=True,
port=cfg.POP3_PORT, ssl=cfg.POP3_SSL,
leave_days=cfg.POP3_KEEP_DAYS,
)
result.steps.append(StepResult("Kerio", "angelegt", "inkl. POP3-Sammler"))
@@ -212,6 +215,34 @@ def _write_admin_report(path: Path, results: List[AccountResult],
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]:
@@ -224,13 +255,19 @@ def run_deploy(accounts: List[Account], cfg: Config,
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)
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"
write_combined_pdf(combined, accounts, cfg.SMTP_PORT, cfg.POP3_PORT)
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)
@@ -245,6 +282,8 @@ def main():
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:
@@ -259,6 +298,12 @@ def main():
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}")