Issue Description
Version: Stalwart 0.16.20 (nixpkgs 0968519e14f7aa7d3e9b389682bd74d2b51c8ce8,
pkgs.stalwart_0_16, store path gmjyhdprvlljzhhn21myj1dzsmxsq313-stalwart-0.16.20), RocksDB
backend, ordinary user account. Also observed on a PostgreSQL-backed deployment of the
same version.
Context: this sits on top of the synthetic-id write support from
stalwartlabs/stalwart#2925 (closed 2026-08-28); the writes themselves work as described
there. Reads and writes on synthetic ids are correct; what changes is which occurrence an
id names.
Spec: draft-ietf-jmap-calendars-28, section 5.11: with expandRecurrences: true “a separate
id will be returned for each instance of a recurring event that matches the query. This
synthetic id is opaque to the client, but uniquely identifies the base event id +
recurrence id within the account, allowing the server to resolve these for “/get” and
“/set” operations.”
Expected Behavior
A synthetic id keeps identifying the same base event + recurrence id for as long as that instance
exists, so that a client can expand once and then update or destroy several instances without
re-querying between writes.
Actual Behavior
What happens: after one update on a synthetic id, the ids of every later instance of the
same series resolve to a different recurrence id than before. The id appears to encode a
position in the current expansion (rule instances that are not overridden, then overrides)
rather than base event + recurrence id.
Reproduction Steps
Reproduction
Weekly series, five occurrences, created with:
[“CalendarEvent/set”, {“accountId”: “b”, “create”: {“c0”: {
“@type”: “Event”, “uid”: “urn:uuid:380ccc1a-d2cd-4fb6-88f7-c34fe81988ee”,
“calendarIds”: {“b”: true}, “title”: “repro bfe4da2e”,
“start”: “2027-03-01T09:00:00”, “duration”: “PT15M”, “timeZone”: “Europe/Berlin”,
“showWithoutTime”: false,
“recurrenceRule”: {“@type”: “RecurrenceRule”, “frequency”: “weekly”, “count”: 5}}}}, “s”]
→ created as id f.
Expansion (the same query is repeated below; only the uid filter and the window are set):
[“CalendarEvent/query”, {“accountId”: “b”,
“filter”: {“after”: “2027-03-01T00:00:00”, “before”: “2027-04-10T00:00:00”, “uid”:
“urn:uuid:380ccc1a-…”},
“sort”: [{“property”: “start”, “isAscending”: true}],
“expandRecurrences”: true, “timeZone”: “Europe/Berlin”}, “q”]
-
Before any write: ids: [“eaaaaaf”, “iaaaaaf”, “maaaaaf”, “qaaaaaf”, “uaaaaaf”] → recurrence ids
03-01, 03-08, 03-15, 03-22, 03-29 respectively (confirmed via /get with recurrenceId). -
Update the second occurrence by its synthetic id:
[“CalendarEvent/set”, {“accountId”: “b”, “update”: {“iaaaaaf”: {“start”: “2027-03-08T10:00:00”}}},
“u”]
→ {“updated”: {“iaaaaaf”: null}}; the base event now has recurrenceOverrides: {“2027-03-08T09:00:00”:
{“start”: “2027-03-08T10:00:00”, “duration”: “PT15M”, “updated”: “…”}}. So far correct. -
Same expansion again: ids: [“eaaaaaf”, “uaaaaaf”, “iaaaaaf”, “maaaaaf”, “qaaaaaf”] — and /get on
them:
| id | recurrenceId before | recurrenceId after |
|---|---|---|
| eaaaaaf | 2027-03-01 | 2027-03-01 |
| iaaaaaf | 2027-03-08 | 2027-03-15 |
| maaaaaf | 2027-03-15 | 2027-03-22 |
| qaaaaaf | 2027-03-22 | 2027-03-29 |
| uaaaaaf | 2027-03-29 | 2027-03-08 (the overridden one) |
Direct /get on [“iaaaaaf”, “maaaaaf”] after the update returns the 03-15 and 03-22 instances.
- A client that still holds maaaaaf from step 1 and destroys it now removes 03-22, not 03-15:
[“CalendarEvent/set”, {“accountId”: “b”, “destroy”: [“maaaaaf”]}, “d”]
→ {“destroyed”: [“maaaaaf”]}, base event recurrenceOverrides now also has “2027-03-22T09:00:00”:
{“excluded”: true}. Expansion afterwards: [“eaaaaaf”, “qaaaaaf”, “iaaaaaf”, “maaaaaf”] = 03-01, 03-08
(override), 03-15, 03-29.
Stalwart Version
v0.16.x
Installation Method
NixOS
Database Backend
PostgreSQL
Blob Storage
PostgreSQL
Search Engine
PostgreSQL
Directory Backend
OIDC
Additional Context
This issue reporting ux is bad.
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