JMAP CalendarEvent/set rejects the standard recurrenceRules property with invalidProperties

Issue Description

Creating a recurring calendar event through JMAP fails. CalendarEvent/set rejects the
JSCalendar recurrenceRules property with a invalidProperties SetError, even when the value
is exactly the canonical example from the JMAP Calendars specification.

The same recurring events are accepted without any problem over CalDAV (iCalendar RRULE),
where Stalwart pre-expands them into individual instances as documented. So the recurrence
support exists in the server — it is specifically the JMAP write path for recurrenceRules
that is broken.

This is the server-side root cause behind a downstream webmail bug report
(bulwarkmail/webmail#327, “CalDav import of recurring events fails”): the webmail correctly
maps the imported iCalendar RRULE to a JSCalendar recurrenceRules array and sends it via
CalendarEvent/set (batchCreate); the server rejects the property and all recurring events
silently drop out of the import.

Expected Behavior

The event is created (created), as required by the JMAP Calendars specification, which lists
recurrenceRules as a regular settable property of a CalendarEvent and uses this exact value
shape in its examples.

Actual Behavior

The event is not created; the server returns an invalidProperties SetError naming
recurrenceRules:

{
  "methodResponses": [["CalendarEvent/set", {
    "accountId": "<account-id>",
    "notCreated": {
      "probe": {
        "type": "invalidProperties",
        "description": "Invalid property.",
        "properties": ["recurrenceRules"]
      }
    }
  }, "0"]],
  "sessionState": "…"
}

Supporting evidence (three independent checks)

  1. Minimal recurrence rejected — the spec-canonical recurrenceRules
    ([{ "@type": "RecurrenceRule", "frequency": "weekly" }]) → invalidProperties: ["recurrenceRules"].
    A second variant with byDay ([{ "@type": "NDay", "day": "mo" }]) is rejected identically,
    so it is not a byDay/NDay shape problem — the property itself is not accepted.
  2. Control event succeeds — an otherwise identical event without recurrenceRules,
    created on the same account and calendar via the same CalendarEvent/set call, returns
    created normally. The create path works; only recurrenceRules is rejected.
  3. CalDAV path works — recurring events created via CalDAV (iCalendar RRULE) are accepted
    and correctly pre-expanded by the server into individual instances, confirming recurrence
    handling exists and the gap is JMAP-specific.

Reproduction Steps

Send a minimal, spec-conformant CalendarEvent/set create with a single weekly recurrence rule
(values taken verbatim from the JMAP Calendars draft example
[{ "@type": "RecurrenceRule", "frequency": "weekly" }]):

{
  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:calendars"],
  "methodCalls": [["CalendarEvent/set", {
    "accountId": "<account-id>",
    "create": {
      "probe": {
        "@type": "Event",
        "calendarIds": { "<calendar-id>": true },
        "title": "RRULE probe (minimal)",
        "start": "2026-06-10T09:00:00",
        "duration": "PT1H",
        "timeZone": "Europe/Berlin",
        "recurrenceRules": [{ "@type": "RecurrenceRule", "frequency": "weekly" }]
      }
    }
  }, "0"]]
}

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

Additional Context

  • Downstream report: bulwarkmail/webmail#327 — the webmail’s ICS import drops all recurring
    events because CalendarEvent/batchCreate returns this same invalidProperties on
    recurrenceRules.
  • Related: GitHub issue #2925 (updates/destroys targeting synthetic instance IDs are likewise
    “not yet supported”). This report is distinct: it covers the create path — setting
    recurrenceRules on a master event — which fails earlier in the lifecycle and with a
    different error (invalidProperties, i.e. the create handler does not recognize the property
    as settable, rather than an explicit “not yet supported” stub). GitHub issue #2925 presupposes that a
    recurring event already exists (e.g. created via CalDAV) and concerns editing its individual
    occurrences. Together they map the same incomplete JMAP recurrence surface:
    create a master ✗ (this issue), edit/delete a single instance ✗ (#2925), and recurrenceRules
    not round-tripped on read (below).
  • Reading side appears affected too: in a non-expanded CalendarEvent/query
    (expandRecurrences: false), recurring master events that were created via CalDAV are
    returned without their recurrenceRules, while the expanded query
    (expandRecurrences: true) does fan them out into individual instances. The synthetic-ID
    expansion model that produces this is confirmed in #2925, so recurrenceRules appears not to
    be round-tripped on either the read or the write side of the JMAP Calendar implementation,
    even though the underlying RRULE data is stored and expanded.

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

Let me research the current state of JMAP CalendarEvent recurrenceRules support in the Stalwart codebase.

Now I have a clear picture of the code. Let me check how the property parsing/deserialization works for recurrenceRules to understand where it’s being rejected.

Tell your AI to read the latest specs: draft-ietf-calext-jscalendarbis-16 - JSCalendar 2.0: A JSON Representation of Calendar Data

Closing this — works as intended. The bug was one letter long, and it was ours.

My AI and I spent a genuinely committed afternoon assembling three independent proofs that Stalwart had a server-side recurrence bug. Plot twist: it was the same mistake wearing three different hats. We had been sending recurrenceRules (plural — the obsolete RFC 8984 / JSCalendar 1.0 form, deprecated in Appendix A.2.1 of the bis draft) instead of recurrenceRule(singular — JSCalendar 2.0, §3.3.3).

Confirmed on 0.16.7: drop the s, send a single RecurrenceRule object, and the event creates without complaint. So Stalwart was right the entire time and politely told us so via invalidProperties — we just weren’t listening.

Sincere apologies for the noise and for the time it took you to read a report that boiled down to a typo with delusions of grandeur. Pointing us at jscalendarbis was the most efficient bug triage either of us has had all week — thank you.

The genuine client-side bug (emitting the 1.0 plural form) now lives at bulwarkmail/webmail#327.