You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: No iMIP response sent when accepting an invitation from Outlook through Nextcloud Mail #50665
When accepting an invitation from Outlook in Nextcloud Mail, no iMIP response is sent to the organizer, and the status in Outlook isn't updated. However, when the event is deleted, a decline message is sent successfully.
Steps to reproduce
Add your email account to Nextcloud Mail.
Configure Nextcloud to send iMIP responses through your email account in Nextcloud Mail.
Invite yourself to an event using Outlook Calendar.
Accept the invitation in Nextcloud Mail.
Observe that the status in Outlook isn’t updated and no email is sent.
Delete the event in Nextcloud Mail.
Observe that a decline message is sent to the organizer, updating the status.
Expected behavior
When accepting an invitation in Nextcloud Mail, an iMIP response should be sent to the organizer, updating the status in Outlook.
Nextcloud Server version
master
Nextcloud Mail version
main
Additional info
Accepting an invitation from Google Calendar triggers a response email to the organizer.
The EventComparisonService.findModified method returns null for the Outlook event, and as a result, no response is sent.
Thanks for forwarding the outlook invite, you where working with, with that invite I was able to reproduce this...
When accepting a calendar invitation there is first a PUT request to create the event, this is then followed by a secondary PUT for the same event to update the status to ACCEPT. The second request triggers the iMIP process but this fails when we try to find a Uri for a external user...
// No changed events after all - this shouldn't happen if there is significant change yet here we are
// The scheduling status is debatable
if (empty($vEvent)) {
$this->logger->warning('iTip message said the change was significant but comparison did not detect any updated VEvents');
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
return;
}
Here, $vEvent is empty/null, which causes the request to be stopped with: 1.0;We got the message, but it\'s not significant enough to warrant an email.
Quick recap:
Accepting invitations from Google works.
Accepting invitations from Outlook does not.
The key difference is that Google’s invitation includes a Last-Modified property, while Outlook’s does not.
There are two separate requests:
Request 1: Imports the unmodified event.
Request 2: Updates the attendance status and adds a Last-Modified property (likely via calendar-js or ical.js).
It seems strange that we’re comparing the original event with the attendee’s reply.
Proposed fix
I’ve attached a small patch that ensures the EventComparisonService is only used for requests. This makes invitation responses work for Outlook while still functioning correctly for Google (since Google's initial invitation already contains Last-Modified, making it different from the generated iTip message).
Index: apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php--- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php (revision 425e44176adee0f2364257b8a91317dbed6d5803)+++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php (date 1740682340483)@@ -45,7 +45,7 @@
* @license http://sabre.io/license/ Modified BSD License
*/
class IMipPlugin extends SabreIMipPlugin {
-+
private ?VCalendar $vCalendar = null;
public const MAX_DATE = '2038-01-01';
public const METHOD_REQUEST = 'request';
@@ -129,12 +129,18 @@
$newEvents = $iTipMessage->message;
$oldEvents = $this->getVCalendar();
- $modified = $this->eventComparisonService->findModified($newEvents, $oldEvents);- /** @var VEvent $vEvent */- $vEvent = array_pop($modified['new']);- /** @var VEvent $oldVevent */- $oldVevent = !empty($modified['old']) && is_array($modified['old']) ? array_pop($modified['old']) : null;- $isModified = isset($oldVevent);+ if ($iTipMessage->method === self::METHOD_REQUEST) {+ $modified = $this->eventComparisonService->findModified($newEvents, $oldEvents);+ /** @var VEvent $vEvent */+ $vEvent = array_pop($modified['new']);+ /** @var VEvent $oldVevent */+ $oldVevent = !empty($modified['old']) && is_array($modified['old']) ? array_pop($modified['old']) : null;+ $isModified = isset($oldVevent);+ } else {+ $vEvent = $newEvents->__get('VEVENT');+ $oldVevent = $oldEvents->__get('VEVENT');+ $isModified = true;+ }
// No changed events after all - this shouldn't happen if there is significant change yet here we are
// The scheduling status is debatable
Next Steps
I think it would be really beneficial to split the logic in ImipPlugin into separate handlers for requests, replies, and cancellations. Right now, understanding the code without stepping through it in a debugger is quite challenging.
I haven’t tested the patch with cancellations yet. The next step should be to verify if this issue is reproducible for others—just to rule out a weird hiccup on my dev instance—before investing more time in a fix.
Note: Mail's IMipMessageJob may add the event when the background job runs. For my tests, I deleted that copy. Either set the imip_processed column to true or wait until the cron job adds the event and then delete it.
(That code also doesn't handle multiple attendees properly, so it might fail as well. 😉)
Yes it would be beneficial to split the logic. This was on my To-Do list once the iTipBroker changes are done/accepted, so I will be doing this. If you want we can talk about the improvement steps that at pending.
And yes alot of the old code does not handles attendees properly, but that section is on a per attendee message basis
for the attendee, although the invitation was accepted through Nextcloud Mail.
The following log entry occurred around the same time:
iTip message said the change was significant but comparison did not detect any updated VEvents
This looks related to the Outlook invitation comparison issue described here, but it additionally causes a temporary inconsistent event time that is observable by an external CalDAV client.
Environment:
Nextcloud: 33.0.3
Mail app: 5.9.2
Calendar app: 6.4.2
Timezone setting: Automatic (Europe/Zurich)
Browser: Firefox 151 on Linux
Still reproducible on Nextcloud 34.0.4 (AIO, dav 1.40.0, calendar 6.6.1), and not only via Nextcloud Mail: accepting an Exchange/Outlook invitation from Nextcloud Calendar (web), iOS Calendar and Thunderbird via CalDAV gives the same result. Each RSVP logs iTip message said the change was significant but comparison did not detect any updated VEvents and no REPLY is mailed to the organizer.
Same root cause as analysed above: the Sabre-generated REPLY only contains UID, DTSTAMP, SEQUENCE, DTSTART, DTEND, SUMMARY, ORGANIZER, ATTENDEE. The Exchange event has no LAST-MODIFIED and an unchanged SEQUENCE. So EventComparisonService::findModified() sees ['', '', '1', ''] on both sides and filters the REPLY out. I reproduced this server-side with Broker::parseEvent() + findModified() on the stored event (method=REPLY significant=true, findModified new count: 0).
As a local workaround, falling back to the REPLY's own VEVENT when the comparison comes back empty for METHOD_REPLY fixes it (organizer gets SCHEDULE-STATUS=1.1, reply mail is delivered):
Still reproducible on 35.0.1. Exchange Online invitation, auto-created in the calendar by Nextcloud Mail ("Automatically create tentative appointments in calendar"), then answered from the Calendar web UI and separately from DAVx5 (Android). Both responses were dropped with SCHEDULE-STATUS=1.0 and:
iTip message said the change was significant but comparison did not detect any updated VEvents
Changing the response a second time does deliver a reply, which fits the analysis above: the first write stamps LAST-MODIFIED, and the REPLY that sabre builds in Broker::parseEventForAttendee() never carries one, so from then on the comparison sees a difference.
Since the comparison is only meaningful for organizer messages, I'm opening a PR that skips findModified() for METHOD:REPLY and takes the VEVENT from the reply directly (REQUEST and CANCEL unchanged). It includes a unit test using the real EventComparisonService that fails with 1.0 before the fix. @SebastianKrupinski@kesselb if this conflicts with the planned rework of this logic, say so and I'll close it.
Activity
kesselb commentedon Feb 5, 2025
cc @SebastianKrupinski @st3iny
SebastianKrupinski commentedon Feb 5, 2025
@kesselb
I am wondering how you accepted the invitation, Outlook.com invitations do not get discovered properly.
This is due to the iMip message being Base64 encoded.
SebastianKrupinski commentedon Feb 5, 2025
@kesselb
Thanks for forwarding the outlook invite, you where working with, with that invite I was able to reproduce this...
When accepting a calendar invitation there is first a PUT request to create the event, this is then followed by a secondary PUT for the same event to update the status to ACCEPT. The second request triggers the iMIP process but this fails when we try to find a Uri for a external user...
kesselb commentedon Feb 27, 2025
Hey,
I did some more digging today, and now I'm even more confused. 🤯
It seems like the event/request is taking a different path on my dev instance than on Sebastian's. 🤷
server/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
Lines 132 to 145 in 425e441
Here,
$vEventis empty/null, which causes the request to be stopped with:1.0;We got the message, but it\'s not significant enough to warrant an email.Quick recap:
The key difference is that Google’s invitation includes a Last-Modified property, while Outlook’s does not.
There are two separate requests:
See nextcloud/mail#9992 why we are using two requests currently.
Backend behavior:
server/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
Line 132 in 001b12c
$oldEvents= Event from Request 1$newEvents= iTIP message generated from Request 2Since neither contains a Last-Modified property, the EventComparisonService assumes they are identical and doesn't send an update.
server/apps/dav/lib/CalDAV/EventComparisonService.php
Lines 18 to 23 in 001b12c
It seems strange that we’re comparing the original event with the attendee’s reply.
Proposed fix
I’ve attached a small patch that ensures the EventComparisonService is only used for requests. This makes invitation responses work for Outlook while still functioning correctly for Google (since Google's initial invitation already contains Last-Modified, making it different from the generated iTip message).
Next Steps
I think it would be really beneficial to split the logic in ImipPlugin into separate handlers for requests, replies, and cancellations. Right now, understanding the code without stepping through it in a debugger is quite challenging.
I haven’t tested the patch with cancellations yet. The next step should be to verify if this issue is reproducible for others—just to rule out a weird hiccup on my dev instance—before investing more time in a fix.
Note: Mail's IMipMessageJob may add the event when the background job runs. For my tests, I deleted that copy. Either set the imip_processed column to true or wait until the cron job adds the event and then delete it.
(That code also doesn't handle multiple attendees properly, so it might fail as well. 😉)
SebastianKrupinski commentedon Feb 27, 2025
@kesselb
I agree with your findings.
Yes it would be beneficial to split the logic. This was on my To-Do list once the iTipBroker changes are done/accepted, so I will be doing this. If you want we can talk about the improvement steps that at pending.
And yes alot of the old code does not handles attendees properly, but that section is on a per attendee message basis
ChristophWurst commentedon May 23, 2025
Transferring to group related changes
handkerchief333 commentedon Jun 11, 2026
I can reproduce an additional symptom when accepting an Outlook invitation through Nextcloud Mail.
The invitation contains a valid UTC-based event:
The configured Nextcloud timezone is:
The expected local time is therefore:
Immediately after accepting the invitation in Nextcloud Mail, the event is temporarily shown as:
This is not limited to the Nextcloud Calendar frontend:
10:30 to 11:30.08:30 to 09:30.08:30 to 09:30in red and the previous correct time10:30 to 11:30as struck through.The calendar object exported from Nextcloud after the state has converged contains the correct UTC values:
However, the exported object still contains:
PARTSTAT=NEEDS-ACTIONfor the attendee, although the invitation was accepted through Nextcloud Mail.
The following log entry occurred around the same time:
This looks related to the Outlook invitation comparison issue described here, but it additionally causes a temporary inconsistent event time that is observable by an external CalDAV client.
Environment:
tmvtmv commentedon Sep 28, 2026
Still reproducible on Nextcloud 34.0.4 (AIO, dav 1.40.0, calendar 6.6.1), and not only via Nextcloud Mail: accepting an Exchange/Outlook invitation from Nextcloud Calendar (web), iOS Calendar and Thunderbird via CalDAV gives the same result. Each RSVP logs
iTip message said the change was significant but comparison did not detect any updated VEventsand no REPLY is mailed to the organizer.Same root cause as analysed above: the Sabre-generated REPLY only contains
UID, DTSTAMP, SEQUENCE, DTSTART, DTEND, SUMMARY, ORGANIZER, ATTENDEE. The Exchange event has noLAST-MODIFIEDand an unchangedSEQUENCE. SoEventComparisonService::findModified()sees['', '', '1', '']on both sides and filters the REPLY out. I reproduced this server-side withBroker::parseEvent()+findModified()on the stored event (method=REPLY significant=true,findModified new count: 0).As a local workaround, falling back to the REPLY's own VEVENT when the comparison comes back empty for
METHOD_REPLYfixes it (organizer getsSCHEDULE-STATUS=1.1, reply mail is delivered):Would be great to see kesselb's proposed fix (skip the comparison for non-REQUEST methods) land.
ItWorksOnMyMachine commentedon Oct 9, 2026
Still reproducible on 35.0.1. Exchange Online invitation, auto-created in the calendar by Nextcloud Mail ("Automatically create tentative appointments in calendar"), then answered from the Calendar web UI and separately from DAVx5 (Android). Both responses were dropped with
SCHEDULE-STATUS=1.0and:Changing the response a second time does deliver a reply, which fits the analysis above: the first write stamps
LAST-MODIFIED, and the REPLY that sabre builds inBroker::parseEventForAttendee()never carries one, so from then on the comparison sees a difference.Since the comparison is only meaningful for organizer messages, I'm opening a PR that skips
findModified()forMETHOD:REPLYand takes the VEVENT from the reply directly (REQUEST and CANCEL unchanged). It includes a unit test using the realEventComparisonServicethat fails with1.0before the fix. @SebastianKrupinski @kesselb if this conflicts with the planned rework of this logic, say so and I'll close it.SebastianKrupinski commentedon Oct 13, 2026
I have a fix on the board for this
#64635