Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/web/components/booking/BookingListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
Tooltip,
} from "@calcom/ui";

import { AddGuestsDialog } from "@components/dialog/AddGuestsDialog";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { ReassignDialog } from "@components/dialog/ReassignDialog";
Expand Down Expand Up @@ -189,6 +190,14 @@ function BookingListItem(booking: BookingItemProps) {
},
icon: "map-pin" as const,
},
{
id: "add_members",
label: t("additional_guests"),
onClick: () => {
setIsOpenAddGuestsDialog(true);
},
icon: "user-plus" as const,
},
Comment on lines +193 to +200

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the addGuests handler implementation
rg -n "addGuests" apps/ packages/ --type ts --type tsx | head -20

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 114


🏁 Script executed:

# Search for booking status validations in the server/routers
rg -n -B5 -A10 "addGuests" packages/trpc/server/routers/ --type ts

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 8147


🏁 Script executed:

# Look for BookingStatus validation patterns
rg -n "BookingStatus\.(CANCELLED|REJECTED|COMPLETED)" packages/trpc/server/routers/viewer/bookings/ --type ts | head -30

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 817


🏁 Script executed:

# Find the addGuests procedure definition more specifically
fd -e ts -e tsx packages/trpc/server/routers/ | xargs rg -l "addGuests" | head -5

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 695


🏁 Script executed:

cat -n packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 6526


🏁 Script executed:

# Check what other handlers do for booking status validation
rg -n -A5 "BookingStatus" packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts | head -20

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 625


🏁 Script executed:

# Check if booking status is validated anywhere in the action flow
rg -n "booking.status" packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 69


🏁 Script executed:

# Check the BookingListItem logic for when editBookingActions are shown
cat -n apps/web/components/booking/BookingListItem.tsx | sed -n '540,570p'

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 1799


🏁 Script executed:

# Check the isBookingInPast logic
rg -n "isBookingInPast" apps/web/components/booking/BookingListItem.tsx -B2 -A2

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 3054


🏁 Script executed:

# Check where editBookingActions is defined and filtered
rg -n "editBookingActions" apps/web/components/booking/BookingListItem.tsx | head -15

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 208


🏁 Script executed:

# Check if booking.status is included in the Prisma query for addGuests
cat -n packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts | sed -n '26,42p'

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 547


🏁 Script executed:

# Check the schema to understand what fields are included
cat -n packages/trpc/server/routers/viewer/bookings/addGuests.schema.ts

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 360


Add booking status validation to prevent adding guests to past, cancelled, or rejected bookings.

The backend addGuests handler lacks status validation. It allows guests to be added to any confirmed booking without checking if it's in the past or if the status is cancelled/rejected. The handler doesn't even fetch the booking.status field. This is inconsistent with other handlers like requestReschedule which explicitly validate booking status.

Additionally, the UI in BookingListItem.tsx (line 554) displays the "add guests" action for past confirmed bookings without backend guard rails to prevent this operation.

Add a booking status check in the handler to reject past bookings and invalid statuses (similar to requestReschedule.handler.ts).

🤖 Prompt for AI Agents
In @apps/web/components/booking/BookingListItem.tsx around lines 193 - 200, The
addGuests flow needs server-side and client-side booking status/time validation:
in the backend addGuests handler (the function/file named addGuests handler)
fetch booking.status and booking.startAt (or equivalent start time) before
adding guests and reject with a 4xx if the booking is in the past or status is
not an active/confirmed state (mirror the logic used in
requestReschedule.handler.ts — check for cancelled/rejected and startAt < now).
On the frontend BookingListItem.tsx hide/disable the "add_members" action (the
menu item with id "add_members" / icon "user-plus" and the
setIsOpenAddGuestsDialog(true) call) when booking.status is cancelled or
rejected or when booking.startAt is before now so the UI matches the backend
guard. Ensure error messages returned by the handler are specific so the UI can
surface them.

];

