mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: find the invited attendee under any sending identity when answering an invite #1283
This commit is contained in:
@@ -23,6 +23,7 @@ const mocks = vi.hoisted(() => {
|
||||
const authState = {
|
||||
client: clientMock,
|
||||
primaryIdentity: { email: 'user@example.com' },
|
||||
identities: [{ email: 'user@example.com' }] as Array<{ email: string }>,
|
||||
getClientForAccount: vi.fn((id: string) => (id === 'login-b' ? sourceClientMock : undefined)),
|
||||
};
|
||||
|
||||
@@ -188,6 +189,7 @@ describe('CalendarInvitationBanner', () => {
|
||||
mocks.sourceClientMock.fetchBlob.mockReset();
|
||||
mocks.sourceClientMock.fetchBlob.mockRejectedValue(new Error('no blob'));
|
||||
mocks.authState.getClientForAccount.mockClear();
|
||||
mocks.authState.identities = [{ email: 'user@example.com' }];
|
||||
mocks.emailState.viewingAccountId = null;
|
||||
mocks.emailState.selectedMailbox = 'inbox';
|
||||
mocks.emailState.mailboxes = [];
|
||||
@@ -546,6 +548,41 @@ describe('CalendarInvitationBanner', () => {
|
||||
expect(await screen.findByText('Response sent')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('finds the user under an alias identity the invite was sent to (#1283)', async () => {
|
||||
mocks.authState.identities = [{ email: 'user@example.com' }, { email: 'Alias@example.com' }];
|
||||
const participants = {
|
||||
aliasAttendee: {
|
||||
'@type': 'Participant',
|
||||
email: 'alias@example.com',
|
||||
name: 'User',
|
||||
roles: { attendee: true },
|
||||
participationStatus: 'needs-action',
|
||||
},
|
||||
};
|
||||
mocks.calendarState.events = [
|
||||
{ id: 'event-8', uid: 'uid-8', start: '2026-03-30T09:00:00Z', participants },
|
||||
];
|
||||
mocks.clientMock.parseCalendarEvents.mockResolvedValue([
|
||||
{ uid: 'uid-8', title: 'Alias Event', start: '2026-03-30T09:00:00Z', participants },
|
||||
]);
|
||||
mocks.calendarState.rsvpEvent.mockResolvedValue(true);
|
||||
|
||||
render(<CalendarInvitationBanner email={makeEmail({ id: 'email-8' })} />);
|
||||
|
||||
fireEvent.click(await screen.findByText('Accept'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.calendarState.rsvpEvent).toHaveBeenCalledWith(
|
||||
mocks.clientMock,
|
||||
'event-8',
|
||||
'aliasAttendee',
|
||||
'accepted',
|
||||
null,
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText('Response sent')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('says nothing was sent when a plugin stops the answer', async () => {
|
||||
mocks.calendarState.events = [
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
getInvitationTrustAssessment,
|
||||
formatEventSummary,
|
||||
formatInvitationSchedule,
|
||||
findParticipantByEmail,
|
||||
findParticipantByEmails,
|
||||
extractMethodFromRawIcs,
|
||||
type EventSummary,
|
||||
type InvitationMethod,
|
||||
@@ -390,7 +390,14 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
||||
const viewedMailbox = viewMailboxes.find((m) => m.id === selectedMailboxId);
|
||||
const sharedOwnerAccountId = viewedMailbox?.isShared ? viewedMailbox.accountId : undefined;
|
||||
const blobAccountId = email.sourceAccountId ?? sharedOwnerAccountId ?? parseClient?.getAccountId();
|
||||
const currentUserEmail = useAuthStore((s) => s.primaryIdentity?.email);
|
||||
// Every address the user sends as, primary first: an invite may be
|
||||
// addressed to an alias identity (#1283).
|
||||
const identityEmails = useAuthStore(useShallow((s) => {
|
||||
const emails = [s.primaryIdentity?.email, ...s.identities.map((identity) => identity.email)]
|
||||
.filter((address): address is string => Boolean(address))
|
||||
.map((address) => address.toLowerCase());
|
||||
return Array.from(new Set(emails));
|
||||
}));
|
||||
const calendarInvitationParsingEnabled = useSettingsStore((s) => s.calendarInvitationParsingEnabled);
|
||||
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
||||
const { calendars, supportsCalendar, importEvents, rsvpEvent, updateEvent, events: storeEvents, setSelectedDate } = useCalendarStore(useShallow((state) => ({
|
||||
@@ -524,12 +531,12 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
||||
? storeEvents.find((e) => e.uid === parsedEvent.uid)
|
||||
: null;
|
||||
|
||||
const currentUserParticipant = existingEvent && currentUserEmail
|
||||
? findParticipantByEmail(existingEvent, currentUserEmail)
|
||||
const currentUserParticipant = existingEvent
|
||||
? findParticipantByEmails(existingEvent, identityEmails)
|
||||
: null;
|
||||
|
||||
const fallbackParsedParticipant = !currentUserParticipant && parsedEvent && currentUserEmail
|
||||
? findParticipantByEmail(parsedEvent, currentUserEmail)
|
||||
const fallbackParsedParticipant = !currentUserParticipant && parsedEvent
|
||||
? findParticipantByEmails(parsedEvent, identityEmails)
|
||||
: null;
|
||||
|
||||
const participantForUser = currentUserParticipant || fallbackParsedParticipant;
|
||||
@@ -545,9 +552,8 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
||||
|| myParticipant?.participant.participationStatus
|
||||
|| null;
|
||||
const userIsOrganizer = Boolean(isOnlyOrganizer);
|
||||
const userIsSender = Boolean(
|
||||
currentUserEmail && email.from?.[0]?.email?.toLowerCase() === currentUserEmail.toLowerCase()
|
||||
);
|
||||
const senderAddress = email.from?.[0]?.email?.toLowerCase();
|
||||
const userIsSender = Boolean(senderAddress && identityEmails.includes(senderAddress));
|
||||
const bannerTitle = getBannerTitle(t, method);
|
||||
const bannerInfo = getBannerInfo(t, method, userIsOrganizer, supportsCalendar);
|
||||
const trustAssessment = parsedEvent && !userIsSender ? getInvitationTrustAssessment(parsedEvent, email, method) : null;
|
||||
@@ -602,8 +608,8 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
||||
const eventForRsvp = existingEvent
|
||||
? await resolveExistingEventForRsvp()
|
||||
: null;
|
||||
const existingEventParticipant = eventForRsvp && currentUserEmail
|
||||
? findParticipantByEmail(eventForRsvp, currentUserEmail)
|
||||
const existingEventParticipant = eventForRsvp
|
||||
? findParticipantByEmails(eventForRsvp, identityEmails)
|
||||
: null;
|
||||
const canFallbackToParsedParticipant = Boolean(
|
||||
existingEvent
|
||||
@@ -646,7 +652,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
||||
(e) => e.uid === parsedEvent.uid
|
||||
);
|
||||
const participant = myParticipant
|
||||
|| (newEvent && currentUserEmail ? findParticipantByEmail(newEvent, currentUserEmail) : null);
|
||||
|| (newEvent ? findParticipantByEmails(newEvent, identityEmails) : null);
|
||||
if (newEvent && participant) {
|
||||
if (!(await rsvpEvent(client, newEvent.id, participant.id, status, replyToForRsvp))) return;
|
||||
setRsvpStatus(status);
|
||||
|
||||
@@ -702,6 +702,22 @@ export function formatInvitationSchedule(
|
||||
return range.end ? `${start} – ${formatPoint(range.end, true)}` : start;
|
||||
}
|
||||
|
||||
/**
|
||||
* The participant matching the first of `emails` found in the event. The
|
||||
* user's identities, primary first: an invite may be addressed to an alias
|
||||
* identity rather than the primary address (#1283).
|
||||
*/
|
||||
export function findParticipantByEmails(
|
||||
event: Partial<CalendarEvent>,
|
||||
emails: readonly string[]
|
||||
): { id: string; participant: CalendarParticipant } | null {
|
||||
for (const email of emails) {
|
||||
const found = findParticipantByEmail(event, email);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findParticipantByEmail(
|
||||
event: Partial<CalendarEvent>,
|
||||
email: string
|
||||
|
||||
Reference in New Issue
Block a user