fix: use calendarAddress/organizerCalendarAddress for scheduling, drop retired sendTo/replyTo #500
This commit is contained in:
@@ -522,13 +522,14 @@ export function EventModal({
|
|||||||
{ name: organizerName, email: organizerEmail },
|
{ name: organizerName, email: organizerEmail },
|
||||||
effectiveAttendees
|
effectiveAttendees
|
||||||
) as Record<string, CalendarParticipant>;
|
) as Record<string, CalendarParticipant>;
|
||||||
data.replyTo = { imip: `mailto:${organizerEmail}` };
|
|
||||||
// Stalwart (calcard) derives the iCalendar ORGANIZER property solely from
|
// Stalwart (calcard) derives the iCalendar ORGANIZER property solely from
|
||||||
// organizerCalendarAddress; without it no ORGANIZER is emitted and iTIP
|
// organizerCalendarAddress; without it no ORGANIZER is emitted and iTIP
|
||||||
// scheduling is silently skipped (NoSchedulingInfo), so no invites are sent.
|
// scheduling is silently skipped (NoSchedulingInfo), so no invites are sent.
|
||||||
|
// The RFC 8984 replyTo property is retired in jscalendarbis and ignored.
|
||||||
data.organizerCalendarAddress = `mailto:${organizerEmail}`;
|
data.organizerCalendarAddress = `mailto:${organizerEmail}`;
|
||||||
} else if (effectiveAttendees.length === 0 && event?.participants) {
|
} else if (effectiveAttendees.length === 0 && event?.participants) {
|
||||||
data.participants = null;
|
data.participants = null;
|
||||||
|
// Also clear the retired replyTo that older releases (<= 1.7.6) wrote.
|
||||||
data.replyTo = null;
|
data.replyTo = null;
|
||||||
data.organizerCalendarAddress = null;
|
data.organizerCalendarAddress = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -540,7 +540,9 @@ describe('CalendarInvitationBanner', () => {
|
|||||||
mocks.clientMock,
|
mocks.clientMock,
|
||||||
'event-8',
|
'event-8',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
replyTo: { imip: 'mailto:organizer@example.com' },
|
// The stored event lacks an ORGANIZER, so the RSVP repair writes
|
||||||
|
// organizerCalendarAddress (replyTo is retired in jscalendarbis).
|
||||||
|
organizerCalendarAddress: 'mailto:organizer@example.com',
|
||||||
participants: expect.objectContaining({
|
participants: expect.objectContaining({
|
||||||
attendee: expect.objectContaining({
|
attendee: expect.objectContaining({
|
||||||
participationStatus: 'accepted',
|
participationStatus: 'accepted',
|
||||||
|
|||||||
@@ -582,7 +582,12 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
|||||||
} else {
|
} else {
|
||||||
await updateEvent(client, eventForRsvp.id, {
|
await updateEvent(client, eventForRsvp.id, {
|
||||||
participants: repairedParticipants,
|
participants: repairedParticipants,
|
||||||
replyTo: replyToForRsvp ?? undefined,
|
// Stalwart routes the iTIP REPLY via the stored ORGANIZER
|
||||||
|
// (organizerCalendarAddress; the RFC 8984 replyTo is retired).
|
||||||
|
// Only repair a missing organizer - attendees may not modify it.
|
||||||
|
...(replyToForRsvp?.imip && !eventForRsvp.organizerCalendarAddress
|
||||||
|
? { organizerCalendarAddress: replyToForRsvp.imip }
|
||||||
|
: {}),
|
||||||
}, true);
|
}, true);
|
||||||
setRsvpStatus(status);
|
setRsvpStatus(status);
|
||||||
setActionNotice(t('rsvp_sent'));
|
setActionNotice(t('rsvp_sent'));
|
||||||
|
|||||||
@@ -285,7 +285,10 @@ describe('buildParticipantMap', () => {
|
|||||||
expect(org!.roles).toEqual({ owner: true, attendee: true });
|
expect(org!.roles).toEqual({ owner: true, attendee: true });
|
||||||
expect(org!.participationStatus).toBe('accepted');
|
expect(org!.participationStatus).toBe('accepted');
|
||||||
expect(org!.scheduleAgent).toBe('server');
|
expect(org!.scheduleAgent).toBe('server');
|
||||||
expect(org!.sendTo).toEqual({ imip: 'mailto:alice@example.com' });
|
// sendTo is retired in draft-ietf-calext-jscalendarbis; the scheduling
|
||||||
|
// address is carried by calendarAddress instead.
|
||||||
|
expect(org!.sendTo).toBeUndefined();
|
||||||
|
expect(org!.calendarAddress).toBe('mailto:alice@example.com');
|
||||||
expect(org!.expectReply).toBe(false);
|
expect(org!.expectReply).toBe(false);
|
||||||
|
|
||||||
const att0 = entries.find(p => p.email === 'bob@example.com');
|
const att0 = entries.find(p => p.email === 'bob@example.com');
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ export function buildParticipantMap(
|
|||||||
|
|
||||||
const generateId = () => generateUUID();
|
const generateId = () => generateUUID();
|
||||||
|
|
||||||
|
// calendarAddress is the scheduling address in draft-ietf-calext-jscalendarbis
|
||||||
|
// (implemented by Stalwart); the RFC 8984 sendTo property is retired there and
|
||||||
|
// stored as an inert JSPROP, so it is intentionally not sent.
|
||||||
participants[generateId()] = {
|
participants[generateId()] = {
|
||||||
'@type': 'Participant',
|
'@type': 'Participant',
|
||||||
name: organizer.name,
|
name: organizer.name,
|
||||||
@@ -115,7 +118,6 @@ export function buildParticipantMap(
|
|||||||
roles: { owner: true, attendee: true },
|
roles: { owner: true, attendee: true },
|
||||||
participationStatus: 'accepted',
|
participationStatus: 'accepted',
|
||||||
scheduleAgent: 'server',
|
scheduleAgent: 'server',
|
||||||
sendTo: { imip: `mailto:${organizer.email}` },
|
|
||||||
expectReply: false,
|
expectReply: false,
|
||||||
kind: 'individual',
|
kind: 'individual',
|
||||||
};
|
};
|
||||||
@@ -129,7 +131,6 @@ export function buildParticipantMap(
|
|||||||
roles: { attendee: true },
|
roles: { attendee: true },
|
||||||
participationStatus: 'needs-action',
|
participationStatus: 'needs-action',
|
||||||
scheduleAgent: 'server',
|
scheduleAgent: 'server',
|
||||||
sendTo: { imip: `mailto:${a.email}` },
|
|
||||||
expectReply: true,
|
expectReply: true,
|
||||||
kind: 'individual',
|
kind: 'individual',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -567,10 +567,13 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
const escapedId = participantId.replace(/~/g, '~0').replace(/\//g, '~1');
|
const escapedId = participantId.replace(/~/g, '~0').replace(/\//g, '~1');
|
||||||
const patchKey = `participants/${escapedId}/participationStatus`;
|
const patchKey = `participants/${escapedId}/participationStatus`;
|
||||||
const patch: Record<string, unknown> = { [patchKey]: status };
|
const patch: Record<string, unknown> = { [patchKey]: status };
|
||||||
// Include replyTo so the server knows where to deliver the iTIP reply
|
// Stalwart routes the iTIP REPLY to the stored ORGANIZER
|
||||||
// (may be missing if the event was imported or auto-created without it).
|
// (organizerCalendarAddress); the RFC 8984 replyTo property is retired
|
||||||
if (replyTo) {
|
// in jscalendarbis and ignored. Repair events that are missing the
|
||||||
patch.replyTo = replyTo;
|
// organizer (e.g. imported ones), but never touch an existing one -
|
||||||
|
// attendees may not modify the ORGANIZER.
|
||||||
|
if (replyTo?.imip && storeEvent && !storeEvent.organizerCalendarAddress) {
|
||||||
|
patch.organizerCalendarAddress = replyTo.imip;
|
||||||
}
|
}
|
||||||
await client.updateCalendarEvent(
|
await client.updateCalendarEvent(
|
||||||
realId,
|
realId,
|
||||||
@@ -677,9 +680,11 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
'@type': 'Participant',
|
'@type': 'Participant',
|
||||||
name: p.name,
|
name: p.name,
|
||||||
email: p.email,
|
email: p.email,
|
||||||
calendarAddress: p.calendarAddress,
|
// calendarAddress carries the scheduling address in jscalendarbis
|
||||||
|
// (Stalwart); sendTo is retired there, so it only serves as a
|
||||||
|
// fallback source for events parsed from legacy RFC 8984 data.
|
||||||
|
calendarAddress: p.calendarAddress || p.sendTo?.imip,
|
||||||
description: p.description,
|
description: p.description,
|
||||||
sendTo: p.sendTo,
|
|
||||||
kind: p.kind,
|
kind: p.kind,
|
||||||
roles: p.roles,
|
roles: p.roles,
|
||||||
participationStatus: p.participationStatus,
|
participationStatus: p.participationStatus,
|
||||||
@@ -719,7 +724,11 @@ export const useCalendarStore = create<CalendarStore>()(
|
|||||||
keywords: src.keywords,
|
keywords: src.keywords,
|
||||||
categories: src.categories,
|
categories: src.categories,
|
||||||
locale: src.locale,
|
locale: src.locale,
|
||||||
replyTo: src.replyTo || (src.organizerCalendarAddress ? { imip: src.organizerCalendarAddress } : undefined),
|
// Stalwart derives the iCalendar ORGANIZER solely from
|
||||||
|
// organizerCalendarAddress (replyTo is retired in jscalendarbis);
|
||||||
|
// dropping it here would strip the ORGANIZER from imported invites
|
||||||
|
// and break RSVP replies afterwards.
|
||||||
|
organizerCalendarAddress: src.organizerCalendarAddress || src.replyTo?.imip,
|
||||||
locations: src.locations,
|
locations: src.locations,
|
||||||
virtualLocations: src.virtualLocations,
|
virtualLocations: src.virtualLocations,
|
||||||
links: src.links,
|
links: src.links,
|
||||||
|
|||||||
Reference in New Issue
Block a user