if (booking.eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
Expand Down Expand Up @@ -256,6 +265,7 @@ function BookingListItem(booking: BookingItemProps) {
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
const [isOpenReassignDialog, setIsOpenReassignDialog] = useState(false);
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
const [isOpenAddGuestsDialog, setIsOpenAddGuestsDialog] = useState(false);
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
onSuccess: () => {
showToast(t("location_updated"), "success");
Expand Down Expand Up @@ -344,6 +354,11 @@ function BookingListItem(booking: BookingItemProps) {
setShowLocationModal={setIsOpenLocationDialog}
teamId={booking.eventType?.team?.id}
/>
<AddGuestsDialog
isOpenDialog={isOpenAddGuestsDialog}
setIsOpenDialog={setIsOpenAddGuestsDialog}
bookingId={booking.id}
/>
{booking.paid && booking.payment[0] && (
<ChargeCardDialog
isOpenDialog={chargeCardDialogIsOpen}
Expand Down
107 changes: 107 additions & 0 deletions apps/web/components/dialog/AddGuestsDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { Dispatch, SetStateAction } from "react";
import { useState } from "react";
import { z } from "zod";

import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
MultiEmail,
Icon,
showToast,
} from "@calcom/ui";

interface IAddGuestsDialog {
isOpenDialog: boolean;
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
bookingId: number;
}

export const AddGuestsDialog = (props: IAddGuestsDialog) => {
const { t } = useLocale();
const ZAddGuestsInputSchema = z.array(z.string().email()).refine((emails) => {
const uniqueEmails = new Set(emails);
return uniqueEmails.size === emails.length;
});
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
const utils = trpc.useUtils();
const [multiEmailValue, setMultiEmailValue] = useState<string[]>([""]);
const [isInvalidEmail, setIsInvalidEmail] = useState(false);

const addGuestsMutation = trpc.viewer.bookings.addGuests.useMutation({
onSuccess: async () => {
showToast(t("guests_added"), "success");
setIsOpenDialog(false);
setMultiEmailValue([""]);
utils.viewer.bookings.invalidate();
},
onError: (err) => {
const message = `${err.data?.code}: ${t(err.message)}`;
showToast(message || t("unable_to_add_guests"), "error");
},
});

const handleAdd = () => {
if (multiEmailValue.length === 0) {
return;
}
const validationResult = ZAddGuestsInputSchema.safeParse(multiEmailValue);
if (validationResult.success) {
addGuestsMutation.mutate({ bookingId, guests: multiEmailValue });
} else {
setIsInvalidEmail(true);
}
};
Comment on lines +48 to +58

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Improve email validation logic.

The validation has two issues:

  1. Line 49: The check multiEmailValue.length === 0 won't catch the case where the user hasn't entered any emails but still has the initial empty string [""]. Consider checking for empty or whitespace-only emails:

    const nonEmptyEmails = multiEmailValue.filter(email => email.trim() !== "");
    if (nonEmptyEmails.length === 0) {
      return;
    }
  2. Lines 26-29: The Zod schema's refine function lacks a custom error message, making it harder to debug validation failures. Add a message parameter:

    const ZAddGuestsInputSchema = z.array(z.string().email()).refine((emails) => {
      const uniqueEmails = new Set(emails);
      return uniqueEmails.size === emails.length;
    }, {
      message: "Emails must be unique"
    });
♻️ Proposed fix
  const handleAdd = () => {
-   if (multiEmailValue.length === 0) {
+   const nonEmptyEmails = multiEmailValue.filter(email => email.trim() !== "");
+   if (nonEmptyEmails.length === 0) {
      return;
    }
-   const validationResult = ZAddGuestsInputSchema.safeParse(multiEmailValue);
+   const validationResult = ZAddGuestsInputSchema.safeParse(nonEmptyEmails);
    if (validationResult.success) {
-     addGuestsMutation.mutate({ bookingId, guests: multiEmailValue });
+     addGuestsMutation.mutate({ bookingId, guests: nonEmptyEmails });
    } else {
      setIsInvalidEmail(true);
    }
  };
🤖 Prompt for AI Agents
In @apps/web/components/dialog/AddGuestsDialog.tsx around lines 48 - 58, The
current handleAdd and ZAddGuestsInputSchema validation miss edge cases: update
handleAdd to first filter multiEmailValue for non-empty, non-whitespace strings
(e.g., const nonEmptyEmails = multiEmailValue.filter(e => e.trim() !== "") ) and
use nonEmptyEmails for the empty-check and for the validation call instead of
raw multiEmailValue; when validation fails call setIsInvalidEmail(true) as
before and pass nonEmptyEmails to addGuestsMutation.mutate({ bookingId, guests:
nonEmptyEmails }). Also update ZAddGuestsInputSchema (the schema used in the
safeParse) to include a custom message on the refine that enforces uniqueness
(e.g., add the message: "Emails must be unique") so validation errors are
descriptive.


return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent enableOverflow>
<div className="flex flex-row space-x-3">
<div className="bg-subtle flex h-10 w-10 flex-shrink-0 justify-center rounded-full ">
<Icon name="user-plus" className="m-auto h-6 w-6" />
</div>
<div className="w-full pt-1">
<DialogHeader title={t("additional_guests")} />
<MultiEmail
label={t("add_emails")}
value={multiEmailValue}
readOnly={false}
setValue={setMultiEmailValue}
/>

{isInvalidEmail && (
<div className="my-4 flex text-sm text-red-700">
<div className="flex-shrink-0">
<Icon name="triangle-alert" className="h-5 w-5" />
</div>
<div className="ml-3">
<p className="font-medium">{t("emails_must_be_unique_valid")}</p>
</div>
</div>
)}

<DialogFooter>
<Button
onClick={() => {
setMultiEmailValue([""]);
setIsInvalidEmail(false);
setIsOpenDialog(false);
}}
type="button"
color="secondary">
{t("cancel")}
</Button>
<Button data-testid="add_members" loading={addGuestsMutation.isPending} onClick={handleAdd}>
{t("add")}
</Button>
</DialogFooter>
</div>
</div>
</DialogContent>
</Dialog>
);
Comment on lines +60 to +106

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Reset error state when dialog closes.

The isInvalidEmail state is reset when the Cancel button is clicked (lines 90-92), but not when the dialog is closed via other means (e.g., clicking outside or pressing ESC). This could leave the error message visible when the dialog is reopened.

♻️ Proposed fix
-   <Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
+   <Dialog open={isOpenDialog} onOpenChange={(open) => {
+     if (!open) {
+       setMultiEmailValue([""]);
+       setIsInvalidEmail(false);
+     }
+     setIsOpenDialog(open);
+   }}>

Alternatively, you could use a useEffect to reset state when the dialog closes:

useEffect(() => {
  if (!isOpenDialog) {
    setMultiEmailValue([""]);
    setIsInvalidEmail(false);
  }
}, [isOpenDialog]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent enableOverflow>
<div className="flex flex-row space-x-3">
<div className="bg-subtle flex h-10 w-10 flex-shrink-0 justify-center rounded-full ">
<Icon name="user-plus" className="m-auto h-6 w-6" />
</div>
<div className="w-full pt-1">
<DialogHeader title={t("additional_guests")} />
<MultiEmail
label={t("add_emails")}
value={multiEmailValue}
readOnly={false}
setValue={setMultiEmailValue}
/>
{isInvalidEmail && (
<div className="my-4 flex text-sm text-red-700">
<div className="flex-shrink-0">
<Icon name="triangle-alert" className="h-5 w-5" />
</div>
<div className="ml-3">
<p className="font-medium">{t("emails_must_be_unique_valid")}</p>
</div>
</div>
)}
<DialogFooter>
<Button
onClick={() => {
setMultiEmailValue([""]);
setIsInvalidEmail(false);
setIsOpenDialog(false);
}}
type="button"
color="secondary">
{t("cancel")}
</Button>
<Button data-testid="add_members" loading={addGuestsMutation.isPending} onClick={handleAdd}>
{t("add")}
</Button>
</DialogFooter>
</div>
</div>
</DialogContent>
</Dialog>
);
return (
<Dialog open={isOpenDialog} onOpenChange={(open) => {
if (!open) {
setMultiEmailValue([""]);
setIsInvalidEmail(false);
}
setIsOpenDialog(open);
}}>
<DialogContent enableOverflow>
<div className="flex flex-row space-x-3">
<div className="bg-subtle flex h-10 w-10 flex-shrink-0 justify-center rounded-full ">
<Icon name="user-plus" className="m-auto h-6 w-6" />
</div>
<div className="w-full pt-1">
<DialogHeader title={t("additional_guests")} />
<MultiEmail
label={t("add_emails")}
value={multiEmailValue}
readOnly={false}
setValue={setMultiEmailValue}
/>
{isInvalidEmail && (
<div className="my-4 flex text-sm text-red-700">
<div className="flex-shrink-0">
<Icon name="triangle-alert" className="h-5 w-5" />
</div>
<div className="ml-3">
<p className="font-medium">{t("emails_must_be_unique_valid")}</p>
</div>
</div>
)}
<DialogFooter>
<Button
onClick={() => {
setMultiEmailValue([""]);
setIsInvalidEmail(false);
setIsOpenDialog(false);
}}
type="button"
color="secondary">
{t("cancel")}
</Button>
<Button data-testid="add_members" loading={addGuestsMutation.isPending} onClick={handleAdd}>
{t("add")}
</Button>
</DialogFooter>
</div>
</div>
</DialogContent>
</Dialog>
);
🤖 Prompt for AI Agents
In @apps/web/components/dialog/AddGuestsDialog.tsx around lines 60 - 106, Reset
the dialog-local state when the dialog closes by clearing multiEmailValue and
isInvalidEmail whenever isOpenDialog becomes false; specifically, add a
useEffect that watches isOpenDialog and when it transitions to false calls
setMultiEmailValue([""]) and setIsInvalidEmail(false). This ensures the same
cleanup currently done in the Cancel button handler (which calls
setMultiEmailValue and setIsInvalidEmail) also runs for other close actions
triggered via the Dialog's onOpenChange (isOpenDialog/setIsOpenDialog).

};
6 changes: 6 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1119,7 +1119,9 @@
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
"impersonating_stop_instructions": "Click here to stop",
"event_location_changed": "Updated - Your event changed the location",
"new_guests_added": "Added - New guests added to your event",
"location_changed_event_type_subject": "Location Changed: {{eventType}} with {{name}} at {{date}}",
"guests_added_event_type_subject": "Guests Added: {{eventType}} with {{name}} at {{date}}",
"current_location": "Current Location",
"new_location": "New Location",
"session": "Session",
Expand All @@ -1131,7 +1133,10 @@
"set_location": "Set Location",
"update_location": "Update Location",
"location_updated": "Location updated",
"guests_added": "Guests added",
"unable_to_add_guests": "Unable to add guests",
"email_validation_error": "That doesn't look like an email address",
"emails_must_be_unique_valid": "Emails must be unique and valid",
"url_validation_error": "That doesn't look like a URL",
"place_where_cal_widget_appear": "Place this code in your HTML where you want your {{appName}} widget to appear.",
"create_update_react_component": "Create or update an existing React component as shown below.",
Expand Down Expand Up @@ -2426,6 +2431,7 @@
"primary": "Primary",
"make_primary": "Make primary",
"add_email": "Add Email",
"add_emails": "Add Emails",
"add_email_description": "Add an email address to replace your primary or to use as an alternative email on your event types.",
"confirm_email": "Confirm your email",
"scheduler_first_name": "The first name of the person booking",
Expand Down
28 changes: 28 additions & 0 deletions packages/emails/email-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { EmailVerifyLink } from "./templates/account-verify-email";
import AccountVerifyEmail from "./templates/account-verify-email";
import type { OrganizationNotification } from "./templates/admin-organization-notification";
import AdminOrganizationNotification from "./templates/admin-organization-notification";
import AttendeeAddGuestsEmail from "./templates/attendee-add-guests-email";
import AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email";
import AttendeeCancelledEmail from "./templates/attendee-cancelled-email";
import AttendeeCancelledSeatEmail from "./templates/attendee-cancelled-seat-email";
Expand Down Expand Up @@ -48,6 +49,7 @@ import type { OrganizationCreation } from "./templates/organization-creation-ema
import OrganizationCreationEmail from "./templates/organization-creation-email";
import type { OrganizationEmailVerify } from "./templates/organization-email-verification";
import OrganizationEmailVerification from "./templates/organization-email-verification";
import OrganizerAddGuestsEmail from "./templates/organizer-add-guests-email";
import OrganizerAttendeeCancelledSeatEmail from "./templates/organizer-attendee-cancelled-seat-email";
import OrganizerCancelledEmail from "./templates/organizer-cancelled-email";
import OrganizerDailyVideoDownloadRecordingEmail from "./templates/organizer-daily-video-download-recording-email";
Expand Down Expand Up @@ -520,6 +522,32 @@ export const sendLocationChangeEmails = async (

await Promise.all(emailsToSend);
};
export const sendAddGuestsEmails = async (calEvent: CalendarEvent, newGuests: string[]) => {
const calendarEvent = formatCalEvent(calEvent);

const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent })));

if (calendarEvent.team?.members) {
for (const teamMember of calendarEvent.team.members) {
emailsToSend.push(
sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent, teamMember }))
);
}
}

