Skip to content

Commit 37cec89

Browse files
fix: Support domains without @ prefix in watchlist (calcom#24476)
* fix: Support domains without @ prefix in watchlist * fix: free email domain check
1 parent 67843ad commit 37cec89

7 files changed

Lines changed: 46 additions & 47 deletions

File tree

packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,8 @@ describe("handleNewBooking - Spam Detection", () => {
243243
"should block booking when domain is in global watchlist and return decoy response",
244244
async () => {
245245
const handleNewBooking = getNewBookingHandler();
246-
const blockedDomain = "@globalspammydomain.com";
247-
const blockedEmail = `user${blockedDomain}`;
246+
const blockedDomain = "globalspammydomain.com";
247+
const blockedEmail = `user@${blockedDomain}`;
248248

249249
const booker = getBooker({
250250
email: blockedEmail,
@@ -496,8 +496,8 @@ describe("handleNewBooking - Spam Detection", () => {
496496
"should block booking when domain is in organization watchlist and return decoy response",
497497
async () => {
498498
const handleNewBooking = getNewBookingHandler();
499-
const blockedDomain = "@spammydomain.com";
500-
const blockedEmail = `user${blockedDomain}`;
499+
const blockedDomain = "spammydomain.com";
500+
const blockedEmail = `user@${blockedDomain}`;
501501

502502
// Create organization with a team
503503
const org = await createOrganization({

packages/features/watchlist/lib/freeEmailDomainCheck/checkIfFreeEmailDomain.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,15 @@ export const checkIfFreeEmailDomain = async (params: CheckFreeEmailDomainParams)
1313

1414
try {
1515
const emailDomain = extractDomainFromEmail(email);
16-
const domainWithoutAt = emailDomain.slice(1);
1716

1817
// If there's no email domain return as if it was a free email domain
19-
if (!domainWithoutAt) return true;
18+
if (!emailDomain) return true;
2019

2120
// Gmail and Outlook are one of the most common email domains so we don't need to check the domains list
22-
if (domainWithoutAt === "gmail.com" || domainWithoutAt === "outlook.com") return true;
21+
if (emailDomain === "gmail.com" || emailDomain === "outlook.com") return true;
2322

2423
const watchlist = await getWatchlistFeature();
25-
return await watchlist.globalBlocking.isFreeEmailDomain(domainWithoutAt);
24+
return await watchlist.globalBlocking.isFreeEmailDomain(emailDomain);
2625
} catch (err) {
2726
log?.error(err);
2827
// If normalization fails, treat as free email domain for safety

packages/features/watchlist/lib/service/GlobalBlockingService.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ describe("GlobalBlockingService", () => {
4747
expect(result.reason).toBe(WatchlistType.EMAIL);
4848
expect(result.watchlistEntry).toEqual(mockEntry);
4949
expect(mockGlobalRepo.findBlockedEmail).toHaveBeenCalledWith("blocked@example.com");
50-
expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com");
50+
expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("example.com");
5151
});
5252

5353
test("should return blocked when domain matches", async () => {
5454
const mockEntry = {
5555
id: "456",
5656
type: WatchlistType.DOMAIN,
57-
value: "@spam.com",
57+
value: "spam.com",
5858
description: null,
5959
action: WatchlistAction.BLOCK,
6060
isGlobal: true,
@@ -72,7 +72,7 @@ describe("GlobalBlockingService", () => {
7272
expect(result.reason).toBe(WatchlistType.DOMAIN);
7373
expect(result.watchlistEntry).toEqual(mockEntry);
7474
expect(mockGlobalRepo.findBlockedEmail).toHaveBeenCalledWith("user@spam.com");
75-
expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("@spam.com");
75+
expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("spam.com");
7676
});
7777

7878
test("should return not blocked when no matches", async () => {
@@ -93,7 +93,7 @@ describe("GlobalBlockingService", () => {
9393
await service.isBlocked("USER@EXAMPLE.COM");
9494

9595
expect(mockGlobalRepo.findBlockedEmail).toHaveBeenCalledWith("user@example.com");
96-
expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com");
96+
expect(mockGlobalRepo.findBlockedDomain).toHaveBeenCalledWith("example.com");
9797
});
9898

9999
test("should check both email and domain in parallel", async () => {
@@ -126,7 +126,7 @@ describe("GlobalBlockingService", () => {
126126
const domainEntry = {
127127
id: "456",
128128
type: WatchlistType.DOMAIN,
129-
value: "@example.com",
129+
value: "example.com",
130130
description: null,
131131
action: WatchlistAction.BLOCK,
132132
isGlobal: true,
@@ -151,7 +151,7 @@ describe("GlobalBlockingService", () => {
151151
const mockEntry = {
152152
id: "789",
153153
type: WatchlistType.DOMAIN,
154-
value: "@yahoo.com",
154+
value: "yahoo.com",
155155
description: null,
156156
action: WatchlistAction.REPORT,
157157
isGlobal: true,
@@ -165,7 +165,7 @@ describe("GlobalBlockingService", () => {
165165
const result = await service.isFreeEmailDomain("yahoo.com");
166166

167167
expect(result).toBe(true);
168-
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@yahoo.com");
168+
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("yahoo.com");
169169
});
170170

171171
test("should return false when domain is not in free email list", async () => {
@@ -174,23 +174,23 @@ describe("GlobalBlockingService", () => {
174174
const result = await service.isFreeEmailDomain("corporatedomain.com");
175175

176176
expect(result).toBe(false);
177-
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@corporatedomain.com");
177+
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("corporatedomain.com");
178178
});
179179

180180
test("should normalize domain before checking", async () => {
181181
vi.mocked(mockGlobalRepo.findFreeEmailDomain).mockResolvedValue(null);
182182

183183
await service.isFreeEmailDomain("GMAIL.COM");
184184

185-
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@gmail.com");
185+
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("gmail.com");
186186
});
187187

188188
test("should handle domain with @ prefix", async () => {
189189
vi.mocked(mockGlobalRepo.findFreeEmailDomain).mockResolvedValue(null);
190190

191191
await service.isFreeEmailDomain("@hotmail.com");
192192

193-
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("@hotmail.com");
193+
expect(mockGlobalRepo.findFreeEmailDomain).toHaveBeenCalledWith("hotmail.com");
194194
});
195195
});
196196
});

packages/features/watchlist/lib/service/OrganizationBlockingService.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ describe("OrganizationBlockingService", () => {
5252
email: "blocked@example.com",
5353
organizationId: ORGANIZATION_ID,
5454
});
55-
expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com", ORGANIZATION_ID);
55+
expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("example.com", ORGANIZATION_ID);
5656
});
5757

5858
test("should return blocked when domain matches for organization", async () => {
5959
const mockEntry = {
6060
id: "456",
6161
type: WatchlistType.DOMAIN,
62-
value: "@competitor.com",
62+
value: "competitor.com",
6363
description: null,
6464
action: WatchlistAction.BLOCK,
6565
isGlobal: false,
@@ -99,7 +99,7 @@ describe("OrganizationBlockingService", () => {
9999
email: "user@example.com",
100100
organizationId: ORGANIZATION_ID,
101101
});
102-
expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com", ORGANIZATION_ID);
102+
expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("example.com", ORGANIZATION_ID);
103103
});
104104

105105
test("should check both email and domain in parallel", async () => {
@@ -132,7 +132,7 @@ describe("OrganizationBlockingService", () => {
132132
const domainEntry = {
133133
id: "456",
134134
type: WatchlistType.DOMAIN,
135-
value: "@example.com",
135+
value: "example.com",
136136
description: null,
137137
action: WatchlistAction.BLOCK,
138138
isGlobal: false,
@@ -165,7 +165,7 @@ describe("OrganizationBlockingService", () => {
165165
email: "test@example.com",
166166
organizationId: ORG_A,
167167
});
168-
expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("@example.com", ORG_A);
168+
expect(mockOrgRepo.findBlockedDomain).toHaveBeenCalledWith("example.com", ORG_A);
169169
expect(mockOrgRepo.findBlockedEmail).not.toHaveBeenCalledWith({
170170
email: "test@example.com",
171171
organizationId: ORG_B,

packages/features/watchlist/lib/service/WatchlistService.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ describe("WatchlistService", () => {
8686
const mockEntry = {
8787
id: "456",
8888
type: WatchlistType.DOMAIN,
89-
value: "@spam.com",
89+
value: "spam.com",
9090
description: "Spam domain",
9191
action: WatchlistAction.BLOCK,
9292
isGlobal: true,
@@ -107,7 +107,7 @@ describe("WatchlistService", () => {
107107

108108
expect(mockGlobalRepo.createEntry).toHaveBeenCalledWith({
109109
type: WatchlistType.DOMAIN,
110-
value: "@spam.com", // Normalized with @ prefix
110+
value: "spam.com", // Normalized without @ prefix
111111
description: "Spam domain",
112112
action: WatchlistAction.BLOCK,
113113
source: undefined,
@@ -172,7 +172,7 @@ describe("WatchlistService", () => {
172172
const mockEntry = {
173173
id: "free-1",
174174
type: WatchlistType.DOMAIN,
175-
value: "@gmail.com",
175+
value: "gmail.com",
176176
description: null,
177177
action: WatchlistAction.REPORT,
178178
isGlobal: true,
@@ -193,7 +193,7 @@ describe("WatchlistService", () => {
193193

194194
expect(mockGlobalRepo.createEntry).toHaveBeenCalledWith({
195195
type: WatchlistType.DOMAIN,
196-
value: "@gmail.com",
196+
value: "gmail.com",
197197
description: undefined,
198198
action: WatchlistAction.REPORT,
199199
source: WatchlistSource.FREE_DOMAIN_POLICY,
@@ -326,7 +326,7 @@ describe("WatchlistService", () => {
326326
{
327327
id: "2",
328328
type: WatchlistType.DOMAIN,
329-
value: "@competitor.com",
329+
value: "competitor.com",
330330
description: null,
331331
action: WatchlistAction.BLOCK,
332332
isGlobal: false,

packages/features/watchlist/lib/utils/normalization.test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,20 @@ describe("normalization", () => {
1818

1919
describe("normalizeDomain", () => {
2020
test("should normalize basic domain", () => {
21-
expect(normalizeDomain("Example.COM")).toBe("@example.com");
22-
expect(normalizeDomain("@Domain.ORG")).toBe("@domain.org");
23-
expect(normalizeDomain(" sub.domain.net ")).toBe("@sub.domain.net");
21+
expect(normalizeDomain("Example.COM")).toBe("example.com");
22+
expect(normalizeDomain("@Domain.ORG")).toBe("domain.org");
23+
expect(normalizeDomain(" sub.domain.net ")).toBe("sub.domain.net");
2424
});
2525

2626
test("should preserve full domain including subdomains", () => {
27-
expect(normalizeDomain("mail.google.com")).toBe("@mail.google.com");
28-
expect(normalizeDomain("sub.domain.example.org")).toBe("@sub.domain.example.org");
27+
expect(normalizeDomain("mail.google.com")).toBe("mail.google.com");
28+
expect(normalizeDomain("sub.domain.example.org")).toBe("sub.domain.example.org");
2929
});
3030

3131
test("should handle multi-level TLDs correctly", () => {
32-
expect(normalizeDomain("example.co.uk")).toBe("@example.co.uk");
33-
expect(normalizeDomain("mail.example.co.uk")).toBe("@mail.example.co.uk");
34-
expect(normalizeDomain("company.com.au")).toBe("@company.com.au");
32+
expect(normalizeDomain("example.co.uk")).toBe("example.co.uk");
33+
expect(normalizeDomain("mail.example.co.uk")).toBe("mail.example.co.uk");
34+
expect(normalizeDomain("company.com.au")).toBe("company.com.au");
3535
});
3636

3737
test("should throw on invalid domains", () => {
@@ -43,14 +43,14 @@ describe("normalization", () => {
4343

4444
describe("extractDomainFromEmail", () => {
4545
test("should extract and normalize domain from email", () => {
46-
expect(extractDomainFromEmail("user@Example.COM")).toBe("@example.com");
47-
expect(extractDomainFromEmail("test@sub.domain.org")).toBe("@sub.domain.org");
48-
expect(extractDomainFromEmail("user@mail.google.com")).toBe("@mail.google.com");
46+
expect(extractDomainFromEmail("user@Example.COM")).toBe("example.com");
47+
expect(extractDomainFromEmail("test@sub.domain.org")).toBe("sub.domain.org");
48+
expect(extractDomainFromEmail("user@mail.google.com")).toBe("mail.google.com");
4949
});
5050

5151
test("should handle multi-level TLDs", () => {
52-
expect(extractDomainFromEmail("user@example.co.uk")).toBe("@example.co.uk");
53-
expect(extractDomainFromEmail("admin@mail.company.com.au")).toBe("@mail.company.com.au");
52+
expect(extractDomainFromEmail("user@example.co.uk")).toBe("example.co.uk");
53+
expect(extractDomainFromEmail("admin@mail.company.com.au")).toBe("mail.company.com.au");
5454
});
5555

5656
test("should throw on invalid emails", () => {
@@ -78,7 +78,7 @@ describe("normalization", () => {
7878
describe("Edge cases and consistency", () => {
7979
test("should handle international domains", () => {
8080
// Note: In a real implementation, you might want to handle punycode
81-
expect(normalizeDomain("münchen.de")).toBe("@münchen.de");
81+
expect(normalizeDomain("münchen.de")).toBe("münchen.de");
8282
});
8383

8484
test("should be consistent across multiple calls", () => {

packages/features/watchlist/lib/utils/normalization.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ export function normalizeEmail(email: string): string {
3232
* Rules applied:
3333
* 1. Convert to lowercase
3434
* 2. Trim whitespace
35-
* 3. Ensure proper @ prefix for domain entries
35+
* 3. Remove @ prefix if present
3636
*
37-
* Note: Domains are stored AS-IS with @ prefix (e.g., @mail.google.com, @example.co.uk)
37+
* Note: Domains are stored without @ prefix (e.g., mail.google.com, example.co.uk)
3838
* No subdomain stripping is performed to avoid multi-level TLD issues.
3939
* If you want to block subdomains separately, create separate entries.
4040
*
4141
* @param domain - Raw domain (with or without @ prefix)
42-
* @returns Normalized domain with @ prefix
42+
* @returns Normalized domain without @ prefix
4343
*/
4444
export function normalizeDomain(domain: string): string {
4545
let normalized = domain.trim().toLowerCase();
@@ -54,14 +54,14 @@ export function normalizeDomain(domain: string): string {
5454
throw new Error(`Invalid domain format: ${domain}`);
5555
}
5656

57-
return `@${normalized}`;
57+
return normalized;
5858
}
5959

6060
/**
6161
* Extracts and normalizes domain from an email address
6262
*
6363
* @param email - Email address
64-
* @returns Normalized domain with @ prefix
64+
* @returns Normalized domain without @ prefix
6565
*/
6666
export function extractDomainFromEmail(email: string): string {
6767
const normalizedEmail = normalizeEmail(email);

0 commit comments

Comments
 (0)