From ced81835ef76f98089348ed9f85e044ba976a562 Mon Sep 17 00:00:00 2001 From: Stefan Hildebrandt <695494+hildebrandttk@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:52:28 +0200 Subject: [PATCH] fix(calendar): drop default SKIP=OMIT from recurrence rules (DAVx5 sync) Recurring events defaulted skip:"omit"+rscale:"gregorian" on every rule, which Stalwart serialises into the CalDAV RRULE as RSCALE=GREGORIAN;SKIP=OMIT. DAVx5 rejects that ("Invalid recurrence rule"), breaking Android sync. Those RFC 7529 fields only matter for non-Gregorian scales / invalid-date handling, so stop emitting them for plain Gregorian rules. - lib/recurrence-rule.ts: createRecurrenceRule() helper with no skip/rscale. - CalendarRecurrenceRule.rscale/skip are now optional. - event-modal + recurrence-editor build rules via the helper. - demo fixture cleaned; unit test asserts no skip/rscale. Closes #805. --- components/calendar/event-modal.tsx | 33 +++++-------------- components/calendar/recurrence-editor.tsx | 19 ++--------- lib/__tests__/recurrence-rule.test.ts | 40 +++++++++++++++++++++++ lib/demo/fixtures/calendars.ts | 2 -- lib/jmap/types.ts | 7 ++-- lib/recurrence-rule.ts | 33 +++++++++++++++++++ 6 files changed, 89 insertions(+), 45 deletions(-) create mode 100644 lib/__tests__/recurrence-rule.test.ts create mode 100644 lib/recurrence-rule.ts diff --git a/components/calendar/event-modal.tsx b/components/calendar/event-modal.tsx index 943da7143..d4eaa5099 100644 --- a/components/calendar/event-modal.tsx +++ b/components/calendar/event-modal.tsx @@ -11,6 +11,7 @@ import { RecurrenceEditor, buildRecurrenceSummary, isSimpleRecurrenceRule } from import { parseDuration, getEventColor } from "./event-card"; import { buildAllDayDuration, getEventDisplayEndDate, getEventEndDate, getEventStartDate, getPrimaryCalendarId } from "@/lib/calendar-utils"; import { displayNow, getEffectiveTimeZone } from "@/lib/timezone"; +import { createRecurrenceRule } from "@/lib/recurrence-rule"; import { ParticipantInput, type ParticipantInputHandle } from "./participant-input"; import { isOrganizer, @@ -543,25 +544,7 @@ export function EventModal({ if (recurrence === "custom" && customRule) { data.recurrenceRules = [customRule]; } else if (recurrence !== "none" && recurrence !== "custom") { - data.recurrenceRules = [{ - "@type": "RecurrenceRule", - frequency: recurrence, - interval: 1, - rscale: "gregorian", - skip: "omit", - firstDayOfWeek: "mo", - byDay: null, - byMonthDay: null, - byMonth: null, - byYearDay: null, - byWeekNo: null, - byHour: null, - byMinute: null, - bySecond: null, - bySetPosition: null, - count: null, - until: null, - }]; + data.recurrenceRules = [createRecurrenceRule(recurrence)]; } else if (event && event.recurrenceRules?.length) { data.recurrenceRules = null; if (event.recurrenceOverrides) data.recurrenceOverrides = null; @@ -1383,9 +1366,9 @@ export function EventModal({ > {t("events.delete")} - )} - + {!showDeleteConfirm && (
-