emailsToSend.push(
...calendarEvent.attendees.map((attendee) => {
if (newGuests.includes(attendee.email)) {
return sendEmail(() => new AttendeeScheduledEmail(calendarEvent, attendee));
} else {
return sendEmail(() => new AttendeeAddGuestsEmail(calendarEvent, attendee));
}
})
);

await Promise.all(emailsToSend);
};
Comment on lines +525 to +550

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for email normalization patterns in the codebase
rg -n '(toLowerCase|toLocaleLowerCase|normalizeEmail)' packages/emails/ --type=ts -C2

# Check for email includes comparisons
rg -n '\.includes\([^)]*email[^)]*\)' packages/emails/email-manager.ts -C2

# Look for email comparison patterns in the entire packages directory
rg -n 'email.*includes|includes.*email' packages/ --type=ts | head -20

# Check how newGuests is used and where it comes from
rg -n 'newGuests' packages/emails/email-manager.ts -B5 -A5

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 10772


🏁 Script executed:

# Find where sendAddGuestsEmails is called
rg -n 'sendAddGuestsEmails' --type=ts -B3 -A3

# Check how CalendarEvent and attendees are created/normalized
rg -n 'formatCalEvent|CalendarEvent' packages/emails/email-manager.ts -B2 -A2

# Look for email normalization functions in the codebase
fd -t f -e ts -e tsx | xargs rg -l 'normalizeEmail|toLowerCase.*email|email.*toLowerCase' | head -10

