JMAP Calendar scheduling does not generate invitations for internal attendees, even with sendSchedulingMessages: true

Issue Description

We created calendar events from one internal account to another internal account using JMAP Calendars. The organizer account had a valid default ParticipantIdentity,
global scheduling was enabled, and we explicitly tested several event payload variants, including:

  • a normal event with attendees
  • an event with attendee sendTo.imip
  • an event with sendSchedulingMessages: true
  • an event with both sendSchedulingMessages: true and explicit replyTo.imip

In every case, the event was created successfully in the organizer’s calendar, but no invitation was generated or delivered:

  • no CalendarEvent appeared in the invitee account
  • no CalendarEventNotification appeared in the invitee account
  • no email invitation appeared in the invitee mailboxes
  • no background CalendarItipMessage task appeared in the task queue
  • no outbound queued message appeared

This does not look like a simple autoAddInvitations issue, because even when automatic calendar insertion is disabled, we would still expect scheduling/invitation
processing to happen when explicitly requested.

Expected Behavior

Expected behavior:

  • When an event is created via CalendarEvent/set with attendees and sendSchedulingMessages: true, Stalwart should generate the appropriate scheduling workflow for
    those attendees.

  • For an internal attendee, this should result in either:

  • a scheduling notification/invitation being available to the recipient account, or

  • delivery through the supported scheduling path configured by Stalwart.

  • At minimum, we would expect server-side evidence that scheduling was triggered, such as a CalendarItipMessage task or an outbound queued message if iMIP is used.

Relevant docs/spec references:

The JMAP Calendars spec says CalendarEvent/set supports sendSchedulingMessages, and when this is true, the server must send the appropriate scheduling messages.

Actual Behavior

Actual behavior:

  • CalendarEvent/set succeeds and stores the event in the organizer’s calendar.
  • The attendee remains only as a participant on the organizer-side object.
  • Nothing is created or delivered on the recipient side.

Specifically:

  • recipient CalendarEvent/query returns no events
  • recipient CalendarEventNotification/query returns no notifications
  • recipient Email/query shows no invitation email
  • admin x:Task/query shows no CalendarItipMessage
  • admin x:QueuedMessage/query shows no outbound queued invitation message

So the deviation is that scheduling appears not to start at all, even when explicitly requested.

Reproduction Steps

  1. Ensure Stalwart JMAP is reachable and calendars are enabled.

  2. Verify admin configuration:

    • x:CalendarScheduling/get returns:
      • enable: true
      • httpRsvpEnable: true
      • autoAddInvitations: false
  3. Verify the organizer account has a default participant identity:

  4. Create an event from organizer to invitee using CalendarEvent/set with:

    • organizer participant
    • internal attendee participant
    • sendTo.imip on the attendee
    • sendSchedulingMessages: true
  5. Observe that the event is created successfully in the organizer account.

  6. Query the invitee account:

    • CalendarEvent/query
    • CalendarEventNotification/query
    • Email/query
  7. Query admin endpoints:

    • x:Task/query
    • x:QueuedMessage/query
  8. Observe that no scheduling artifact is produced anywhere.

Example JMAP request used for reproduction:

{
  "using": [
    "urn:ietf:params:jmap:core",
    "urn:ietf:params:jmap:calendars"
  ],
  "methodCalls": [
    [
      "CalendarEvent/set",
      {
        "accountId": "ORGANIZER_ACCOUNT_ID",
        "sendSchedulingMessages": true,
        "create": {
          "test1": {
            "@type": "Event",
            "title": "Scheduling reproduction test",
            "description": "<p>Test event</p>",
            "start": "2026-06-07T12:00:00",
            "duration": "PT30M",
            "timeZone": "Africa/Djibouti",
            "showWithoutTime": false,
            "status": "confirmed",
            "freeBusyStatus": "busy",
            "privacy": "public",
            "calendarIds": {
              "DEFAULT_CALENDAR_ID": true
            },
            "participants": {
              "p1": {
                "@type": "Participant",
                "calendarAddress": "mailto:[email protected]",
                "name": "User One",
                "roles": {
                  "owner": true,
                  "attendee": true
                },
                "participationStatus": "accepted",
                "expectReply": false,
                "kind": "individual"
              },
              "p2": {
                "@type": "Participant",
                "calendarAddress": "mailto:[email protected]",
                "name": "User Two",
                "roles": {
                  "attendee": true
                },
                "participationStatus": "needs-action",
                "expectReply": true,
                "kind": "individual",
                "sendTo": {
                  "imip": "mailto:[email protected]"
                }
              }
            }
          }
        }
      },
      "c1"
    ]
  ]
}

