Skip to content

Fix: non-destructive calendar updates, attendee support, RFC 5545 compliance#53

Merged
MadLlama25 merged 3 commits into
MadLlama25:mainfrom
JonathanGodley:calendar-attendee-support
Jul 6, 2026
Merged

Fix: non-destructive calendar updates, attendee support, RFC 5545 compliance#53
MadLlama25 merged 3 commits into
MadLlama25:mainfrom
JonathanGodley:calendar-attendee-support

Conversation

@JonathanGodley

Copy link
Copy Markdown
Contributor

Summary

  • Bug fix: updateCalendarEvent destroyed all non-standard properties (attendees, reminders, recurrence rules, X-GOOGLE-* etc.) by rebuilding VEVENT from scratch. Now patches in-place, preserving everything not explicitly changed.
  • Bug fix: createCalendarEvent silently failed for all-day events (produced midnight UTC instead of VALUE=DATE)
  • Bug fix: JMAP calendar handlers used a try/catch JMAP-then-CalDAV fallback pattern, but the JMAP Calendars spec (JSCalendar / RFC 8984) is not yet finalized and Fastmail has not enabled JMAP calendar support. The JMAP calendar code also has known bugs (participants sent as array instead of RFC 8984 object/map, startDate/endDate not passed through, participants: [] sent when none provided). Replaced with direct CalDAV calls; JMAP calendar code retained with bug fixes and TODO markers for when Fastmail enables support.
  • Feature: Attendee/organizer parsing with PARTSTAT, ROLE, CUTYPE, RSVP
  • Feature: Participant support on create and update (with email validation to prevent injection)
  • Feature: Recurring event safety — confirmRecurring flag required for time changes, orphaned exceptions pruned via RRULE expansion
  • Feature: DURATION parsing, timezone preservation, RFC 5545 compliance (SEQUENCE increment, LAST-MODIFIED, value type consistency)

What changed

src/caldav-client.ts (+966 lines)

  • Non-destructive patch-based updates using line-based VEVENT editing with VALARM depth tracking
  • New functions: parseAllICalProperties, parseAttendee, findValueBoundary (quote-aware), replaceICalProperty, removeAllICalProperties, insertBeforeEndVEvent, removeOrphanedVTimezones, removeExceptionVEvents, parseICalDuration, formatDateTimeProperty, validateAttendeeEmail, quoteParamValue, detectLineEnding, toICalUTC (with date-only guard), foldICalLine (with configurable line ending)
  • extractVEvent returns null instead of full input when no VEVENT found (was silently parsing VTIMEZONE properties)
  • Participant interface and CalendarEvent extended with organizer? and participants?
  • parseCalendarObject takes optional { includeParticipants } (only get_calendar_event enables it, keeping list responses compact)

src/caldav-client.test.ts (+1475 lines, 171 new tests)

Comprehensive coverage including CRLF/LF preservation, VALARM sub-component skipping, injection prevention, recurring event orphan detection, quote-aware parsing, and all edge cases.

src/index.ts

  • JMAP calendar fallback removed — CalDAV called directly with comment explaining why
  • update_calendar_event and delete_calendar_event tool schemas and handlers added
  • create_calendar_event now passes participants through to CalDAV (was silently dropped)
  • get_calendar_event uses getCalendarEventById with participant parsing

src/contacts-calendar.ts

  • Fixed participants: [] being sent when none provided (JMAP bug)
  • Added TODO markers on known JMAP calendar issues

package.json

  • Added rrule dependency for RRULE expansion during recurring event orphan detection
    • Single transitive dependency (tslib), built-in TypeScript types
    • Alternative: if this dependency is undesirable, the orphan detection can be replaced with "delete ALL exceptions on time change" (matching Google Calendar behavior). The code has a comment at the import explaining this tradeoff.

README.md

  • Calendar known limitations section
  • FASTMAIL_CALDAV_DISPLAY_NAME env var documented
  • Updated tool descriptions

Supersedes

This PR supersedes #45 (iCal encoding fix) — those fixes are included and extended here.

Test plan

  • npx tsc --noEmit passes
  • All 171 new CalDAV tests pass (224 total pass; 3 pre-existing validateSavePath failures on Windows unrelated to this PR)
  • npm run build compiles
  • get_calendar_event on event with Google-synced attendees — participants visible with PARTSTAT/ROLE/CUTYPE
  • update_calendar_event changing only title — attendees survive, timezone preserved, SEQUENCE unchanged
  • create_calendar_event with participants — ORGANIZER and ATTENDEE lines present
  • create_calendar_event with date-only start — all-day event, not midnight UTC

- Parse attendees/organizer from CalDAV events (PARTSTAT, ROLE, CUTYPE, RSVP)
- Rewrite updateCalendarEvent to patch-in-place instead of rebuilding VEVENT,
  preserving all existing data (attendees, reminders, X-GOOGLE-*, etc.)
