From ce997447a4ccb6bab643d1b35037696c66d78a8f Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Wed, 14 Jan 2026 01:26:28 +0530 Subject: [PATCH] fix: fix email exclusion validation for full emails and and @-prefixed domains (#26727) * fix(bookings): fix email exclusion validation for full emails and @-prefixed domains * NIT * make case insensitive * fix: make domain matching case-insensitive in doesEmailMatchEntry Co-Authored-By: unknown <> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../lib/getBookingResponsesSchema.test.ts | 120 ++++++++++++++++++ .../bookings/lib/getBookingResponsesSchema.ts | 28 +++- 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts index 106dfb9d3e..d3e820aacd 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts @@ -1536,6 +1536,126 @@ describe("excluded email/domain validation", () => { email: "blocked.com@allowed.com", }); }); + + test("should block full email match when exact email is excluded", async () => { + const excludedEmails = "anik@cal.com"; + + const schema = getBookingResponsesSchema({ + bookingFields: [ + { + name: "name", + type: "name", + required: true, + }, + { + name: "email", + type: "email", + required: true, + excludeEmails: excludedEmails, + }, + ] as z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">, + view: "ALL_VIEWS", + }); + + // anik@cal.com should be blocked + const parsedResponses = await schema.safeParseAsync({ + name: "test", + email: "anik@cal.com", + }); + + expect(parsedResponses.success).toBe(false); + + if (parsedResponses.success) { + throw new Error("Should not reach here"); + } + + expect(parsedResponses.error.issues[0]).toEqual( + expect.objectContaining({ + code: "custom", + message: `{email}${CUSTOM_EMAIL_EXCLUDED_ERROR_MSG}`, + }) + ); + }); + + test("should block emails with domain when domain starts with @", async () => { + const excludedEmails = "@gmail.com"; + + const schema = getBookingResponsesSchema({ + bookingFields: [ + { + name: "name", + type: "name", + required: true, + }, + { + name: "email", + type: "email", + required: true, + excludeEmails: excludedEmails, + }, + ] as z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">, + view: "ALL_VIEWS", + }); + + // anik@gmail.com should be blocked when @gmail.com is excluded + const parsedResponses = await schema.safeParseAsync({ + name: "test", + email: "anik@gmail.com", + }); + + expect(parsedResponses.success).toBe(false); + + if (parsedResponses.success) { + throw new Error("Should not reach here"); + } + + expect(parsedResponses.error.issues[0]).toEqual( + expect.objectContaining({ + code: "custom", + message: `{email}${CUSTOM_EMAIL_EXCLUDED_ERROR_MSG}`, + }) + ); + }); + + test("should block emails with domain when excluded email is just the domain without @", async () => { + const excludedEmails = "gmail.com"; + + const schema = getBookingResponsesSchema({ + bookingFields: [ + { + name: "name", + type: "name", + required: true, + }, + { + name: "email", + type: "email", + required: true, + excludeEmails: excludedEmails, + }, + ] as z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">, + view: "ALL_VIEWS", + }); + + // test@gmail.com should be blocked when gmail.com is excluded + const parsedResponses = await schema.safeParseAsync({ + name: "test", + email: "test@gmail.com", + }); + + expect(parsedResponses.success).toBe(false); + + if (parsedResponses.success) { + throw new Error("Should not reach here"); + } + + expect(parsedResponses.error.issues[0]).toEqual( + expect.objectContaining({ + code: "custom", + message: `{email}${CUSTOM_EMAIL_EXCLUDED_ERROR_MSG}`, + }) + ); + }); }); describe("require email/domain validation", () => { diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.ts b/packages/features/bookings/lib/getBookingResponsesSchema.ts index f858b84203..41918948bf 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.ts @@ -24,6 +24,28 @@ const ensureValidPhoneNumber = (value: string) => { // Replace the space(s) in the beginning with + as it is supposed to be provided in the beginning only return value.replace(/^ +/, "+"); }; + +/** + * Checks if a booker email matches an email/domain entry. + * Supports three formats: + * - Full email: "user@example.com" - matches exactly + * - Domain with @ prefix: "@example.com" - matches any email ending with "@example.com" + * - Domain without @ prefix: "example.com" - matches any email ending with "@example.com" + */ +const doesEmailMatchEntry = (bookerEmail: string, entry: string): boolean => { + const bookerEmailLower = bookerEmail.toLowerCase(); + + if (entry.startsWith("@")) { + const domain = entry.slice(1).toLowerCase(); + return bookerEmailLower.endsWith("@" + domain); + } + + if (entry.includes("@")) { + return bookerEmailLower === entry.toLowerCase(); + } + + return bookerEmailLower.endsWith("@" + entry.toLowerCase()); +}; export const getBookingResponsesPartialSchema = ({ bookingFields, view, translateFn }: CommonParams) => { const schema = bookingResponses.unwrap().partial().and(catchAllSchema); @@ -208,7 +230,7 @@ function preprocess({ const excludedEmails = bookingField.excludeEmails?.split(",").map((domain) => domain.trim()) || []; - const match = excludedEmails.find((email) => bookerEmail.endsWith("@" + email)); + const match = excludedEmails.find((excludedEntry) => doesEmailMatchEntry(bookerEmail, excludedEntry)); if (match) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -220,7 +242,9 @@ function preprocess({ ?.split(",") .map((domain) => domain.trim()) .filter(Boolean) || []; - const requiredEmailsMatch = requiredEmails.find((email) => bookerEmail.endsWith("@" + email)); + const requiredEmailsMatch = requiredEmails.find((requiredEntry) => + doesEmailMatchEntry(bookerEmail, requiredEntry) + ); if (requiredEmails.length > 0 && !requiredEmailsMatch) { ctx.addIssue({ code: z.ZodIssueCode.custom,