CalDAV: updating an event via JMAP does not update its CalDAV representation (stale content, ETag not bumped)

Issue Description

An existing calendar event was modified through JMAP (CalendarEvent/set update:
changed start time and duration; client: Bulwark webmail). The JMAP view reflects the change, but the CalDAV representation of the same event is frozen: both a GET on the resource and a calendar-query REPORT keep returning the pre-edit iCalendar content, and the getetag remains unchanged.

Since CalDAV clients detect changes via the ETag, they never learn about edits made through JMAP clients - the change is invisible on the entire CalDAV protocol surface.

Possibly related to my other report about calendar-query returning empty calendar-data for JMAP-created events: both look like the JMAP/JSCalendar store and the CalDAV/iCalendar view not being kept in sync.

Expected Behavior

After a JMAP update, GET and calendar-query REPORT on the corresponding CalDAV resource return the updated event, and the ETag changes so CalDAV sync clients pick up the modification.

Actual Behavior

JMAP CalendarEvent/get (after the edit):
start=2026-08-01T11:00:00, duration=PT5M, showWithoutTime=false (new state)

CalDAV GET on the same event’s resource:
DTSTART;VALUE=DATE;TZID=Europe/Berlin:20260801 (all-day, old state)
DTSTAMP from before the edit

ETag before edit == ETag after edit (“1282476642”) — unchanged in GET, PROPFIND and REPORT. The calendar-query REPORT also returns the old content.

Reproduction Steps

  1. Create a (recurring) event via CalDAV PUT (iCalendar resource) or have an existing CalDAV-stored event.
  2. Modify the event via JMAP, e.g. change start/duration:

curl -u user:pass https://mail.example.com/jmap/
-H ‘Content-Type: application/json’
-d ‘{“using”:[“urn:ietf:params:jmap:core”,“urn:ietf:params:jmap:calendars”],
“methodCalls”:[[“CalendarEvent/set”,{“accountId”:“”,
“update”:{“”:{“start”:“2026-08-01T11:00:00”,
“duration”:“PT5M”,“showWithoutTime”:false}}},“c1”]]}’

  1. Verify via JMAP CalendarEvent/get that the change is stored.
  2. GET the CalDAV resource / run a calendar-query REPORT / PROPFIND the getetag.
  3. Observe: old iCalendar content, unchanged ETag.

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

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

I can’t reproduce this one from the code as described, and it’s a bit puzzling, because JMAP and CalDAV read and write the same underlying event record: a successful JMAP update rewrites the archive and bumps the modified timestamp, and the CalDAV GET and ETag are derived straight from that same archive, so an applied JMAP change should always move both the body and the ETag. An unchanged ETag with stale content usually means the update landed on a different event than the CalDAV href resolved to (an account or id mismatch, or a duplicate event the client created), or a caching layer in between.

Could you send the exact accountId and JMAP event id you update, the exact CalDAV href you then GET, confirmation that both point at the same event, and whether a second copy of the event appeared after the JMAP edit? A trace-level log of the JMAP set followed by the CalDAV GET would help most. One thing to rule out: if what you actually saw under REPORT was empty (not stale) calendar-data, that’s a separate confirmed bug I’m already tracking, so let me know which it was.

Thanks for looking into it. To answer your questions with what I have on record (the original resource unfortunately no longer exists - the user deleted and ecreated the event as a workaround - so I can’t pull trace logs retroactively):

Stale vs. empty:
It was definitely STALE, not empty. The calendar-query REPORT returned 2147 bytes of pre-edit iCalendar for that resource, and a direct GET returned the same pre-edit content (2226 bytes). I’m aware of the empty-calendar-data bug (I reported it separately) - this was a different resource and a different symptom.

Same event, same account:

  • accountId: “l” (the personal account; the session also exposes a shared account “ev”, but the event lived in “l”, same principal as the CalDAV URL)
  • JMAP event id: “bkf”
  • CalDAV href: /dav/cal//default/040000008200E00074C5B7101A82E008000000002C56A703B49ACF010000000000000000140000003FC51DF270E52DD93A92604AADC3E68E78A7FB7A.ics
  • The UID inside the iCalendar was identical to the href basename, and CalendarEvent/get on “bkf” returned that same uid - so JMAP id and CalDAV href pointed at the same event.

No duplicate:
After the edit, CalendarEvent/query returned exactly one id for that title, and a full Depth-1 PROPFIND plus an unfiltered calendar-query over the whole collection (~396 resources) contained exactly one resource with that event. No second copy appeared.

Caching layer ruled out:
Two server-internal signals agreed with the stale GET: (1) a sync-collection REPORT (RFC 6578) using a pre-edit sync-token never reported the href as changed afterwards - so the WebDAV-Sync index itself considered the resource unmodified; (2) the ETag was identical across GET, PROPFIND and REPORT (“1282476642”, same value as before the edit). Meanwhile CalendarEvent/get showed the new state (start 2026-08-01T11:00:00, PT5M, showWithoutTime=false) vs. CalDAV serving the old all-day event.

One more data point:
even in the JMAP view, the “updated” property still showed 2021-08-31T22:09:59Z (the stored DTSTAMP) after the edit. That part IS still reproducible on a current build: PUT any event via CalDAV, update it via CalendarEvent/set (update succeeds, content and ETag change correctly) - but CalendarEvent/get keeps returning the old DTSTAMP as “updated” instead of the modification time.

Repro attempts today (both negative):
I PUT a fresh simple event and also a faithful replica of the affected resource (Exchange-style: two VEVENTs - an all-day recurring master plus a RECURRENCE-ID override with quoted Windows TZIDs and a “W. Europe Standard Time” VTIMEZONE) and updated each via JMAP (all-day → timed, like the original edit). In both cases GET/REPORT returned the new content and the ETag was bumped. So current-state resources behave correctly.

My working theory:
the affected resource predated the current state - it came from an Exchange migration. The same population of old resources also fails time-range RRULE expansion in calendar-query (my other report: a yearly recurring event with an occurrence inside the range is not matched, while freshly PUT recurring events are matched fine - looks like a missing/stale expansion index for pre-existing resources). I suspect the stale-after-JMAP-update case is the same underlying inconsistency: the update applied to the JSCalendar view while the CalDAV read path and the sync index kept serving the pre-edit archive.

If it happens again on one of the remaining legacy resources I’ll capture a trace-level log of the CalendarEvent/set followed by the CalDAV GET and attach it.