This commit is contained in:
2026-05-06 08:41:06 +02:00
parent 8f0674b6c2
commit 3d98c9ec9c
38 changed files with 87867 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
# ══════════════════════════════════════════════════════
# caldav_crud.py — Konfigurationsvorlage
# Kopieren nach: ~/.openclaw/.env
# Berechtigungen setzen: chmod 600 ~/.openclaw/.env
# ══════════════════════════════════════════════════════
# CalDAV-Server (mailbox.org Standard — nicht ändern nötig)
MAILBOX_DAV_URL="https://dav.mailbox.org"
# Deine mailbox.org E-Mail-Adresse
MAILBOX_DAV_USERNAME="minitux@mailbox.org"
# App-Passwort (NICHT dein Haupt-Passwort!)
# Erstellen unter: mailbox.org → Einstellungen → Sicherheit → App-Passwörter
#
# EMPFEHLUNG: Dieses Feld leer lassen und stattdessen keyring verwenden:
# ./caldav_crud.py store-password
#
# Nur als Fallback (z.B. auf Headless-Servern ohne keyring-Backend):
MAILBOX_DAV_PASSWORD="dbba-guvm-perd-pdhq"
# Kalenderbezeichnung (optional)
# Leer lassen → ersten verfügbaren Kalender verwenden
# Exakter Name wie er in mailbox.org angezeigt wird (z.B. "Persönlich")
MAILBOX_DAV_CALENDAR=
# ══════════════════════════════════════════════════════
# Sicherheitshinweise:
# • Nie diese Datei in git committen (.gitignore eintragen)
# • chmod 600 ~/.openclaw/.env (nur Owner lesbar)
# • App-Passwort in mailbox.org hat eingeschränkten Scope
# • Bevorzuge keyring für interaktive Umgebungen
# ══════════════════════════════════════════════════════
+151
View File
@@ -0,0 +1,151 @@
---
name: caldav
description: "CRUD operations on mailbox.org CalDAV calendars. Use when: user asks to list, create, read, update or delete calendar events. NOT for: contacts (CardDAV), tasks/todos, or other CalDAV providers."
homepage: https://dav.mailbox.org
metadata:
{
"openclaw":
{
"emoji": "📅",
"requires": { "bins": ["python3"] },
"install":
[
{
"id": "pip",
"kind": "run",
"label": "Install Python dependencies",
"run": "pip install caldav python-dotenv keyring vobject pytz",
},
],
},
}
---
# CalDAV Skill (mailbox.org)
Lesen und Verwalten von Kalenderterminen via CalDAV auf mailbox.org.
## When to Use
**USE this skill when:**
- "Zeig mir meine Termine"
- "Was habe ich heute / diese Woche?"
- "Erstelle einen Termin für morgen um 10 Uhr"
- "Verschiebe meinen Termin auf Freitag"
- "Lösche den Termin Arztbesuch"
- "Ändere den Ort des Meetings"
## When NOT to Use
**DON'T use this skill when:**
- Kontakte verwalten → CardDAV verwenden
- Aufgaben/Todos → separate Tasks-API
- Andere CalDAV-Anbieter (Google, Apple) → andere Skills
## Setup
Credentials in `~/.openclaw/.env` eintragen:
```bash
MAILBOX_DAV_URL=https://dav.mailbox.org
MAILBOX_DAV_USERNAME=deine@adresse.mailbox.org
# Passwort besser via keyring:
~/.openclaw/skills/caldav/caldav_crud.py store-password
```
## Commands
### Alle Termine auflisten
```bash
~/.openclaw/skills/caldav/caldav_crud.py list
```
### Termin erstellen
```bash
~/.openclaw/skills/caldav/caldav_crud.py create \
--summary "Titel" \
--start "2026-04-25T10:00" \
--end "2026-04-25T11:00" \
--description "Optionale Beschreibung" \
--location "Wien"
```
### Termin lesen (Details)
```bash
~/.openclaw/skills/caldav/caldav_crud.py read --uid "<UID>"
```
### Termin aktualisieren
```bash
# Titel ändern
~/.openclaw/skills/caldav/caldav_crud.py update --uid "<UID>" --summary "Neuer Titel"
# Zeit verschieben
~/.openclaw/skills/caldav/caldav_crud.py update --uid "<UID>" \
--start "2026-04-26T14:00" \
--end "2026-04-26T15:00"
# Ort ändern
~/.openclaw/skills/caldav/caldav_crud.py update --uid "<UID>" --location "Graz"
```
### Termin löschen
```bash
# Mit Bestätigungsabfrage
~/.openclaw/skills/caldav/caldav_crud.py delete --uid "<UID>"
# Ohne Bestätigung (für Agenten-Aufrufe)
~/.openclaw/skills/caldav/caldav_crud.py delete --uid "<UID>" --force
```
### Passwort-Verwaltung
```bash
# Einmalig im System-Schlüsselbund speichern (empfohlen)
~/.openclaw/skills/caldav/caldav_crud.py store-password
# Passwort aus Schlüsselbund entfernen
~/.openclaw/skills/caldav/caldav_crud.py delete-password
```
## Workflow-Beispiele
**"Zeig mir meine Termine für heute"**
```bash
~/.openclaw/skills/caldav/caldav_crud.py list
# → Ausgabe filtern nach heutigem Datum
```
**"Erstelle morgen um 15 Uhr einen Termin Zahnarzt"**
```bash
~/.openclaw/skills/caldav/caldav_crud.py create \
--summary "Zahnarzt" \
--start "2026-04-24T15:00" \
--end "2026-04-24T16:00"
```
**"Verschiebe den Termin Meeting auf Freitag 10 Uhr"**
```bash
# Zuerst UID aus list ermitteln, dann:
~/.openclaw/skills/caldav/caldav_crud.py update \
--uid "<UID-aus-list>" \
--start "2026-04-25T10:00" \
--end "2026-04-25T11:00"
```
## Notes
- Authentifizierung: keyring (bevorzugt) → `.env` → interaktive Eingabe
- App-Passwort von mailbox.org verwenden, NICHT das Haupt-Passwort
- UID eines Events über `list` ermitteln
- `--verbose` Flag für Debug-Ausgabe verfügbar
+581
View File
@@ -0,0 +1,581 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
caldav_crud.py — Sichere CalDAV CRUD-Operationen für mailbox.org
=================================================================
Authentifizierung: python-keyring (bevorzugt) → .env-Datei → Umgebungsvariablen
Abhängigkeiten:
pip install caldav python-dotenv keyring vobject
Verwendung:
chmod +x caldav_crud.py
./caldav_crud.py --help
./caldav_crud.py list
./caldav_crud.py create --summary "Meeting" --start 2026-04-24T10:00 --end 2026-04-24T11:00
./caldav_crud.py read --uid <event-uid>
./caldav_crud.py update --uid <event-uid> --summary "Neuer Titel"
./caldav_crud.py delete --uid <event-uid>
./caldav_crud.py store-password # Passwort einmalig in keyring speichern
./caldav_crud.py delete-password # Passwort aus keyring entfernen
"""
import argparse
import logging
import os
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path
# ── Drittanbieter-Importe mit Fehlerhinweis ──────────────────────────────────
try:
import caldav
except ImportError:
sys.exit("Fehlend: caldav → pip install caldav")
try:
from dotenv import load_dotenv
except ImportError:
sys.exit("Fehlend: python-dotenv → pip install python-dotenv")
try:
import keyring
import keyring.errors
HAS_KEYRING = True
except ImportError:
HAS_KEYRING = False
print("Hinweis: keyring nicht verfügbar, falle zurück auf .env / Umgebungsvariablen.",
file=sys.stderr)
try:
import vobject
except ImportError:
sys.exit("Fehlend: vobject → pip install vobject")
try:
import pytz
except ImportError:
sys.exit("Fehlend: pytz → pip install pytz")
# ── Konstanten ───────────────────────────────────────────────────────────────
KEYRING_SERVICE = "mailbox_caldav"
KEYRING_USERNAME = "caldav_password"
ENV_FILE_PATHS = [
Path.home() / ".openclaw" / ".env", # OpenClaw-Standardpfad
Path(".env"), # Aktuelles Verzeichnis (Fallback)
]
DEFAULT_CALENDAR_NAME = None # None = ersten verfügbaren Kalender nutzen
# ── Logging ──────────────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.WARNING,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S",
)
log = logging.getLogger(__name__)
# ═══════════════════════════════════════════════════════════════════════════════
# Authentifizierung & Konfiguration
# ═══════════════════════════════════════════════════════════════════════════════
def load_env() -> None:
"""Lädt .env-Datei aus bekannten Pfaden (erste gefundene gewinnt)."""
for path in ENV_FILE_PATHS:
if path.exists():
load_dotenv(dotenv_path=path, override=False)
log.info(f".env geladen von: {path}")
return
log.info("Keine .env-Datei gefunden — verwende reine Umgebungsvariablen.")
def get_password(username: str) -> str:
"""
Passwort-Abruf-Reihenfolge (sicherste zuerst):
1. python-keyring (systemischer Schlüsselbund / libsecret)
2. Umgebungsvariable MAILBOX_DAV_PASSWORD (aus .env oder Shell)
3. Interaktive Eingabe (Fallback, kein Echo)
"""
# 1. Keyring
if HAS_KEYRING:
try:
password = keyring.get_password(KEYRING_SERVICE, KEYRING_USERNAME)
if password:
log.info("Passwort aus keyring geladen.")
return password
except keyring.errors.KeyringError as exc:
log.warning(f"Keyring-Fehler: {exc} — falle zurück auf .env")
# 2. Umgebungsvariable
password = os.getenv("MAILBOX_DAV_PASSWORD")
if password:
log.info("Passwort aus Umgebungsvariable MAILBOX_DAV_PASSWORD geladen.")
return password
# 3. Interaktive Eingabe
import getpass
print(f"Kein gespeichertes Passwort für '{username}' gefunden.")
print("Tipp: Einmalig speichern mit: ./caldav_crud.py store-password")
return getpass.getpass(f"App-Passwort für {username}: ")
def get_config() -> dict:
"""
Liest Konfiguration aus Umgebungsvariablen.
Priorität: keyring-Passwort > MAILBOX_DAV_PASSWORD > interaktiv.
"""
load_env()
url = os.getenv("MAILBOX_DAV_URL", "https://dav.mailbox.org")
username = os.getenv("MAILBOX_DAV_USERNAME", "")
calendar = os.getenv("MAILBOX_DAV_CALENDAR", "") # optional: Kalenderbezeichnung
if not username:
sys.exit(
"Fehler: MAILBOX_DAV_USERNAME ist nicht gesetzt.\n"
" → Setze ihn in ~/.openclaw/.env oder als Umgebungsvariable."
)
password = get_password(username)
return {
"url": url,
"username": username,
"password": password,
"calendar": calendar or None,
}
# ═══════════════════════════════════════════════════════════════════════════════
# CalDAV-Verbindung
# ═══════════════════════════════════════════════════════════════════════════════
def connect(cfg: dict) -> caldav.DAVClient:
"""Baut eine authentifizierte DAV-Verbindung auf."""
client = caldav.DAVClient(
url=cfg["url"],
username=cfg["username"],
password=cfg["password"],
)
# Verbindungstest
try:
client.principal()
log.info(f"Verbunden mit {cfg['url']} als {cfg['username']}")
except Exception as exc:
sys.exit(f"Verbindungsfehler: {exc}")
return client
def get_calendar(client: caldav.DAVClient, calendar_name: str | None = None) -> caldav.Calendar:
"""
Gibt den gewünschten Kalender zurück.
Wenn calendar_name=None → ersten verfügbaren Kalender verwenden.
"""
principal = client.principal()
calendars = principal.calendars()
if not calendars:
sys.exit("Keine Kalender auf dem Server gefunden.")
if calendar_name:
for cal in calendars:
if cal.name and cal.name.lower() == calendar_name.lower():
return cal
available = ", ".join(c.name or "<unnamed>" for c in calendars)
sys.exit(
f"Kalender '{calendar_name}' nicht gefunden.\n"
f"Verfügbar: {available}"
)
return calendars[0]
# ═══════════════════════════════════════════════════════════════════════════════
# iCalendar-Hilfsfunktionen
# ═══════════════════════════════════════════════════════════════════════════════
def _to_vobject_dt(dt: datetime) -> datetime:
"""
vobject kann stdlib timezone.utc nicht erkennen → in pytz.UTC konvertieren.
Alle anderen tz-Infos werden ebenfalls in pytz-Objekte überführt.
"""
if dt.tzinfo is None:
return dt.replace(tzinfo=pytz.UTC)
if dt.tzinfo is timezone.utc or str(dt.tzinfo) == "UTC":
return dt.replace(tzinfo=pytz.UTC)
# Versuche, den IANA-Namen zu ermitteln (bei zoneinfo / pytz-Objekten)
try:
iana_name = dt.tzname()
if iana_name and iana_name != "UTC":
return dt.astimezone(pytz.timezone(iana_name))
except Exception:
pass
return dt.replace(tzinfo=pytz.UTC)
def make_vcalendar(
uid: str,
summary: str,
start: datetime,
end: datetime,
description: str = "",
location: str = "",
) -> str:
"""Erstellt einen validen VCALENDAR-String (RFC 5545)."""
cal = vobject.iCalendar()
cal.add("prodid").value = "-//caldav_crud.py//mailbox.org//DE"
cal.add("version").value = "2.0"
event = cal.add("vevent")
event.add("uid").value = uid
event.add("summary").value = summary
event.add("dtstart").value = _to_vobject_dt(start)
event.add("dtend").value = _to_vobject_dt(end)
event.add("dtstamp").value = _to_vobject_dt(datetime.now(tz=timezone.utc))
if description:
event.add("description").value = description
if location:
event.add("location").value = location
return cal.serialize()
def parse_datetime(value: str) -> datetime:
"""
Parst ISO-8601-Datetime-Strings flexibel.
Unterstützt: 2026-04-24T10:00, 2026-04-24T10:00:00, 2026-04-24T10:00+02:00
"""
formats = [
"%Y-%m-%dT%H:%M:%S%z",
"%Y-%m-%dT%H:%M%z",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%dT%H:%M",
"%Y-%m-%d",
]
for fmt in formats:
try:
dt = datetime.strptime(value, fmt)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
except ValueError:
continue
sys.exit(
f"Ungültiges Datum/Uhrzeit-Format: '{value}'\n"
f"Erwartet z.B.: 2026-04-24T10:00 oder 2026-04-24T10:00:00+02:00"
)
def extract_event_info(event: caldav.Event) -> dict:
"""Extrahiert lesbare Felder aus einem CalDAV-Event-Objekt."""
try:
vcal = vobject.readOne(event.data)
vevent = vcal.vevent
return {
"uid": getattr(vevent, "uid", None) and vevent.uid.value,
"summary": getattr(vevent, "summary", None) and vevent.summary.value,
"dtstart": getattr(vevent, "dtstart", None) and str(vevent.dtstart.value),
"dtend": getattr(vevent, "dtend", None) and str(vevent.dtend.value),
"description": getattr(vevent, "description", None) and vevent.description.value,
"location": getattr(vevent, "location", None) and vevent.location.value,
}
except Exception as exc:
log.warning(f"Event-Parsing-Fehler: {exc}")
return {"raw": event.data}
# ═══════════════════════════════════════════════════════════════════════════════
# CRUD-Operationen
# ═══════════════════════════════════════════════════════════════════════════════
def cmd_list(args, cal: caldav.Calendar) -> None:
"""READ-ALL: Alle Events auflisten."""
results = cal.events()
if not results:
print("Keine Events im Kalender gefunden.")
return
print(f"\n{'UID':<38} {'Start':<22} Titel")
print("-" * 90)
for ev in results:
info = extract_event_info(ev)
uid = (info.get("uid") or "")[:36]
start = (info.get("dtstart") or "")[:22]
summary = (info.get("summary") or "<kein Titel>")[:50]
print(f"{uid:<38} {start:<22} {summary}")
def cmd_create(args, cal: caldav.Calendar) -> None:
"""CREATE: Neues Event anlegen."""
if not args.summary:
sys.exit("Fehler: --summary ist Pflichtfeld für 'create'")
if not args.start:
sys.exit("Fehler: --start ist Pflichtfeld für 'create'")
if not args.end:
sys.exit("Fehler: --end ist Pflichtfeld für 'create'")
event_uid = args.uid or str(uuid.uuid4())
start_dt = parse_datetime(args.start)
end_dt = parse_datetime(args.end)
ical_data = make_vcalendar(
uid=event_uid,
summary=args.summary,
start=start_dt,
end=end_dt,
description=args.description or "",
location=args.location or "",
)
cal.save_event(ical_data)
print(f"✓ Event erstellt UID: {event_uid}")
def cmd_read(args, cal: caldav.Calendar) -> None:
"""READ: Einzelnes Event anzeigen."""
if not args.uid:
sys.exit("Fehler: --uid ist Pflichtfeld für 'read'")
event = _find_event(cal, args.uid)
info = extract_event_info(event)
print(f"\n── Event ──────────────────────────────────")
for key, val in info.items():
if val:
print(f" {key:<12}: {val}")
print()
def cmd_update(args, cal: caldav.Calendar) -> None:
"""UPDATE: Vorhandenes Event bearbeiten."""
if not args.uid:
sys.exit("Fehler: --uid ist Pflichtfeld für 'update'")
event = _find_event(cal, args.uid)
vcal = vobject.readOne(event.data)
vevent = vcal.vevent
if args.summary:
vevent.summary.value = args.summary
if args.start:
vevent.dtstart.value = _to_vobject_dt(parse_datetime(args.start))
if args.end:
vevent.dtend.value = _to_vobject_dt(parse_datetime(args.end))
if args.description:
if hasattr(vevent, "description"):
vevent.description.value = args.description
else:
vevent.add("description").value = args.description
if args.location:
if hasattr(vevent, "location"):
vevent.location.value = args.location
else:
vevent.add("location").value = args.location
# DTSTAMP aktualisieren
vevent.dtstamp.value = _to_vobject_dt(datetime.now(tz=timezone.utc))
event.data = vcal.serialize()
event.save()
print(f"✓ Event aktualisiert UID: {args.uid}")
def cmd_delete(args, cal: caldav.Calendar) -> None:
"""DELETE: Event löschen."""
if not args.uid:
sys.exit("Fehler: --uid ist Pflichtfeld für 'delete'")
event = _find_event(cal, args.uid)
if not args.force:
info = extract_event_info(event)
print(f"Event zum Löschen: {info.get('summary', '<kein Titel>')} ({args.uid})")
confirm = input("Wirklich löschen? [j/N] ").strip().lower()
if confirm not in ("j", "ja", "y", "yes"):
print("Abgebrochen.")
return
event.delete()
print(f"✓ Event gelöscht UID: {args.uid}")
def _find_event(cal: caldav.Calendar, uid: str) -> caldav.Event:
"""Sucht ein Event anhand seiner UID — wirft SystemExit wenn nicht gefunden."""
results = cal.events()
for ev in results:
info = extract_event_info(ev)
if info.get("uid") == uid:
return ev
# Fallback: direkte URL-basierte Suche via search
try:
events = cal.search(uid=uid, event=True)
if events:
return events[0]
except Exception:
pass
sys.exit(f"Event mit UID '{uid}' nicht gefunden.")
# ═══════════════════════════════════════════════════════════════════════════════
# Keyring-Verwaltung
# ═══════════════════════════════════════════════════════════════════════════════
def cmd_store_password(args) -> None:
"""Speichert das App-Passwort einmalig im System-Schlüsselbund."""
if not HAS_KEYRING:
sys.exit("Fehler: python-keyring ist nicht installiert.\n"
" → pip install keyring")
import getpass
load_env()
username = os.getenv("MAILBOX_DAV_USERNAME", "")
if not username:
username = input("mailbox.org Benutzername (E-Mail): ").strip()
password = getpass.getpass(f"App-Passwort für '{username}' (wird nicht angezeigt): ")
if not password:
sys.exit("Abgebrochen — kein Passwort eingegeben.")
try:
keyring.set_password(KEYRING_SERVICE, KEYRING_USERNAME, password)
print(f"✓ Passwort sicher im System-Schlüsselbund gespeichert.")
print(f" Service : {KEYRING_SERVICE}")
print(f" Username : {KEYRING_USERNAME}")
print(f" Hinweis : MAILBOX_DAV_PASSWORD in .env kann jetzt entfernt werden.")
except keyring.errors.KeyringError as exc:
sys.exit(f"Keyring-Fehler: {exc}")
def cmd_delete_password(args) -> None:
"""Entfernt das gespeicherte Passwort aus dem System-Schlüsselbund."""
if not HAS_KEYRING:
sys.exit("Fehler: python-keyring ist nicht installiert.")
try:
keyring.delete_password(KEYRING_SERVICE, KEYRING_USERNAME)
print(f"✓ Passwort aus keyring entfernt.")
except keyring.errors.PasswordDeleteError:
print("Kein Passwort im keyring gespeichert.")
except keyring.errors.KeyringError as exc:
sys.exit(f"Keyring-Fehler: {exc}")
# ═══════════════════════════════════════════════════════════════════════════════
# CLI-Argument-Parser
# ═══════════════════════════════════════════════════════════════════════════════
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="caldav_crud.py",
description="Sichere CalDAV CRUD-Operationen für mailbox.org",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Beispiele:
./caldav_crud.py store-password
./caldav_crud.py list
./caldav_crud.py create --summary "Arzttermin" --start 2026-04-25T09:00 --end 2026-04-25T10:00
./caldav_crud.py read --uid 550e8400-e29b-41d4-a716-446655440000
./caldav_crud.py update --uid 550e8400-e29b-41d4-a716-446655440000 --summary "Verschoben"
./caldav_crud.py delete --uid 550e8400-e29b-41d4-a716-446655440000 --force
Umgebungsvariablen (.env oder Shell):
MAILBOX_DAV_URL CalDAV-Server (Standard: https://dav.mailbox.org)
MAILBOX_DAV_USERNAME E-Mail-Adresse oder App-Benutzername
MAILBOX_DAV_PASSWORD App-Passwort (besser: keyring verwenden)
MAILBOX_DAV_CALENDAR Kalenderbezeichnung (optional, Standard: erster Kalender)
""",
)
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="Ausführliche Ausgabe (DEBUG-Level)"
)
parser.add_argument(
"--calendar",
metavar="NAME",
help="Kalenderbezeichnung (überschreibt MAILBOX_DAV_CALENDAR)"
)
subparsers = parser.add_subparsers(dest="command", metavar="BEFEHL")
subparsers.required = True
# ── list ──────────────────────────────────────────────────────────────
subparsers.add_parser("list", help="Alle Events auflisten")
# ── create ────────────────────────────────────────────────────────────
p_create = subparsers.add_parser("create", help="Neues Event anlegen")
p_create.add_argument("--summary", required=True, help="Titel des Events")
p_create.add_argument("--start", required=True, help="Start (ISO-8601, z.B. 2026-04-25T10:00)")
p_create.add_argument("--end", required=True, help="Ende (ISO-8601)")
p_create.add_argument("--description", default="", help="Beschreibung (optional)")
p_create.add_argument("--location", default="", help="Ort (optional)")
p_create.add_argument("--uid", default=None, help="Eigene UID (Standard: auto-generiert)")
# ── read ──────────────────────────────────────────────────────────────
p_read = subparsers.add_parser("read", help="Einzelnes Event anzeigen")
p_read.add_argument("--uid", required=True, help="UID des Events")
# ── update ────────────────────────────────────────────────────────────
p_update = subparsers.add_parser("update", help="Vorhandenes Event bearbeiten")
p_update.add_argument("--uid", required=True, help="UID des Events")
p_update.add_argument("--summary", default=None, help="Neuer Titel")
p_update.add_argument("--start", default=None, help="Neuer Start (ISO-8601)")
p_update.add_argument("--end", default=None, help="Neues Ende (ISO-8601)")
p_update.add_argument("--description", default=None, help="Neue Beschreibung")
p_update.add_argument("--location", default=None, help="Neuer Ort")
# ── delete ────────────────────────────────────────────────────────────
p_delete = subparsers.add_parser("delete", help="Event löschen")
p_delete.add_argument("--uid", required=True, help="UID des Events")
p_delete.add_argument("--force", action="store_true", help="Ohne Bestätigungsabfrage löschen")
# ── Passwort-Verwaltung ───────────────────────────────────────────────
subparsers.add_parser("store-password", help="App-Passwort sicher im Schlüsselbund speichern")
subparsers.add_parser("delete-password", help="Passwort aus Schlüsselbund entfernen")
return parser
# ═══════════════════════════════════════════════════════════════════════════════
# Einstiegspunkt
# ═══════════════════════════════════════════════════════════════════════════════
def main() -> None:
parser = build_parser()
args = parser.parse_args()
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
# Passwort-Verwaltung braucht keine CalDAV-Verbindung
if args.command == "store-password":
cmd_store_password(args)
return
if args.command == "delete-password":
cmd_delete_password(args)
return
# Konfiguration laden & verbinden
cfg = get_config()
# --calendar überschreibt Env-Variable
if args.calendar:
cfg["calendar"] = args.calendar
client = connect(cfg)
cal = get_calendar(client, cfg.get("calendar"))
# CRUD-Dispatcher
dispatch = {
"list": cmd_list,
"create": cmd_create,
"read": cmd_read,
"update": cmd_update,
"delete": cmd_delete,
}
dispatch[args.command](args, cal)
if __name__ == "__main__":
main()
+33
View File
@@ -0,0 +1,33 @@
# ══════════════════════════════════════════════════════
# caldav_crud.py — Konfigurationsvorlage
# Kopieren nach: ~/.openclaw/.env
# Berechtigungen setzen: chmod 600 ~/.openclaw/.env
# ══════════════════════════════════════════════════════
# CalDAV-Server (mailbox.org Standard — nicht ändern nötig)
MAILBOX_DAV_URL=https://dav.mailbox.org
# Deine mailbox.org E-Mail-Adresse
MAILBOX_DAV_USERNAME=deine-adresse@mailbox.org
# App-Passwort (NICHT dein Haupt-Passwort!)
# Erstellen unter: mailbox.org → Einstellungen → Sicherheit → App-Passwörter
#
# EMPFEHLUNG: Dieses Feld leer lassen und stattdessen keyring verwenden:
# ./caldav_crud.py store-password
#
# Nur als Fallback (z.B. auf Headless-Servern ohne keyring-Backend):
MAILBOX_DAV_PASSWORD=
# Kalenderbezeichnung (optional)
# Leer lassen → ersten verfügbaren Kalender verwenden
# Exakter Name wie er in mailbox.org angezeigt wird (z.B. "Persönlich")
MAILBOX_DAV_CALENDAR=
# ══════════════════════════════════════════════════════
# Sicherheitshinweise:
# • Nie diese Datei in git committen (.gitignore eintragen)
# • chmod 600 ~/.openclaw/.env (nur Owner lesbar)
# • App-Passwort in mailbox.org hat eingeschränkten Scope
# • Bevorzuge keyring für interaktive Umgebungen
# ══════════════════════════════════════════════════════