Files
webmail-client/plugins/webmail_plugin_calendar/webmail_plugin_calendar/queries.py
T
lukasandClaude Opus 4.8 8c634f7967 M4: Calendar plugin (builtin) + mail HTML/attachment fixes
Calendar plugin (webmail_plugin_calendar, non-removable core plugin):
- Local calendars/events CRUD with RLS isolation; recurrence expanded at
  query time (DST-correct), EXDATE + detached-override merge.
- Calendar app bundle: month/week/day/3-day views, mini-month, event form
  (color, show-as, timezone combobox, repeat, reminders, guests w/ recipient
  autocomplete, Jitsi video call), event popover with RSVP badges, resizable/
  collapsible left panel + theme toggle, overlap layout.
- iMIP invites: sdk.mail sends REQUEST from the organizer's mailbox (HTML +
  text/calendar + invite.ics); public /rsvp endpoint (vault-sealed token)
  records accepted/declined/tentative per attendee.
- "This and following" edit + scoped delete (this / this-and-following), with
  a delete confirmation dialog for recurring events.

Core:
- Settings: time_format (12/24h, default 24h) + jitsi_server; new Integrations
  section; date order follows time format (en-GB vs en-US) across Mail.
- Plugin SDK: mail (send from a user's account), vault (envelope encrypt),
  multi-method SSRF-guarded http; AppSwitcher product switcher; full-bleed
  PluginPage.

Mail HTML rendering:
- Sanitizer allows <style> blocks + class (CSS sanitized: drop script-like,
  strip @import/url() when remote images blocked); SANITIZER_VERSION 3 so
  already-indexed mail re-sanitizes on view.
- Attachments: a Content-Disposition: attachment part is a real attachment
  even when it also carries a Content-ID (fixes hidden invite.ics).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 16:33:34 +02:00

94 lines
3.4 KiB
Python

"""Range expansion + override merge for the events list (SPEC §A.3).
Given the base events (``recurrence_id IS NULL``) and detached override rows
(``recurrence_id`` set) that overlap a window, produce the flat list of instances
the UI renders: expand each base event's RRULE (DST-correct, EXDATE-aware), drop the
occurrence an override replaces, and emit the override at its own moved time.
"""
from __future__ import annotations
from collections.abc import Mapping
from datetime import UTC, datetime, timedelta
from typing import Any
from webmail_plugin_calendar.recurrence import expand_occurrences
Row = Mapping[str, Any]
def _duration(dtstart: datetime, dtend: datetime | None, all_day: bool) -> timedelta:
if dtend is not None:
return dtend - dtstart
return timedelta(days=1) if all_day else timedelta(0)
def _instance(
row: Row, start: datetime, end: datetime, recurrence_id: datetime | None
) -> dict[str, Any]:
origin = row.get("origin_message_id")
return {
"id": str(row["id"]),
"calendar_id": str(row["calendar_id"]),
"ical_uid": row["ical_uid"],
"summary": row["summary"],
"description": row.get("description") or "",
"location": row["location"],
"tz": row.get("tz"),
"rrule": row.get("rrule"),
"reminders": list(row.get("reminders") or []),
"start": start.isoformat(),
"end": end.isoformat(),
"all_day": row["all_day"],
"status": row["status"],
"recurring": row["rrule"] is not None,
"recurrence_id": recurrence_id.isoformat() if recurrence_id else None,
"color": row["color"],
"show_as": row.get("show_as", "busy"),
"conference_url": row.get("conference_url", ""),
"attendees": row.get("attendees") or [],
"organizer": row.get("organizer"),
"my_partstat": row.get("my_partstat"),
"origin_message_id": str(origin) if origin else None,
}
def expand_range(
base_rows: list[Row],
override_rows: list[Row],
range_start: datetime,
range_end: datetime,
) -> list[dict[str, Any]]:
# Overrides keyed by (ical_uid, original-occurrence-start in UTC); also emitted
# as their own instance at the moved time.
replaced: set[tuple[str, datetime]] = set()
out: list[dict[str, Any]] = []
for o in override_rows:
replaced.add((o["ical_uid"], o["recurrence_id"].astimezone(UTC)))
if o["status"] == "cancelled":
continue
dur = _duration(o["dtstart"], o["dtend"], o["all_day"])
start, end = o["dtstart"], o["dtstart"] + dur
if end > range_start and start < range_end:
out.append(_instance(o, start, end, o["recurrence_id"]))
for b in base_rows:
dur = _duration(b["dtstart"], b["dtend"], b["all_day"])
occ = expand_occurrences(
dtstart=b["dtstart"],
duration=dur,
rrule=b["rrule"],
exdates=list(b["exdates"] or []),
range_start=range_start,
range_end=range_end,
tz=b["tz"],
all_day=b["all_day"],
)
for start, end in occ:
if (b["ical_uid"], start.astimezone(UTC)) in replaced:
continue # a detached override provides this instance
out.append(_instance(b, start, end, start if b["rrule"] else None))
out.sort(key=lambda i: i["start"])
return out