fix: send the RSVP for invitations outside the loaded calendar window

Accepting an invitation looked the event up in the calendar store,
which only holds the window the calendar last showed. For an event
further out the lookup came back empty, no response was sent, and the
banner still said "Added to calendar".

- find the event on the server with a `uid` filter, like webmail's
  `queryCalendarEvents({ uid })`, both when the banner opens (so it
  shows as already in the calendar) and after importing it
- let `rsvpEvent` take the event when it isn't in the store, so the
  organizer repair still applies
- show "Could not send your response" whenever no response went out
This commit is contained in:
Linus Rath
2026-09-24 19:12:02 +02:00
parent 15717f137a
commit c0582ab950
5 changed files with 117 additions and 19 deletions
+30
View File
@@ -16,6 +16,7 @@ import {
createEvent,
updateEvent,
deleteEvents,
findEventsByUid,
toLocalDateTime,
} from '../calendar';
@@ -337,6 +338,35 @@ describe('calendar operations', () => {
});
});
describe('findEventsByUid', () => {
it('queries by uid, whatever the date, and fetches the matches', async () => {
mockRequest
.mockResolvedValueOnce({
methodResponses: [['CalendarEvent/query', { ids: ['ev9'] }, '0']],
})
.mockResolvedValueOnce({
methodResponses: [['CalendarEvent/get', { list: [{ id: 'ev9', uid: 'inv@example.com' }] }, '0']],
});
const found = await findEventsByUid('inv@example.com');
const query = mockRequest.mock.calls[0][0][0];
expect(query[0]).toBe('CalendarEvent/query');
expect(query[1]).toEqual({ accountId: 'acc-1', filter: { uid: 'inv@example.com' } });
expect(mockRequest.mock.calls[1][0][0][1].ids).toEqual(['ev9']);
expect(found.map((e) => e.id)).toEqual(['ev9']);
});
it('returns nothing without a second request when no event matches', async () => {
mockRequest.mockResolvedValueOnce({
methodResponses: [['CalendarEvent/query', { ids: [] }, '0']],
});
expect(await findEventsByUid('missing@example.com')).toEqual([]);
expect(mockRequest).toHaveBeenCalledTimes(1);
});
});
describe('deleteEvents', () => {
it('should destroy events by ids', async () => {
mockRequest.mockResolvedValue({
+15
View File
@@ -388,6 +388,21 @@ export async function getEvents(ids: string[], accountId?: string): Promise<Cale
return all;
}
/**
* The calendar objects carrying an iCalendar UID, found on the server with a
* `uid` filter — unlike the store, not limited to the loaded date window.
* Mirrors webmail's `queryCalendarEvents({ uid })`.
*/
export async function findEventsByUid(uid: string, accountId?: string): Promise<CalendarEvent[]> {
const account = accountId || jmapClient.accountId;
const res = await jmapClient.request(
[['CalendarEvent/query', { accountId: account, filter: { uid } }, '0']],
USING,
);
const ids = methodResult<{ ids: string[] }>(res).ids ?? [];
return getEvents(ids, accountId);
}
// `sendSchedulingMessages` asks Stalwart to deliver iMIP (RFC 6047) invitation
// / reply / cancellation emails to participants. We pass it whenever an event
// has participants so creating/updating/deleting a meeting notifies attendees.