# Check how attendee.email is set in the system
rg -n 'attendee\.email.*=' packages/ --type=ts | head -20

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 12300


🏁 Script executed:

# Read the addGuests.handler.ts file to understand how guests are constructed
cat -n packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts | head -100

# Look for where guests are normalized or processed
rg -n 'guests' packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts -B2 -A2 | head -50

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 4478


🏁 Script executed:

# Check the schema for how attendee emails are stored
fd -t f -name "*.prisma" | xargs cat | grep -A10 -B10 "attendee" | head -50

# Look for any email normalization in the booking creation
rg -n 'toLowerCase.*email|normalizeEmail' packages/ --type=ts | head -20

# Check the input schema for addGuests
cat -n packages/trpc/server/routers/viewer/bookings/addGuests.schema.ts

# Check if attendee.email is normalized when stored
rg -n 'attendee.*email.*=' packages/features/bookings/lib/ --type=ts -A2 | head -30

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 3372


🏁 Script executed:

# Find Prisma schema file
find . -name "schema.prisma" -type f

# Check for email normalization at database creation
rg -n 'email.*lowercase|email.*normalize' packages/prisma/ --type=ts

# Look at how emails are stored when creating attendees
rg -n 'attendees.*create|createMany' packages/features/bookings/lib/ --type=ts -B3 -A5 | head -40

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 3026


