CalDAV PUT on events created via JMAP fails with 412 no-uid-conflict when a VALARM carries a UID (v0.16.24)

Issue Description

About 5,000 calendar events were imported into Stalwart through JMAP (vandelay)
from another server; others were later edited in a JMAP webmail. Apple Calendar
(macOS 10.13.6 and 10.14.6) can display these events but cannot modify them:
moving an event to another day looks fine for a few seconds, then the client
silently reverts it. Events created by Apple Calendar itself over CalDAV can be
modified normally. On our server 3,288 of 4,981 events are affected: all of them
were created via JMAP and have at least one VALARM with a UID property (Apple
clients write UID and X-WR-ALARMUID on every alarm).

Expected Behavior

A PUT with a matching If-Match ETag whose VEVENT keeps the same UID as the stored
event is accepted, whatever the UIDs of its VALARM sub-components.

Actual Behavior

The PUT is rejected with 412 and the no-uid-conflict precondition, although the
VEVENT UID is unchanged (stored event, GET over CalDAV, abridged):

BEGIN:VCALENDAR
BEGIN:VEVENT
UID:38B59F21-B572-4C1B-9DDC-998D657ABC33
…
BEGIN:VALARM
UID:60FB50D0-B8D8-43A4-9565-783F34C6024E
X-WR-ALARMUID:60FB50D0-B8D8-43A4-9565-783F34C6024E
…
END:VALARM
(three more VALARMs, each with its own UID)
END:VEVENT
END:VCALENDAR

What we read in the source at tag v0.16.24:

  • crates/dav/src/calendar/update.rs compares
    event.inner.data.event.uids().next() with the UID returned by
    validate_ical(&ical); validate_ical() only collects UIDs of VEVENT/VTODO/
    VJOURNAL/VFREEBUSY/VAVAILABILITY.
  • Cargo.lock pins calcard 0.3.14, where ArchivedICalendar::uids() iterates
    ALL components, VALARM included.
  • The JSCalendar → iCalendar export (calcard src/jscalendar/export/convert.rs)
    pushes each VALARM with push_component(alert) while its parent is being
    built, and the parent VEVENT is pushed afterwards. For an event created via
    JMAP the component order is therefore VCALENDAR, VALARM…, VEVENT, and the
    first UID found is the alarm’s.
    calcard 0.4.0 (commit f84db86, “Compliance fixes”, 22/09) restricts uids() to
    calendar objects and VCALENDAR, which would fix it; Cargo.lock on main still
    pins 0.3.14 (checked 01/10).

Reproduction Steps

  1. Import via JMAP an event whose iCalendar source has a VALARM with a UID
    (we used vandelay with events exported from a CalDAV server; Apple Calendar
    writes UID and X-WR-ALARMUID on every VALARM).
  2. GET the event over CalDAV: the resource name has no .ics extension; note
    the ETag; the VEVENT UID differs from the VALARM UIDs.
  3. PUT the same iCalendar back with only DTSTART/DTEND changed and
    If-Match: <etag>.
  4. The server answers 412 with no-uid-conflict.
  5. Create the same event by PUT (text order: VEVENT before its VALARMs) and
    repeat step 3: the PUT is accepted.

Relevant Log Output

2026-10-01T10:56:16Z INFO WebDAV PUT request (web-dav.put) listenerId = "https", localPort = 443, remoteIp = 192.168.12.51, url = "/dav/cal/<account>%40<domain>/OAMjbx41J9/Trb1umzjhN", type = "CalDAV", details = ["Content-Type", "text/calendar", "If", ["ETag", "\"746741392\""]], code = 412, result = "NoUidConflict", reason = (null), elapsed = 0ms

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

RocksDB

Blob Storage

Filesystem

Search Engine

Internal

Directory Backend

Internal

Additional Context

Clients: Apple Calendar on macOS 10.13.6 and 10.14.6, CalDAV over HTTPS on 443,
no reverse proxy. Workaround that works: copy/paste the event in the client and
delete the original (the copy is created by PUT, so its VEVENT comes first).
The VEVENT UID never changes here, so RFC 4791 section 5.3.2.1 does not apply:
a VALARM UID (RFC 9074) is not the UID of the calendar object resource.
Related topics, none of them a duplicate: 1702 (CardDAV, same precondition,
urn:uuid prefix, a genuine UID change), 1085 (calendar-query on JMAP-created
events, fixed in 0.16.14; on 0.16.24 our REPORTs return the data), 1819 and
1714 (JMAP-created events served over CalDAV), the “iMIP invitations silently
skipped when the client omits uid” topic (missing UID, a different case). If v1.0.0 already ships
calcard >= 0.4.0, a confirmation is enough for us: we will wait for 1.0 and
keep the workaround until then.

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

That’s what happens. In 0.16 the UID used for that comparison is the first UID property found anywhere in the stored object, and calcard 0.3 exports JMAP alerts ahead of their VEVENT, so for these events it ends up being a VALARM UID. Your PUT keeps the VEVENT UID, the two differ, and you get no-uid-conflict even though RFC 4791 only cares about the event’s own UID.

1.0.0 ships calcard 0.4.0, where uids() only looks at the calendar components, and the server now also stores and compares the VEVENT/VTODO UID explicitly, so these PUTs go through. 1.0.0 is due at the end of this month or early November, and the copy-and-delete workaround is fine until then. In 0.16 the alarm UID also lands in the UID index for those events, so iTIP matching on them can go wrong as well.

Just to recognise and complete this issue, it is the same the other way around:

An event that was stored via CalDAV and whose `VALARM` has a `UID` property cannot be updated via JMAP. Every `CalendarEvent/set` update is rejected, even one that only changes the title:

```json

{

“type”: “invalidProperties”,

“properties”: [“uid”],

“description”: “You cannot change the UID of a calendar event.”

}

```

The request does not contain `uid`. Apple Calendar (iOS and macOS) writes a `UID` into every `VALARM`, so this hits every event with a reminder that was created on an iPhone or Mac.

@MagnusFriendz yes, that’s the same bug seen from the JMAP side. The update compares the first UID in the stored object with the first UID after converting your change back to iCalendar, and with alarms that carry their own UID one of those can belong to an alarm.

The 1.0.0 change covers this direction too: the comparison now only looks at the event (or task) component’s UID, so alarm UIDs no longer matter. 1.0.0 is due at the end of this month or early November. Until then, editing those events from a CalDAV client works.