Observed response:

{
  "methodResponses": [
    [
      "CalendarEvent/set",
      {
        "accountId": "ORGANIZER_ACCOUNT_ID",
        "newState": "…",
        "created": {
          "test1": {
            "id": "EVENT_ID"
          }
        }
      },
      "c1"
    ]
  ],
  "sessionState": "…"
}

Then:

  • invitee CalendarEvent/query returns ids:
  • invitee CalendarEventNotification/query returns ids:
  • invitee Email/query shows no invitation
  • admin x:QueuedMessage/query returns ids:
  • admin x:Task/query does not contain any CalendarItipMessage

Relevant Log Output

No relevant logs were collected yet.

What we did verify through JMAP:

x:CalendarScheduling/get:

  • enable = true
  • httpRsvpEnable = true
  • autoAddInvitations = false

ParticipantIdentity/get on organizer:

  • default participant identity exists

Recipient-side checks after event creation:

  • CalendarEvent/query =>
  • CalendarEventNotification/query =>
  • Email/query => no invitation message found

Admin-side checks:

  • x:Task/query => no CalendarItipMessage task
  • x:QueuedMessage/query => no queued outbound message

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

PostgreSQL

Blob Storage

PostgreSQL

Search Engine

Meilisearch

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

@helpbot any comment ?

Thanks for the report. The good news is it’s not a scheduling bug, it’s a property mismatch caused by following an older revision of the JSCalendar draft.

The issue is that you’re identifying your attendees with sendTo (the { "imip": "mailto:..." } form). That property comes from RFC 8984 and the earlier JSCalendar drafts. Stalwart implements the current draft-ietf-calext-jscalendarbis, where sendTo has been retired and replaced. The spec is pretty blunt about it: “Participant.calendarAddress: This replaces the reserved Participant.sendTo property.” So sendTo is now a reserved, no-op property, and the participant’s scheduling address (the equivalent of the iCalendar ATTENDEE/ORGANIZER CAL-ADDRESS) is carried by calendarAddress instead.

Because your participants only set sendTo and never set calendarAddress, the JSCalendar to iCalendar conversion ends up producing no ATTENDEE properties at all. With no recipients, the iTIP engine has nothing to send, so there’s no CalendarItipMessage task, no outbound message, and no notification. The event itself still gets created, which is exactly why you see it appear in the organizer’s calendar with no scheduling side effects.

I’ll also own one rough edge on our side here: when you pass sendSchedulingMessages: true but the event has no schedulable participants, the request currently succeeds silently instead of returning an error. That made this far harder to diagnose than it should have been, and we’ll look at surfacing a clearer signal for that case.

To fix it, swap sendTo for calendarAddress on each participant, and set the organizer via organizerCalendarAddress at the event level (and/or an owner participant whose calendarAddress matches). Something like:

{
  "@type": "Event",
  "title": "Team sync",
  "start": "2026-06-20T10:00:00",
  "timeZone": "Europe/London",
  "duration": "PT1H",
  "organizerCalendarAddress": "mailto:[email protected]",
  "participants": {
    "org": {
      "name": "Organizer",
      "calendarAddress": "mailto:[email protected]",
      "roles": { "owner": true, "attendee": true }
    },
    "att1": {
      "name": "Attendee One",
      "calendarAddress": "mailto:[email protected]",
      "roles": { "attendee": true },
      "expectReply": true,
      "participationStatus": "needs-action"
    }
  }
}

Then send CalendarEvent/set with sendSchedulingMessages: true as before.

A few things worth double checking so the messages actually go out: the organizerCalendarAddress has to match one of the organizer account’s own configured email addresses, since Stalwart only acts as the scheduling agent when the organizer is local. The account also needs the CalendarSchedulingSend permission and iTIP enabled on the server, though your CalendarScheduling/get output already shows scheduling is on. And give the attendees expectReply: true (which maps to RSVP=TRUE) and participationStatus: "needs-action" so they’re treated as needing an invite. One last note, autoAddInvitations only controls whether an inbound invitation gets auto-filed into the recipient’s calendar, it doesn’t affect whether the organizer sends invitations in the first place.

So the short version is: switch sendTo to calendarAddress, use organizerCalendarAddress for the organizer, and the invitations will start flowing. As a general rule, anywhere the older drafts used sendTo, the current draft wants calendarAddress.