initial
This commit is contained in:
@@ -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
|
||||
# ══════════════════════════════════════════════════════
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
# ══════════════════════════════════════════════════════
|
||||
Reference in New Issue
Block a user