From 36160f9fbdda59efc3e6597f0c8ede792c32569e Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 1 Nov 2026 06:00:43 +0100 Subject: [PATCH] fix: answer for a group mailbox as the group when the group address is one of your identities #634 --- lib/__tests__/calendar-delegate.test.ts | 7 +++++++ lib/calendar-delegate.ts | 14 +++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/__tests__/calendar-delegate.test.ts b/lib/__tests__/calendar-delegate.test.ts index 8f3e27de1..aea6584e0 100644 --- a/lib/__tests__/calendar-delegate.test.ts +++ b/lib/__tests__/calendar-delegate.test.ts @@ -45,6 +45,13 @@ describe('resolveAnswerTargets', () => { expect(view.targets[0].sentBy).toBeUndefined(); }); + it('answers as the group also when the member has the group address as an identity', () => { + const view = resolveAnswerTargets(base(invitation(['team@example.org'], { accountId: 'team' }), { + owner: TEAM, userEmails: ['bob@example.org', 'team@example.org'], canRsvpSelf: true, + })); + expect(view.targets.map((t) => [t.kind, t.participantId, t.sentBy])).toEqual([['owner', 'team', undefined]]); + }); + it('offers no answer for the owner without edit access, and says why', () => { for (const rights of [CALENDAR_PRESETS.read, { ...CALENDAR_PRESETS.read, mayRSVP: true }]) { const view = resolveAnswerTargets(base(invitation(['alice@example.org']), { calendar: { isShared: true, myRights: rights } })); diff --git a/lib/calendar-delegate.ts b/lib/calendar-delegate.ts index 5aa8555e9..04f63083e 100644 --- a/lib/calendar-delegate.ts +++ b/lib/calendar-delegate.ts @@ -99,27 +99,31 @@ export function resolveAnswerTargets(input: ResolveAnswerInput): AnswerView { if (calendar?.isShared && owner) { const ownerId = getUserParticipantId(event, owner.emails); - if (ownerId && ownerId !== selfId && !isOrganizer(event, owner.emails) && !eventHasNoOwner(event)) { - const participant = event.participants![ownerId]; + // Members of a group often have the group's address as an identity of + // their own: the group's participant is still answered as the group. + const ownersOwn = ownerId && (ownerId !== selfId || owner.isGroup); + if (ownersOwn && !isOrganizer(event, owner.emails) && !eventHasNoOwner(event)) { + const participant = event.participants![ownerId!]; const answeredBy = addressOf(participant.sentBy); if (!calendar.myRights?.mayWriteAll) { return { targets: selfTarget(), note: { kind: 'no_write', owner: owner.name }, answeredBy }; } - if (input.server016 && selfId) { + const alsoInvited = !!selfId && selfId !== ownerId; + if (input.server016 && alsoInvited) { return { targets: ownCopyTarget(), note: { kind: 'also_invited', owner: owner.name }, answeredBy }; } const delegate = userEmails[0]?.trim().toLowerCase(); const ownerTarget: AnswerTarget = { kind: 'owner', eventId: event.id, - participantId: ownerId, + participantId: ownerId!, status: participant.participationStatus ?? null, ownerName: owner.name, // Always the mailto: form: Stalwart passes sentBy through unchecked, // and RFC 5545's SENT-BY needs a cal-address. ...(!owner.isGroup && delegate ? { sentBy: `mailto:${delegate}` } : {}), }; - return { targets: [ownerTarget, ...(selfId ? ownCopyTarget() : [])], note: null, answeredBy }; + return { targets: [ownerTarget, ...(alsoInvited ? ownCopyTarget() : [])], note: null, answeredBy }; } }