🏁 Script executed:

# Extract Attendee model from Prisma schema
rg -A20 "^model Attendee" packages/prisma/schema.prisma

# Check if there's any email field configuration
rg -B5 -A15 "model Attendee" packages/prisma/schema.prisma

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 1180


Normalize email addresses before comparison to prevent case-sensitivity issues.

The comparison at line 541 (newGuests.includes(attendee.email)) is case-sensitive, which will cause new guests to receive incorrect email templates if there's a case mismatch. Additionally, the handler file has an inconsistency where blacklisted emails are normalized to lowercase (line 71 of addGuests.handler.ts) but guest emails are compared without normalization (line 76-77), potentially allowing duplicate guests to be added if they differ only in case.

Normalize email addresses with .toLowerCase() before comparison in both the handler and email-manager functions to ensure consistent behavior.

🤖 Prompt for AI Agents
In @packages/emails/email-manager.ts around lines 525 - 550, In
sendAddGuestsEmails, the comparison newGuests.includes(attendee.email) is
case-sensitive; normalize both sides by lowercasing emails before comparing —
update sendAddGuestsEmails to use newGuestsLower = newGuests.map(e =>
e.toLowerCase()) (or equivalent) and check
newGuestsLower.includes(attendee.email.toLowerCase()) when deciding which email
class to send; also ensure the addGuests handler logic that lowercases
blacklisted emails and guest emails (referenced in addGuests.handler.ts)
consistently normalizes incoming guest addresses to lowercase before
deduplication/blacklist checks so comparisons are case-insensitive across both
places.

