Skip to content

Commit 983af06

Browse files
authored
fix: allow two dots in exclude email (calcom#24968)
* add test and fix two dots bug in exclude email * add test and fix two dots bug in exclude email
1 parent 29076d6 commit 983af06

2 files changed

Lines changed: 111 additions & 7 deletions

File tree

packages/prisma/zod-utils.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { describe, it, expect } from "vitest";
2+
import z from "zod";
3+
4+
import { excludeOrRequireEmailSchema } from "./zod-utils";
5+
6+
describe("excludeOrRequireEmailSchema", () => {
7+
const parse = (input: string) => z.object({ v: excludeOrRequireEmailSchema }).safeParse({ v: input });
8+
9+
describe("valid inputs", () => {
10+
it("accepts single TLD domains", () => {
11+
expect(parse("gmail.com").success).toBe(true);
12+
});
13+
14+
it("accepts uppercase domains", () => {
15+
expect(parse("GMAIL.COM").success).toBe(true);
16+
});
17+
18+
it("accepts multi-level TLD domains (co.uk)", () => {
19+
expect(parse("hotmail.co.uk").success).toBe(true);
20+
});
21+
22+
it("accepts multi-level TLD domains (k12.us)", () => {
23+
expect(parse("mail.school.k12.us").success).toBe(true);
24+
});
25+
26+
it("accepts full email addresses", () => {
27+
expect(parse("user@example.co.uk").success).toBe(true);
28+
});
29+
30+
it("accepts @domain format", () => {
31+
expect(parse("@example.co.uk").success).toBe(true);
32+
});
33+
34+
it("accepts multiple comma-separated entries", () => {
35+
expect(parse("gmail.com, @example.com, user@example.co.uk").success).toBe(true);
36+
});
37+
38+
it("accepts domains with trailing comma (ignores empty entry)", () => {
39+
expect(parse("gmail.com, hotmail.co.uk,").success).toBe(true);
40+
});
41+
42+
it("accepts domains with extra spaces", () => {
43+
expect(parse(" gmail.com , hotmail.co.uk ").success).toBe(true);
44+
});
45+
46+
it("accepts domains with numbers and hyphens", () => {
47+
expect(parse("example123.com").success).toBe(true);
48+
expect(parse("my-domain.com").success).toBe(true);
49+
});
50+
51+
it("accepts very short domains (min 2-char TLD)", () => {
52+
expect(parse("a.co").success).toBe(true);
53+
});
54+
55+
it("accepts long multi-level domains", () => {
56+
expect(parse("example.co.uk.test.com").success).toBe(true);
57+
});
58+
});
59+
60+
describe("invalid inputs", () => {
61+
it("rejects single-label domains and missing dots", () => {
62+
expect(parse("example").success).toBe(false);
63+
expect(parse("@example").success).toBe(false);
64+
});
65+
66+
it("rejects invalid TLD lengths", () => {
67+
expect(parse("example.c").success).toBe(false);
68+
});
69+
70+
it("rejects leading/trailing hyphens in labels", () => {
71+
expect(parse("-bad.com").success).toBe(false);
72+
expect(parse("bad-.com").success).toBe(false);
73+
});
74+
75+
it("rejects leading/trailing dots and consecutive dots", () => {
76+
expect(parse(".bad.com").success).toBe(false);
77+
expect(parse("bad.com.").success).toBe(false);
78+
expect(parse("ex..ample.com").success).toBe(false);
79+
});
80+
81+
it("allows empty input but rejects commas-only", () => {
82+
expect(parse("").success).toBe(true);
83+
expect(parse(" ").success).toBe(true);
84+
expect(parse(",,,").success).toBe(false);
85+
});
86+
87+
it("rejects Unicode domains (ASCII-only)", () => {
88+
expect(parse("münchen.de").success).toBe(false);
89+
});
90+
});
91+
});

packages/prisma/zod-utils.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -950,16 +950,29 @@ export const fieldTypeEnum = z.enum([
950950
export type FieldType = z.infer<typeof fieldTypeEnum>;
951951

952952
export const excludeOrRequireEmailSchema = z.string().superRefine((val, ctx) => {
953-
const allDomains = val.split(",").map((dom) => dom.trim());
953+
// Allow empty input: field is optional at the form level but may come through as empty string
954+
if (val.trim() === "") return;
954955

955-
const regex = /^(?:@?[a-z0-9-]+(?:\.[a-z]{2,})?)?(?:@[a-z0-9-]+\.[a-z]{2,})?$/i;
956+
const allDomains = val
957+
.split(",")
958+
.map((dom) => dom.trim())
959+
.filter(Boolean);
956960

957-
/*
958-
Valid patterns - [ example, example.anything, anyone@example.anything ]
959-
Invalid patterns - Patterns involving capital letter [ Example, Example.anything, Anyone@example.anything ]
960-
*/
961+
// If user entered only separators/commas, treat as invalid input
962+
if (allDomains.length === 0) {
963+
ctx.addIssue({
964+
code: z.ZodIssueCode.custom,
965+
message: "Enter valid domain or email",
966+
});
967+
return;
968+
}
969+
970+
// Accept forms: domain-only, `@domain`, or `local@domain`
971+
// - Domain labels: alnum, hyphens allowed internally, no leading/trailing hyphen
972+
// - Require at least one dot and end with an alpha TLD of length ≥2
973+
const EMAIL_OR_DOMAIN_PATTERN = /^(?:[a-z0-9._+'-]+@|@)?(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/i;
961974

962-
const isValid = !allDomains.some((domain) => !regex.test(domain));
975+
const isValid = allDomains.every((entry) => EMAIL_OR_DOMAIN_PATTERN.test(entry));
963976

964977
if (!isValid) {
965978
ctx.addIssue({

0 commit comments

Comments
 (0)