From 37cec89bf66f38fee2fd2ab54ec3c0b70598bb68 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Wed, 15 Oct 2025 19:12:02 +0530 Subject: [PATCH] fix: Support domains without @ prefix in watchlist (#24476) * fix: Support domains without @ prefix in watchlist * fix: free email domain check --- .../test/spam-booking.integration-test.ts | 8 +++--- .../checkIfFreeEmailDomain.ts | 7 ++--- .../lib/service/GlobalBlockingService.test.ts | 20 ++++++------- .../OrganizationBlockingService.test.ts | 10 +++---- .../lib/service/WatchlistService.test.ts | 10 +++---- .../watchlist/lib/utils/normalization.test.ts | 28 +++++++++---------- .../watchlist/lib/utils/normalization.ts | 10 +++---- 7 files changed, 46 insertions(+), 47 deletions(-) diff --git a/packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts b/packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts index 94787b2d93..aa2e8ea5f4 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts @@ -243,8 +243,8 @@ describe("handleNewBooking - Spam Detection", () => { "should block booking when domain is in global watchlist and return decoy response", async () => { const handleNewBooking = getNewBookingHandler(); - const blockedDomain = "@globalspammydomain.com"; - const blockedEmail = `user${blockedDomain}`; + const blockedDomain = "globalspammydomain.com"; + const blockedEmail = `user@${blockedDomain}`; const booker = getBooker({ email: blockedEmail, @@ -496,8 +496,8 @@ describe("handleNewBooking - Spam Detection", () => { "should block booking when domain is in organization watchlist and return decoy response", async () => { const handleNewBooking = getNewBookingHandler(); - const blockedDomain = "@spammydomain.com"; - const blockedEmail = `user${blockedDomain}`; + const blockedDomain = "spammydomain.com"; + const blockedEmail = `user@${blockedDomain}`; // Create organization with a team const org = await createOrganization({ diff --git a/packages/features/watchlist/lib/freeEmailDomainCheck/checkIfFreeEmailDomain.ts b/packages/features/watchlist/lib/freeEmailDomainCheck/checkIfFreeEmailDomain.ts index 43dba2381f..f9442ac8f5 100644 --- a/packages/features/watchlist/lib/freeEmailDomainCheck/checkIfFreeEmailDomain.ts +++ b/packages/features/watchlist/lib/freeEmailDomainCheck/checkIfFreeEmailDomain.ts @@ -13,16 +13,15 @@ export const checkIfFreeEmailDomain = async (params: CheckFreeEmailDomainParams) try { const emailDomain = extractDomainFromEmail(email); - const domainWithoutAt = emailDomain.slice(1); // If there's no email domain return as if it was a free email domain - if (!domainWithoutAt) return true; + if (!emailDomain) return true; // Gmail and Outlook are one of the most common email domains so we don't need to check the domains list - if (domainWithoutAt === "gmail.com" || domainWithoutAt === "outlook.com") return true; + if (emailDomain === "gmail.com" || emailDomain === "outlook.com") return true; const watchlist = await getWatchlistFeature(); - return await watchlist.globalBlocking.isFreeEmailDomain(domainWithoutAt); + return await watchlist.globalBlocking.isFreeEmailDomain(emailDomain); } catch (err) { log?.error(err); // If normalization fails, treat as free email domain for safety diff --git a/packages/features/watchlist/lib/service/GlobalBlockingService.test.ts b/packages/features/watchlist/lib/service/GlobalBlockingService.test.ts index 516bb18377..92ca4a3d7c 100644 --- a/packages/features/watchlist/lib/service/GlobalBlockingService.test.ts +++ b/packages/features/watchlist/lib/service/GlobalBlockingService.test.ts @@ -47,14 +47,14 @@ describe("GlobalBlockingService", () => { expect(result.reason).toBe(WatchlistType.EMAIL); expect(result.watchlistEntry).toEqual(mockEntry); expect(mockGlobalRepo.findBlockedEmail).toHaveBeenCalledWith("blocked@example.com"); - expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com"); + expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("example.com"); }); test("should return blocked when domain matches", async () => { const mockEntry = { id: "456", type: WatchlistType.DOMAIN, - value: "@spam.com", + value: "spam.com", description: null, action: WatchlistAction.BLOCK, isGlobal: true, @@ -72,7 +72,7 @@ describe("GlobalBlockingService", () => { expect(result.reason).toBe(WatchlistType.DOMAIN); expect(result.watchlistEntry).toEqual(mockEntry); expect(mockGlobalRepo.findBlockedEmail).toHaveBeenCalledWith("user@spam.com"); - expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("@spam.com"); + expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("spam.com"); }); test("should return not blocked when no matches", async () => { @@ -93,7 +93,7 @@ describe("GlobalBlockingService", () => { await service.isBlocked("USER@EXAMPLE.COM"); expect(mockGlobalRepo.findBlockedEmail).toHaveBeenCalledWith("user@example.com"); - expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com"); + expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("example.com"); }); test("should check both email and domain in parallel", async () => { @@ -126,7 +126,7 @@ describe("GlobalBlockingService", () => { const domainEntry = { id: "456", type: WatchlistType.DOMAIN, - value: "@example.com", + value: "example.com", description: null, action: WatchlistAction.BLOCK, isGlobal: true, @@ -151,7 +151,7 @@ describe("GlobalBlockingService", () => { const mockEntry = { id: "789", type: WatchlistType.DOMAIN, - value: "@yahoo.com", + value: "yahoo.com", description: null, action: WatchlistAction.REPORT, isGlobal: true, @@ -165,7 +165,7 @@ describe("GlobalBlockingService", () => { const result = await service.isFreeEmailDomain("yahoo.com"); expect(result).toBe(true); - expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@yahoo.com"); + expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("yahoo.com"); }); test("should return false when domain is not in free email list", async () => { @@ -174,7 +174,7 @@ describe("GlobalBlockingService", () => { const result = await service.isFreeEmailDomain("corporatedomain.com"); expect(result).toBe(false); - expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@corporatedomain.com"); + expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("corporatedomain.com"); }); test("should normalize domain before checking", async () => { @@ -182,7 +182,7 @@ describe("GlobalBlockingService", () => { await service.isFreeEmailDomain("GMAIL.COM"); - expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@gmail.com"); + expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("gmail.com"); }); test("should handle domain with @ prefix", async () => { @@ -190,7 +190,7 @@ describe("GlobalBlockingService", () => { await service.isFreeEmailDomain("@hotmail.com"); - expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@hotmail.com"); + expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("hotmail.com"); }); }); }); diff --git a/packages/features/watchlist/lib/service/OrganizationBlockingService.test.ts b/packages/features/watchlist/lib/service/OrganizationBlockingService.test.ts index 129e00b35d..f8342b9b8a 100644 --- a/packages/features/watchlist/lib/service/OrganizationBlockingService.test.ts +++ b/packages/features/watchlist/lib/service/OrganizationBlockingService.test.ts @@ -52,14 +52,14 @@ describe("OrganizationBlockingService", () => { email: "blocked@example.com", organizationId: ORGANIZATION_ID, }); - expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com", ORGANIZATION_ID); + expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("example.com", ORGANIZATION_ID); }); test("should return blocked when domain matches for organization", async () => { const mockEntry = { id: "456", type: WatchlistType.DOMAIN, - value: "@competitor.com", + value: "competitor.com", description: null, action: WatchlistAction.BLOCK, isGlobal: false, @@ -99,7 +99,7 @@ describe("OrganizationBlockingService", () => { email: "user@example.com", organizationId: ORGANIZATION_ID, }); - expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com", ORGANIZATION_ID); + expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("example.com", ORGANIZATION_ID); }); test("should check both email and domain in parallel", async () => { @@ -132,7 +132,7 @@ describe("OrganizationBlockingService", () => { const domainEntry = { id: "456", type: WatchlistType.DOMAIN, - value: "@example.com", + value: "example.com", description: null, action: WatchlistAction.BLOCK, isGlobal: false, @@ -165,7 +165,7 @@ describe("OrganizationBlockingService", () => { email: "test@example.com", organizationId: ORG_A, }); - expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com", ORG_A); + expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("example.com", ORG_A); expect(mockOrgRepo.findBlockedEmail).not.toHaveBeenCalledWith({ email: "test@example.com", organizationId: ORG_B, diff --git a/packages/features/watchlist/lib/service/WatchlistService.test.ts b/packages/features/watchlist/lib/service/WatchlistService.test.ts index 1910a48a39..c20fc55c77 100644 --- a/packages/features/watchlist/lib/service/WatchlistService.test.ts +++ b/packages/features/watchlist/lib/service/WatchlistService.test.ts @@ -86,7 +86,7 @@ describe("WatchlistService", () => { const mockEntry = { id: "456", type: WatchlistType.DOMAIN, - value: "@spam.com", + value: "spam.com", description: "Spam domain", action: WatchlistAction.BLOCK, isGlobal: true, @@ -107,7 +107,7 @@ describe("WatchlistService", () => { expect(mockGlobalRepo.createEntry).toHaveBeenCalledWith({ type: WatchlistType.DOMAIN, - value: "@spam.com", // Normalized with @ prefix + value: "spam.com", // Normalized without @ prefix description: "Spam domain", action: WatchlistAction.BLOCK, source: undefined, @@ -172,7 +172,7 @@ describe("WatchlistService", () => { const mockEntry = { id: "free-1", type: WatchlistType.DOMAIN, - value: "@gmail.com", + value: "gmail.com", description: null, action: WatchlistAction.REPORT, isGlobal: true, @@ -193,7 +193,7 @@ describe("WatchlistService", () => { expect(mockGlobalRepo.createEntry).toHaveBeenCalledWith({ type: WatchlistType.DOMAIN, - value: "@gmail.com", + value: "gmail.com", description: undefined, action: WatchlistAction.REPORT, source: WatchlistSource.FREE_DOMAIN_POLICY, @@ -326,7 +326,7 @@ describe("WatchlistService", () => { { id: "2", type: WatchlistType.DOMAIN, - value: "@competitor.com", + value: "competitor.com", description: null, action: WatchlistAction.BLOCK, isGlobal: false, diff --git a/packages/features/watchlist/lib/utils/normalization.test.ts b/packages/features/watchlist/lib/utils/normalization.test.ts index b5524d2aa4..a94d7173fe 100644 --- a/packages/features/watchlist/lib/utils/normalization.test.ts +++ b/packages/features/watchlist/lib/utils/normalization.test.ts @@ -18,20 +18,20 @@ describe("normalization", () => { describe("normalizeDomain", () => { test("should normalize basic domain", () => { - expect(normalizeDomain("Example.COM")).toBe("@example.com"); - expect(normalizeDomain("@Domain.ORG")).toBe("@domain.org"); - expect(normalizeDomain(" sub.domain.net ")).toBe("@sub.domain.net"); + expect(normalizeDomain("Example.COM")).toBe("example.com"); + expect(normalizeDomain("@Domain.ORG")).toBe("domain.org"); + expect(normalizeDomain(" sub.domain.net ")).toBe("sub.domain.net"); }); test("should preserve full domain including subdomains", () => { - expect(normalizeDomain("mail.google.com")).toBe("@mail.google.com"); - expect(normalizeDomain("sub.domain.example.org")).toBe("@sub.domain.example.org"); + expect(normalizeDomain("mail.google.com")).toBe("mail.google.com"); + expect(normalizeDomain("sub.domain.example.org")).toBe("sub.domain.example.org"); }); test("should handle multi-level TLDs correctly", () => { - expect(normalizeDomain("example.co.uk")).toBe("@example.co.uk"); - expect(normalizeDomain("mail.example.co.uk")).toBe("@mail.example.co.uk"); - expect(normalizeDomain("company.com.au")).toBe("@company.com.au"); + expect(normalizeDomain("example.co.uk")).toBe("example.co.uk"); + expect(normalizeDomain("mail.example.co.uk")).toBe("mail.example.co.uk"); + expect(normalizeDomain("company.com.au")).toBe("company.com.au"); }); test("should throw on invalid domains", () => { @@ -43,14 +43,14 @@ describe("normalization", () => { describe("extractDomainFromEmail", () => { test("should extract and normalize domain from email", () => { - expect(extractDomainFromEmail("user@Example.COM")).toBe("@example.com"); - expect(extractDomainFromEmail("test@sub.domain.org")).toBe("@sub.domain.org"); - expect(extractDomainFromEmail("user@mail.google.com")).toBe("@mail.google.com"); + expect(extractDomainFromEmail("user@Example.COM")).toBe("example.com"); + expect(extractDomainFromEmail("test@sub.domain.org")).toBe("sub.domain.org"); + expect(extractDomainFromEmail("user@mail.google.com")).toBe("mail.google.com"); }); test("should handle multi-level TLDs", () => { - expect(extractDomainFromEmail("user@example.co.uk")).toBe("@example.co.uk"); - expect(extractDomainFromEmail("admin@mail.company.com.au")).toBe("@mail.company.com.au"); + expect(extractDomainFromEmail("user@example.co.uk")).toBe("example.co.uk"); + expect(extractDomainFromEmail("admin@mail.company.com.au")).toBe("mail.company.com.au"); }); test("should throw on invalid emails", () => { @@ -78,7 +78,7 @@ describe("normalization", () => { describe("Edge cases and consistency", () => { test("should handle international domains", () => { // Note: In a real implementation, you might want to handle punycode - expect(normalizeDomain("münchen.de")).toBe("@münchen.de"); + expect(normalizeDomain("münchen.de")).toBe("münchen.de"); }); test("should be consistent across multiple calls", () => { diff --git a/packages/features/watchlist/lib/utils/normalization.ts b/packages/features/watchlist/lib/utils/normalization.ts index 851a6b93cf..04f2c2377c 100644 --- a/packages/features/watchlist/lib/utils/normalization.ts +++ b/packages/features/watchlist/lib/utils/normalization.ts @@ -32,14 +32,14 @@ export function normalizeEmail(email: string): string { * Rules applied: * 1. Convert to lowercase * 2. Trim whitespace - * 3. Ensure proper @ prefix for domain entries + * 3. Remove @ prefix if present * - * Note: Domains are stored AS-IS with @ prefix (e.g., @mail.google.com, @example.co.uk) + * Note: Domains are stored without @ prefix (e.g., mail.google.com, example.co.uk) * No subdomain stripping is performed to avoid multi-level TLD issues. * If you want to block subdomains separately, create separate entries. * * @param domain - Raw domain (with or without @ prefix) - * @returns Normalized domain with @ prefix + * @returns Normalized domain without @ prefix */ export function normalizeDomain(domain: string): string { let normalized = domain.trim().toLowerCase(); @@ -54,14 +54,14 @@ export function normalizeDomain(domain: string): string { throw new Error(`Invalid domain format: ${domain}`); } - return `@${normalized}`; + return normalized; } /** * Extracts and normalizes domain from an email address * * @param email - Email address - * @returns Normalized domain with @ prefix + * @returns Normalized domain without @ prefix */ export function extractDomainFromEmail(email: string): string { const normalizedEmail = normalizeEmail(email);