export const sendFeedbackEmail = async (feedback: Feedback) => {
await sendEmail(() => new FeedbackEmail(feedback));
};
Expand Down
10 changes: 10 additions & 0 deletions packages/emails/src/templates/AttendeeAddGuestsEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";

export const AttendeeAddGuestsEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => (
<AttendeeScheduledEmail
title="new_guests_added"
headerType="calendarCircle"
subject="guests_added_event_type_subject"
{...props}
/>
);
11 changes: 11 additions & 0 deletions packages/emails/src/templates/OrganizerAddGuestsEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";

export const OrganizerAddGuestsEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => (
<OrganizerScheduledEmail
title="new_guests_added"
headerType="calendarCircle"
subject="guests_added_event_type_subject"
callToAction={null}
{...props}
/>
);
2 changes: 2 additions & 0 deletions packages/emails/src/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ export { AdminOrganizationNotificationEmail } from "./AdminOrganizationNotificat
export { BookingRedirectEmailNotification } from "./BookingRedirectEmailNotification";
export { VerifyEmailChangeEmail } from "./VerifyEmailChangeEmail";
export { OrganizationCreationEmail } from "./OrganizationCreationEmail";
export { OrganizerAddGuestsEmail } from "./OrganizerAddGuestsEmail";
export { AttendeeAddGuestsEmail } from "./AttendeeAddGuestsEmail";
export { OrganizationAdminNoSlotsEmail } from "./OrganizationAdminNoSlots";
34 changes: 34 additions & 0 deletions packages/emails/templates/attendee-add-guests-email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import AttendeeScheduledEmail from "./attendee-scheduled-email";

