CalendarEvent/get omits baseEventId for synthetic occurrences unless explicitly requested

Issue Description

CalendarEvent/get omits the baseEventId property from synthetic recurring-event occurrences when the request does not specify a properties list.

Under JMAP semantics, omitting properties requests all properties. A synthetic expanded occurrence should therefore include baseEventId, since that property identifies the real base event from which the occurrence was generated.

Stalwart does return the correct baseEventId when that property is explicitly requested. The issue is specifically with the default all-properties response.

This can prevent clients from associating expanded occurrence IDs with editable base events, particularly when the event UID is missing or malformed.

Expected Behavior

For a synthetic event ID returned from a query with expandRecurrences: true, a subsequent CalendarEvent/get with properties omitted should include:

{
  "id": "synthetic-id",
  "baseEventId": "base-id",
  "recurrenceId": "2026-07-25T09:00:00"
}

The JMAP Calendars draft defines baseEventId as:

This is only defined if the id property is a synthetic id, generated by the server to represent a particular instance of a recurring event. This property gives the id of the real CalendarEvent this was generated from.

When the properties argument is omitted, the server should return all properties of the object, including server-defined JMAP properties applicable to that object.

Actual Behavior

When properties is omitted, Stalwart returns the synthetic occurrence without baseEventId.

For example:

{
  "using": [
    "urn:ietf:params:jmap:core",
    "urn:ietf:params:jmap:calendars"
  ],
  "methodCalls": [
    [
      "CalendarEvent/get",
      {
        "accountId": "c",
        "ids": [
          "iiaaaaa3"
        ],
        "timeZone": "Pacific/Auckland"
      },
      "all"
    ]
  ]
}

The response included:

{
  "recurrenceId": "2026-07-25T02:20:00",
  "uid": "[email protected]",
  "title": "Water Softener Running",
  "start": "2026-07-25T02:20:00",
  "id": "iiaaaaa3",
  "calendarIds": {
    "c": true
  },
  "isDraft": false,
  "isOrigin": true
}

The baseEventId property is absent.

However, explicitly requesting the property:

{
  "accountId": "c",
  "ids": [
    "iiaaaaa3"
  ],
  "properties": [
    "id",
    "baseEventId",
    "uid",
    "recurrenceId",
    "title"
  ],
  "timeZone": "Pacific/Auckland"
}

returns the correct value:

{
  "uid": "[email protected]",
  "recurrenceId": "2026-07-25T02:20:00",
  "title": "Water Softener Running",
  "id": "iiaaaaa3",
  "baseEventId": "3"
}

Therefore, Stalwart can calculate the property correctly but omits it from the default all-properties response.

Reproduction Steps

  1. Create or import a recurring calendar event.

  2. Run CalendarEvent/query with:

    • expandRecurrences: true
    • bounded after and before values.
  3. Select one of the returned synthetic occurrence IDs.

  4. Call CalendarEvent/get for that synthetic ID with the properties argument omitted.

  5. Observe that baseEventId is absent.

  6. Repeat the same request while explicitly including baseEventId in properties.

  7. Observe that Stalwart now returns the correct base event ID.

Relevant Log Output

No error is logged because both requests complete successfully.

The relevant responses are:

CalendarEvent/get with properties omitted:
id = iiaaaaa3
baseEventId = absent
CalendarEvent/get with baseEventId explicitly requested:
id = iiaaaaa3
baseEventId = 3

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

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

Confirmed by looking at the source. In crates/jmap/src/calendar_event/get.rs, when properties is omitted (return_all_properties is true), the server builds its default property list like this:

} else {
    return_is_origin = true;
    (
        vec![
            JSCalendarProperty::Id,
            JSCalendarProperty::CalendarIds,
            JSCalendarProperty::IsDraft,
            JSCalendarProperty::IsOrigin,
        ],
        vec![],
    )
};

JSCalendarProperty::BaseEventId is not included in that default list, even though the code elsewhere (the branch used when properties is explicitly given) does know how to compute and emit it for synthetic ids. That matches exactly what you observed: the value is correctly derived when requested explicitly but silently dropped from the “all properties” default set, so this is a genuine gap in the default-properties handling for CalendarEvent/get.

This is a bug report rather than a how-to question, so I’ll let a human/staff member confirm and follow up on a fix; I’ve located the relevant code path above to save them time.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

The JMAP for Calendars draft does not require baseEventId in the /get responses when properties is omitted:

If the client makes a “CalendarEvent/get” call with a null or omitted “properties” argument, all properties that are defined on the JSCalendar Event object in the store are returned except for “iCalendar”. In addition, the “id”, “calendarIds”, “isDraft”, and “isOrigin” properties MUST be included on each object.

Ugh, sorry. I should have checked this one more closely. Feel free to delete.