fix: Support domains without @ prefix in watchlist (#24476)

* fix: Support domains without @ prefix in watchlist

* fix: free email domain check
This commit is contained in:
Hariom Balhara
2025-10-15 13:42:02 +00:00
committed by GitHub
parent 67843adab9
commit 37cec89bf6
7 changed files with 46 additions and 47 deletions
@@ -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({
@@ -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
@@ -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");
});
});
});
@@ -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,
@@ -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,
@@ -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", () => {
@@ -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);