export default class AttendeeAddGuestsEmail extends AttendeeScheduledEmail {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("new_guests_added"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "attendee",
status: "CONFIRMED",
}),
method: "REQUEST",
},
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.t("guests_added_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: this.getFormattedDate(),
})}`,
html: await renderEmail("AttendeeAddGuestsEmail", {
calEvent: this.calEvent,
attendee: this.attendee,
}),
text: this.getTextBody("new_guests_added"),
};
}
}
38 changes: 38 additions & 0 deletions packages/emails/templates/organizer-add-guests-email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { APP_NAME } from "@calcom/lib/constants";

import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import OrganizerScheduledEmail from "./organizer-scheduled-email";

export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];

return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("new_guests_added"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "organizer",
status: "CONFIRMED",
}),
method: "REQUEST",
},
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Consider limiting the replyTo list for events with many guests.

Including all attendees in the replyTo field could cause issues:

  • Email servers and clients may have limits on header sizes (typically 998 characters per line per RFC 5322)
  • Events with many guests (e.g., 50+) could exceed these limits
  • This may cause email delivery failures or truncation
💡 Potential solutions

Option 1: Limit to organizer only

-      replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
+      replyTo: [this.calEvent.organizer.email],

Option 2: Limit to first N attendees

-      replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
+      replyTo: [
+        this.calEvent.organizer.email,
+        ...this.calEvent.attendees.slice(0, 5).map(({ email }) => email)
+      ],

Option 3: Add conditional logic based on attendee count

+      replyTo: this.calEvent.attendees.length > 10
+        ? [this.calEvent.organizer.email]
+        : [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
replyTo: [this.calEvent.organizer.email],
Suggested change
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
replyTo: [
this.calEvent.organizer.email,
...this.calEvent.attendees.slice(0, 5).map(({ email }) => email)
],
🤖 Prompt for AI Agents
In @packages/emails/templates/organizer-add-guests-email.ts at line 25, The
replyTo currently expands to [this.calEvent.organizer.email,
...this.calEvent.attendees.map(({ email }) => email)] which can create oversized
headers for large attendee lists; update the logic in the code that builds
replyTo (where replyTo is set) to limit entries—e.g., always include
this.calEvent.organizer.email and then append only the first N attendee emails
(pick a constant like 10) or omit attendees entirely when
this.calEvent.attendees.length exceeds a threshold; ensure you dedupe and filter
falsy emails when slicing and keep the same replyTo shape (array) so callers
remain unchanged.

subject: `${this.t("guests_added_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: this.getFormattedDate(),
})}`,
html: await renderEmail("OrganizerAddGuestsEmail", {
attendee: this.calEvent.organizer,
calEvent: this.calEvent,
}),
text: this.getTextBody("new_guests_added"),
};
}
}
19 changes: 19 additions & 0 deletions packages/trpc/server/routers/viewer/bookings/_router.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import authedProcedure from "../../../procedures/authedProcedure";
import publicProcedure from "../../../procedures/publicProcedure";
import { router } from "../../../trpc";
import { ZAddGuestsInputSchema } from "./addGuests.schema";
import { ZConfirmInputSchema } from "./confirm.schema";
import { ZEditLocationInputSchema } from "./editLocation.schema";
import { ZFindInputSchema } from "./find.schema";
Expand All @@ -14,6 +15,7 @@ type BookingsRouterHandlerCache = {
get?: typeof import("./get.handler").getHandler;
requestReschedule?: typeof import("./requestReschedule.handler").requestRescheduleHandler;
editLocation?: typeof import("./editLocation.handler").editLocationHandler;
addGuests?: typeof import("./addGuests.handler").addGuestsHandler;
confirm?: typeof import("./confirm.handler").confirmHandler;
getBookingAttendees?: typeof import("./getBookingAttendees.handler").getBookingAttendeesHandler;
find?: typeof import("./find.handler").getHandler;
Expand Down Expand Up @@ -74,6 +76,23 @@ export const bookingsRouter = router({
input,
});
}),
addGuests: authedProcedure.input(ZAddGuestsInputSchema).mutation(async ({ input, ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.addGuests) {
UNSTABLE_HANDLER_CACHE.addGuests = await import("./addGuests.handler").then(
(mod) => mod.addGuestsHandler
);
}

// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.addGuests) {
throw new Error("Failed to load handler");
}

return UNSTABLE_HANDLER_CACHE.addGuests({
ctx,
input,
});
}),

confirm: authedProcedure.input(ZConfirmInputSchema).mutation(async ({ input, ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.confirm) {
Expand Down
Loading