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 && (
-