Skip to content

[Bug]: No iMIP response sent when accepting an invitation from Outlook through Nextcloud Mail #50665

Description

@kesselb

Bug description

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

  1. Add your email account to Nextcloud Mail.
  2. Configure Nextcloud to send iMIP responses through your email account in Nextcloud Mail.
  3. Invite yourself to an event using Outlook Calendar.
  4. Accept the invitation in Nextcloud Mail.
  5. Observe that the status in Outlook isn’t updated and no email is sent.
  6. Delete the event in Nextcloud Mail.
  7. 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.

$modified = $this->eventComparisonService->findModified($newEvents, $oldEvents);
/** @var VEvent $vEvent */
$vEvent = array_pop($modified['new']);

Activity

  1. added
    0. Needs triagePending check for reproducibility or if it fits our roadmap
    on Feb 5, 2025
  2. kesselb commented on Feb 5, 2025

    @kesselb
    ContributorAuthor
  3. SebastianKrupinski commented on Feb 5, 2025

    @SebastianKrupinski
    Contributor

    @kesselb

    I am wondering how you accepted the invitation, Outlook.com invitations do not get discovered properly.

    Image

    This is due to the iMip message being Base64 encoded.

    --_000_CY4PR0401MB36678BE7D5F3B935DF4B16D4D9F72CY4PR0401MB3667_
    
    Content-Type: text/calendar; charset="utf-8"; method=REQUEST
    Content-Transfer-Encoding: base64
    
    QkVHSU46VkNBTEVOREFSDQpNRVRIT0Q6UkVRVUVTVA0KUFJPRElEOk1pY3Jvc29mdCBFeGNoYW5n
    ZSBTZXJ2ZXIgMjAxMA0KVkVSU0lPTjoyLjANCkJFR0lOOlZUSU1FWk9ORQ0KVFpJRDpFYXN0ZXJu
    IFN0YW5kYXJkIFRpbWUNCkJFR0lOOlNUQU5EQVJEDQpEVFNUQVJUOjE2MDEwMTAxVDAyMDAwMA0K
    VFpPRkZTRVRGUk9NOi0wNDAwDQpUWk9GRlNFVFRPOi0wNTAwDQpSUlVMRTpGUkVRPVlFQVJMWTtJ
    TlRFUlZBTD0xO0JZREFZPTFTVTtCWU1PTlRIPTExDQpFTkQ6U1RBTkRBUkQNCkJFR0lOOkRBWUxJ
    R0hU
    
  4. SebastianKrupinski commented on Feb 5, 2025

    @SebastianKrupinski
    Contributor

    @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...

    Image

  5. kesselb commented on Feb 27, 2025

    @kesselb
    ContributorAuthor

    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. 🤷

    $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);
    // 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).

    See nextcloud/mail#9992 why we are using two requests currently.

    Backend behavior:

    $modified = $this->eventComparisonService->findModified($newEvents, $oldEvents);

    • $oldEvents = Event from Request 1
    • $newEvents = iTIP message generated from Request 2

    Since neither contains a Last-Modified property, the EventComparisonService assumes they are identical and doesn't send an update.

    private const EVENT_DIFF = [
    'RECURRENCE-ID',
    'RRULE',
    'SEQUENCE',
    'LAST-MODIFIED'
    ];

    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. 😉)

  6. SebastianKrupinski commented on Feb 27, 2025

    @SebastianKrupinski
    Contributor

    @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

  7. moved this from 📄 To do to 🧭 Planning evaluation in 💌 📅 👥 Groupware teamon Feb 27, 2025
  8. moved this from 🧭 Planning evaluation to 📄 To do in 💌 📅 👥 Groupware teamon Feb 27, 2025
  9. moved this from 📄 To do to 🧭 Planning evaluation in 💌 📅 👥 Groupware teamon Feb 27, 2025
  10. moved this from 🧭 Planning evaluation to 📄 To do in 💌 📅 👥 Groupware teamon Feb 27, 2025
  11. self-assigned this
    on Apr 11, 2025
  12. added
    1. to developAccepted and waiting to be taken care of
    and removed
    0. Needs triagePending check for reproducibility or if it fits our roadmap
    on May 23, 2025
  13. ChristophWurst commented on May 23, 2025

    @ChristophWurst
    Member

    Transferring to group related changes

  14. moved this from To triage to Confirmed in 🧑‍🤝‍🧑 Community triageon Jun 24, 2025
  15. added theissue type on Jul 1, 2025
  16. handkerchief333 commented on Jun 11, 2026

    @handkerchief333

    I can reproduce an additional symptom when accepting an Outlook invitation through Nextcloud Mail.

    The invitation contains a valid UTC-based event:

    DTSTART;TZID=UTC:20260617T083000
    DTEND;TZID=UTC:20260617T093000

    The configured Nextcloud timezone is:

    Automatic (Europe/Zurich)
    

    The expected local time is therefore:

    10:30 to 11:30 Europe/Zurich
    

    Immediately after accepting the invitation in Nextcloud Mail, the event is temporarily shown as:

    08:30 to 09:30
    

    This is not limited to the Nextcloud Calendar frontend:

    1. Before accepting the invitation in Nextcloud Mail, Thunderbird displays the invitation correctly as 10:30 to 11:30.
    2. After accepting it in Nextcloud Mail, the Nextcloud Calendar web app displays 08:30 to 09:30.
    3. Thunderbird also temporarily displays 08:30 to 09:30 in red and the previous correct time 10:30 to 11:30 as struck through.
    4. After reloading the Nextcloud Calendar web app, the event is displayed correctly again in both Nextcloud Calendar and Thunderbird.

    The calendar object exported from Nextcloud after the state has converged contains the correct UTC values:

    DTSTART;TZID=UTC:20260617T083000
    DTEND;TZID=UTC:20260617T093000

    However, the exported object still contains:

    PARTSTAT=NEEDS-ACTION

    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
    
  17. tmvtmv commented on Sep 28, 2026

    @tmvtmv

    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):

    if (empty($vEvent) && strcasecmp((string)$iTipMessage->method, self::METHOD_REPLY) === 0) {
        $replyEvents = $newEvents->select('VEVENT');
        $vEvent = end($replyEvents) ?: null;
    }

    Would be great to see kesselb's proposed fix (skip the comparison for non-REQUEST methods) land.

  18. ItWorksOnMyMachine commented on Oct 9, 2026

    @ItWorksOnMyMachine

    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.

  19. SebastianKrupinski commented on Oct 13, 2026

    @SebastianKrupinski
    Contributor

    I have a fix on the board for this

    #64635

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions