Issue Description
When an organizer modifies a single occurrence of an existing recurring event through CalDAV, Stalwart sends the attendee an invalid iTIP message:
METHOD:ADD
RECURRENCE-ID:…
The newly added VEVENT override is present only in the modified calendar object, so organizer_handle_update() classifies it as a new instance and selects
ICalendarMethod::Add:
} else if instance_id != &InstanceId::Main {
let method = if matches!(
instance.comp.status(),
Some(ICalendarStatus::Cancelled)
) {
&ICalendarMethod::Cancel
} else {
&ICalendarMethod::Add
};
However, this override represents a modification of an occurrence already generated by the master event’s RRULE; it is not an additional occurrence.
RFC 5546 §3.2.4 prohibits RECURRENCE-ID in a METHOD:ADD component:
https://www.rfc-editor.org/rfc/rfc5546.html#section-3.2.4
RFC 5546 §4.4.2 represents a modified recurring occurrence as METHOD:REQUEST with RECURRENCE-ID:
https://www.rfc-editor.org/rfc/rfc5546.html#section-4.4.2
In our observed case, Stalwart delivered the generated email, but Google Calendar could not process the attached event and displayed “Unable to load event.” The changed time
was visible only by inspecting the raw ICS payload.
This was reproduced with Stalwart 0.16.19. The same branch is also present in the current upstream source.
Expected Behavior
When a new detached VEVENT override has a RECURRENCE-ID corresponding to an occurrence generated by the old calendar object’s recurrence set, Stalwart should send:
METHOD:REQUEST
The component should retain its RECURRENCE-ID, contain the moved DTSTART/DTEND, and have an appropriately advanced SEQUENCE.
METHOD:ADD should be reserved for introducing a genuinely additional occurrence, as described by RFC 5546 §3.2.4 and §4.4.6. Such an ADD component must not contain
RECURRENCE-ID.
Actual Behavior
Stalwart sends:
BEGIN:VCALENDAR
METHOD:ADD
…
BEGIN:VEVENT
UID:[email protected]
RECURRENCE-ID:20261006T090000Z
DTSTART:20261006T110000Z
DTEND:20261006T120000Z
SEQUENCE:2
…
END:VEVENT
END:VCALENDAR
The presence of RECURRENCE-ID makes this payload invalid for METHOD:ADD.
The email itself is delivered successfully, so Stalwart does not report a scheduling or SMTP error. However, the recipient’s Google Calendar cannot apply the occurrence
update.
Reproduction Steps
- Create a recurring event in a calendar owned by a local Stalwart user. Include an external attendee:
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Reproduction//EN
BEGIN:VEVENT
UID:[email protected]
DTSTAMP:20260903T120000Z
DTSTART:20261006T090000Z
DTEND:20261006T100000Z
RRULE:FREQ=WEEKLY;COUNT=4
SEQUENCE:0
ORGANIZER:mailto:[email protected]
ATTENDEE;RSVP=TRUE:mailto:[email protected]
SUMMARY:Recurring test
END:VEVENT
END:VCALENDAR
- Update the same CalDAV resource by retaining the master component and adding a detached override that moves the October 6 occurrence:
BEGIN:VEVENT
UID:[email protected]
DTSTAMP:20260903T121000Z
RECURRENCE-ID:20261006T090000Z
DTSTART:20261006T110000Z
DTEND:20261006T120000Z
SEQUENCE:1
ORGANIZER:mailto:[email protected]
ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:[email protected]
SUMMARY:Recurring test
END:VEVENT
- Inspect the raw scheduling email delivered to the external attendee.
- Observe that its calendar payload contains both:
METHOD:ADD
RECURRENCE-ID:20261006T090000Z
- Import or open the message in Google Calendar. In our observed case, Google Calendar reports “Unable to load event” instead of updating the occurrence.
Relevant Log Output
No error is logged because Stalwart successfully generates and delivers the message. The relevant evidence is the generated calendar payload:
BEGIN:VCALENDAR
METHOD:ADD
PRODID:-//Stalwart Labs LLC//Stalwart Server//EN
VERSION:2.0
BEGIN:VEVENT
UID:[email protected]
RECURRENCE-ID:20261006T090000Z
DTSTART:20261006T110000Z
DTEND:20261006T120000Z
SEQUENCE:2
ORGANIZER:mailto:[email protected]
ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:[email protected]
SUMMARY:Recurring test
END:VEVENT
END:VCALENDAR
Stalwart Version
v0.16.x
Installation Method
NixOS
Database Backend
RocksDB
Blob Storage
RocksDB
Search Engine
Internal
Directory Backend
Internal
Additional Context
The behavior is deterministic from the organizer diff logic: every non-cancelled component present only in the new snapshot is classified as ADD, without checking whether
its RECURRENCE-ID already belongs to the old master component’s recurrence set.
A possible fix is:
- If the new component is an additional occurrence outside the old recurrence set, use ADD.
- If its RECURRENCE-ID identifies an occurrence generated by the old recurrence set, use REQUEST.
- Continue using CANCEL when the added override has STATUS:CANCELLED.
The repository’s existing RFC 5546 fixture currently expects the invalid combination, so it will also need updating:
METHOD:ADD
…
RECURRENCE-ID:19970701T210000Z
I reviewed the documentation and FAQ and searched the public support forum for METHOD:ADD, RECURRENCE-ID, single-occurrence updates, and related terms. I did not find an
existing report for this outbound scheduling defect.
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