- Add participant support to createCalendarEvent with ORGANIZER auto-generation
- Add all-day event support (VALUE=DATE) on create and update
- Parse DURATION and compute end time for DURATION-based events
- Handle recurring events safely: require confirmRecurring for time changes
  that would orphan exception VEVENTs, using rrule for expansion
- Preserve timezone (TZID) on floating-time updates
- Remove JMAP calendar fallback (dead code), call CalDAV directly
- Fix extractVEvent to return null instead of full input on no match
- Add quote-aware iCal parsing (findValueBoundary) for properties with
  quoted colons in parameters (DELEGATED-FROM, ALTREP, etc.)
- Validate attendee emails to prevent iCal property injection
- SEQUENCE increment only for scheduling-significant changes with attendees
- Orphaned VTIMEZONE cleanup after time changes
- 355 tests (all passing), 97% statement coverage on caldav-client.ts
@JonathanGodley

Copy link
Copy Markdown
Contributor Author

Couldn't see attendees, and that sent me down one hell of a rabbit hole when I found other issues.

Got everything working with CalDAV and intentionally disabled JMAP calendar support. Reasoning was that JMAP isn't actually supported by FastMail, existing implementation appears to have bugs, and when Fastmail DO enable support, significant testing and likely bugfixing will be required.

IMO - safer to explicitly disable it until the the JMAP implementation is enabled, so that it can be properly tested and feature parity can be ensured.

Tried to closely follow the RFC specs, or other existing implementations where possible.

As per the pull request - i have brought in an additional dependency to try and intelligently handle calendar entries in repeated series - but a workaround is feasible (albeit a bit of a blunt instrument).

…phaned ORGANIZER (MadLlama25#56)

update_calendar_event previously overwrote title/description/location to empty
when passed "" (guarding only on !== undefined), and would crash on null. Empty,
whitespace-only, and null values on those fields are now loud-rejected with a
per-field error ("omit the field to leave it unchanged"). Dates already threw on
bad input and are unchanged.

To deliberately delete an optional field, pass it in the new clearFields array
(allowed: description, location). Setting and clearing the same field in one call
is rejected.

Also fixes a related malformed-VEVENT bug: participants: [] removed all ATTENDEEs
but left a dangling ORGANIZER. It now strips ORGANIZER too, gated on the empty
case so the participant-add path still emits an ORGANIZER.

Adds requireNonEmpty / validateClearFields helpers (exported, unit-tested) and
flips the existing "preserves ORGANIZER" test to assert removal.

Closes MadLlama25#56.
@JonathanGodley

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit (de56612) addressing #56 and a related bug found alongside it:

  • Empty-string overwrite (update_calendar_event silently overwrites fields with empty strings #56): title/description/location are now loud-rejected on empty/whitespace/null instead of silently blanking the property. A new clearFields array (description, location) handles deliberate deletion; setting and clearing the same field in one call is rejected.
  • Orphaned ORGANIZER: participants: [] previously removed all ATTENDEEs but left a dangling ORGANIZER (a malformed scheduling VEVENT). It now strips ORGANIZER too, gated on the empty case so the participant-add path still emits one.

Both come with unit tests (the existing participants: [] test was flipped to assert ORGANIZER removal), and the validation helpers are self-contained in caldav-client.ts. Closes #56 when this merges.

@MadLlama25 MadLlama25 merged commit 4b29e5e into MadLlama25:main Jul 6, 2026
MadLlama25 added a commit that referenced this pull request Jul 6, 2026
….11.0

Five findings from an adversarial review of the merged PR #53 code:

- escapeICalText: normalize bare CR to LF before escaping and strip
  remaining control characters — a lone \r previously passed through
  verbatim and could act as a line terminator for downstream parsers
  (iCal property injection via title/description/location).
- normalizeMasterVEventFirst: reorder VEVENT blocks so the master (no
  RECURRENCE-ID) is first before in-place patching. Component order is
  not guaranteed by RFC; an exception-first payload previously had its
  exception patched and skipped the recurring-event guard.
- parseICalDateAsUTC: orphaned-exception detection now compares
  RECURRENCE-IDs and RRULE occurrences in a single UTC frame. With
  TZ != UTC, naive datetimes were parsed in local time while rrule used
  naive-as-UTC, flagging valid exceptions as orphans (deleted once the
  user passed confirmRecurring).
- replaceICalProperty: insert new properties before the first
  sub-component (VALARM) per RFC 5545 ABNF eventprop *alarmc.
- Quoted TZID params handled in formatDateTimeProperty and
  removeOrphanedVTimezones (plus fold-aware reference scanning).

15 new unit tests pin these behaviors. Version synced to 1.11.0 across
package.json, manifest.json, src/index.ts, and README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants