Outlook attendee's Accept/Decline never reaches the organizer

Issue Description

Thanks again for the quick fixes in v0.16.17 (see #1299) and v0.16.19 (see #1468), both were important steps for us toward being able to run Stalwart in production.

While testing a bit further, though, we unfortunately ran into another issue:

Whatever the attendee chooses when replying to an invite (Accept/Decline/Tentative) doesn’t seem to make it back to us as the organizer.

We also noticed two smaller probably related things: Clicking Accept/Decline/Tentative doesn’t bring up the usual sub-choices (“Edit the response before sending”, “Send the response now”, “Do not send a response”), the status just gets set locally right away. The option to propose a different time is also missing.

We create calendar invitations for external attendees either via Bulwark (JMAP) or via Thunderbird (CalDAV), both go through Stalwart’s server-side scheduling. The invited attendee reads their mail in Outlook.

Expected Behavior

  • When the attendee accepts, declines, or tentatively accepts an invitation created via Bulwark/JMAP or Thunderbird/CalDAV, the organizer should get that response.
  • Clicking Accept/Decline/Tentative should offer the usual sub-choices (“Edit the response before sending”, “Send the response now”, “Do not send a response”).
  • The attendee should be able to propose a different time, the same as they can for invitations that don’t go through Stalwart’s server-side scheduling.

Actual Behavior

  • The attendee can click Accept/Decline/Tentative in Outlook, but the organizer doesn’t get notified, regardless of what the attendee has chosen.
  • The usual sub-choices (“Edit the response before sending”, “Send the response now”, “Do not send a response”) don’t show up, the status just gets set locally.
  • The “Propose New Time” option isn’t offered to the attendee for these invitations.

Reproduction Steps

  1. As organizer, create a calendar event with an external attendee, either via Bulwark (JMAP), or via Thunderbird (CalDAV), where the attendee’s mailbox is read with Outlook.
  2. Send the invitation to an attendee who uses Outlook.
  3. In Outlook, open the invitation and click Accept (or Decline, or Tentative).
  4. As organizer, check mailbox. No response shows up.
  5. For comparison, try creating the same invitation directly from an Outlook or Thunderbird (with “Prefer client-side email scheduling” enabled), bypassing Stalwart’s scheduling. There the response does come through normally, and “Propose New Time” is available.

Note on step 5: with client-side scheduling, the organizer does receive a confirmation email when the attendee accepts or declines, but the organizer’s own calendar entry doesn’t get updated with that response. We suspect this isn’t a bug though, but simply how client-side scheduling normally behaves. We just happened to notice it while testing and wanted to mention it here as well, just in case it does turn out to be a bug after all.

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

PostgreSQL

Blob Storage

PostgreSQL

Search Engine

Internal

Directory Backend

Internal

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

What does the ATTENDEE line for the invitee look like in the raw .ics Outlook received? Specifically whether RSVP=TRUE is on it.

RFC 5545 defaults RSVP to FALSE when the parameter is absent, and Outlook maps RSVP onto “Response Requested”, which is the switch controlling both the send-response sub-choices and Propose New Time. Stalwart copies RSVP through from the stored event but never adds it, while internally treating a missing RSVP as “reply expected” so it can stamp PARTSTAT=NEEDS-ACTION. An event whose attendee has no RSVP therefore goes out saying “needs action” and “no reply expected” in the same message. If your client did not set it, that is all three symptoms in one cause, and the exporter should be emitting RSVP=TRUE itself when it is the scheduling origin.

Propose New Time will not work either way: inbound METHOD:COUNTER is not implemented, so a counter-proposal has nothing on our end to land in.

Thanks a lot for taking a look at this!

We ran a broader round of tests, here is what we found.

Thunderbird/CalDAV

We can confirm your suspicion. For invitations created via Thunderbird/CalDAV, the ATTENDEE line for the invitee has no RSVP parameter at all. When we switch Thunderbird to prefer client-side scheduling instead, RSVP=TRUE is present on that same line and everything works as expected.

Bulwark/JMAP

Here we found something unexpected at first. RSVP=TRUE was already correctly set for the invitee, but the confirmation still did not come back. Further testing showed that a second mailbox on the same external mail server worked correctly with the exact same kind of invitation. The difference we could pin down: the working mailbox is connected via IMAP in Outlook, the failing one via POP3. We suspect this is an Outlook behavior tied to POP3 accounts, so we do not see any need for changes on your end here. Sorry for wrongly suggesting earlier that Bulwark/JMAP was affected by the same problem as Thunderbird/CalDAV.

ATTENDEE lines you asked for

Addresses and IDs are anonymized: organizer@ is the sender in all five cases, attendee-stalwart@ is a mailbox hosted on Stalwart, attendee-external@ is a mailbox hosted on an external mail server.

SW-to-External-via-BW-event.ics (Bulwark/JMAP, organizer to attendee-external)

JSPROP;JSPTR=participants/00000000-0000-4000-8000-000000000001/scheduleAgen
 t:"server"
ATTENDEE;EMAIL="[email protected]";RSVP=FALSE;CUTYPE=INDIVIDUAL;PARTSTAT=AC
 CEPTED;CN=;JSID=00000000-0000-4000-8000-000000000001:mailto:[email protected]
JSPROP;JSPTR=participants/00000000-0000-4000-8000-000000000002/roles/attend
 ee:true
JSPROP;JSPTR=participants/00000000-0000-4000-8000-000000000002/scheduleAgen
 t:"server"
ATTENDEE;EMAIL="[email protected]";RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=
 NEEDS-ACTION;CN=;JSID=00000000-0000-4000-8000-000000000002:mailto:attendee-external
 @mydomain.tld
ORGANIZER;EMAIL="[email protected]";CN=:mailto:[email protected]

SW-to-SW-via-BW-event.ics (Bulwark/JMAP, organizer to attendee-stalwart)

JSPROP;JSPTR=participants/00000000-0000-4000-8000-000000000003/scheduleAgen
 t:"server"
ATTENDEE;EMAIL="[email protected]";RSVP=FALSE;CUTYPE=INDIVIDUAL;PARTSTAT=AC
 CEPTED;CN=;JSID=00000000-0000-4000-8000-000000000003:mailto:[email protected]
JSPROP;JSPTR=participants/00000000-0000-4000-8000-000000000004/roles/attend
 ee:true
JSPROP;JSPTR=participants/00000000-0000-4000-8000-000000000004/scheduleAgen
 t:"server"
ATTENDEE;EMAIL="[email protected]";RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-
 ACTION;CN="Attendee Name";JSID=00000000-0000-4000-8000-000000000004
 :mailto:[email protected]
ORGANIZER;EMAIL="[email protected]";CN=:mailto:[email protected]

SW-to-SW-via-TB-event.ics (Thunderbird/CalDAV, organizer to attendee-stalwart)

ORGANIZER;CN=Organizer Test;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL:mailto:organizer
 @mydomain.tld
ATTENDEE;CN=Organizer Test;PARTSTAT=ACCEPTED;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICI
 PANT:mailto:[email protected]
ATTENDEE;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT:mailt
 o:[email protected]

SW-to-External-via-TB-event.ics (Thunderbird/CalDAV, organizer to attendee-external)

ORGANIZER;CN=Organizer Test;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL:mailto:organizer
 @mydomain.tld
ATTENDEE;CN=Organizer Test;PARTSTAT=ACCEPTED;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICI
 PANT:mailto:[email protected]
ATTENDEE;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT:mailt
 o:[email protected]

SW-to-External-via-TB-client-sc-event.ics (Thunderbird, client-side scheduling, organizer to attendee-external)

ORGANIZER;CN=Organizer Test;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL:mailto:organizer
 @mydomain.tld
ATTENDEE;RSVP=TRUE;CN=Organizer Test;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL;RO
 LE=REQ-PARTICIPANT:mailto:[email protected]
ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICI
 PANT:mailto:[email protected]

Two questions for you

  1. On the missing RSVP for Thunderbird/CalDAV invitations, is that something Stalwart could add on its side, or would you consider this a Thunderbird bug that we should raise with them instead?

  2. On Propose New Time, how do you weigh that one? Is that a bug from your point of view, or should we file it as a separate feature request?

One correction first, I said the missing RSVP was ours and implied we were doing something wrong. We are not. Stalwart never removes RSVP. The only ATTENDEE parameters we strip when composing an outbound REQUEST are SCHEDULE-STATUS, SCHEDULE-AGENT and SCHEDULE-FORCE-SEND; everything else is copied from the stored event untouched. When the parameter is absent from the invitation Outlook receives, it was absent from what the client PUT.

Your samples show the same thing. SW-to-External-via-TB-client-sc-event.ics has RSVP=TRUE on both attendee lines, and the same event through server-side scheduling has none. Thunderbird writes the parameter when it composes the invitation itself and drops it when it hands scheduling to the server, which is where I would take this. Emitting it in one mode and not the other is inconsistent no matter who is at the other end.

No spec is being broken by either of us, which is why this sat unnoticed for so long. The RFC 5546 constraints table for METHOD:REQUEST lists ATTENDEE with no parameter requirements at all. RFC 6638 says what a server has to put into a generated scheduling message, SCHEDULE-STATUS, DTSTAMP, SEQUENCE and recurrence trimming, and RSVP is not on that list. RFC 5545 defines the parameter and gives it a FALSE default, nothing more. Outlook is strict about that default in a way Apple Calendar, Google and Thunderbird are not; all three offer accept and decline whether or not the parameter is present, so your combination is the one that surfaces it.

We could synthesize RSVP=TRUE on our side, and internally we already treat a missing RSVP as “a reply is expected”, so it would not be much of a change. What holds me back is your second question. METHOD:COUNTER has never been implemented here. A counter-proposal arriving at the organizer is logged as an unsupported method and dropped, and the mail is still delivered to the mailbox. Nothing regressed there, it was never built, and it is not on the roadmap. If Outlook gates Propose New Time on the same flag as the response buttons, and your testing points that way, then setting RSVP=TRUE hands your users a button that quietly discards whatever they propose. As it stands they get no button, which is at least an accurate picture of what the server will do with it. So this would be a feature request for the second one.