feat(anhaenge): ATTACH-Zeile im erzeugten ICS

Jeder Anhang wird als ATTACH mit FMTTYPE, SIZE und FILENAME ausgegeben,
als URI statt inline base64 -- bei 10 MB je Datei wuerde sonst bei jeder
Synchronisation der ganze Anhang im ICS-Rumpf mitgeschleppt.

Die absolute URL kommt aus dav_util.public_base, das PUBLIC_BASE_URL und
die Forwarded-Header schon behandelt; ohne Basis-URL wird die Zeile
weggelassen, weil eine relative ATTACH-URL einem externen Client nichts
nuetzt. Abschaltbar ueber ATTACHMENT_PUBLIC_LINKS=0.

Der CalDAV-PUT ist geprueft: ein externer Client, der den Termin
zurueckschreibt, laesst die Anhaenge unberuehrt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-14 10:19:14 +02:00
co-authored by Claude Opus 5
parent 4b9c09df74
commit a024fa54b9
6 changed files with 207 additions and 21 deletions
+28 -3
View File
@@ -9,9 +9,11 @@ from __future__ import annotations
import logging
import uuid
from urllib.parse import quote
from datetime import date, datetime, timedelta, timezone
from icalendar import Calendar, Event, vCalAddress, vRecur, vText
from icalendar import Calendar, Event, vCalAddress, vRecur, vText, vUri
from icalendar.parser import Parameters
logger = logging.getLogger(__name__)
@@ -150,8 +152,17 @@ def _parse_iso(s: str) -> datetime:
return dt
def build_ics(calendar, events, *, name_cache: dict | None = None) -> str:
"""Build a VCALENDAR string for a local calendar and its events."""
def build_ics(calendar, events, *, name_cache: dict | None = None,
attachments_by_event: dict | None = None,
base_url: str | None = None) -> str:
"""Build a VCALENDAR string for a local calendar and its events.
``attachments_by_event`` maps LocalEvent.id -> list of EventAttachment. With
a ``base_url`` each one becomes an ATTACH property pointing at its
capability URL, so external clients (Apple Calendar, Thunderbird) can fetch
it — they cannot send a bearer token. Both arguments are optional so every
existing caller keeps working and emits no ATTACH at all.
"""
cal = Calendar()
cal.add("prodid", "-//Calendarr//EN")
cal.add("version", "2.0")
@@ -200,6 +211,20 @@ def build_ics(calendar, events, *, name_cache: dict | None = None) -> str:
organizer.params["CN"] = vText(organizer_name.replace('"', ""))
item.add("organizer", organizer)
# ATTACH: one line per attachment, as a URI rather than inline base64 —
# the files are up to 10 MB each and would otherwise be copied into
# every ICS body on every sync.
for att in (attachments_by_event or {}).get(getattr(ev, "id", None), ()):
if not base_url:
continue # a relative ATTACH URL is useless to an external client
uri = vUri(f"{base_url}/api/attach/{att.token}/{quote(att.filename)}")
uri.params = Parameters({
"FMTTYPE": att.content_type,
"SIZE": str(att.size_bytes),
"FILENAME": att.filename,
})
item.add("attach", uri, encode=0)
cal.add_component(item)
return cal.to_ical().decode("utf-8")