From afe443267d78b7edb043b5141a7572128a390627 Mon Sep 17 00:00:00 2001 From: Patrik Date: Wed, 19 Mar 2025 09:24:45 -0400 Subject: [PATCH] fix: email format validation with hyphens (#11761) This PR updates the email validation regex to better handle use cases with hyphens. Changes: - Disallows domains starting or ending with a hyphen (`user@-example.com`, `user@example-.com`). - Allows domains with consecutive hyphens inside (`user@ex--ample.com`). - Allows multiple subdomains (`user@sub.domain.example.com`). - Adds `int test` coverage for multiple domain use case scenarios. --- packages/payload/src/fields/validations.ts | 13 +++-- test/auth/int.spec.ts | 65 +++++++++++++++++++++- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/packages/payload/src/fields/validations.ts b/packages/payload/src/fields/validations.ts index 919cc83376..a22e5cd5d8 100644 --- a/packages/payload/src/fields/validations.ts +++ b/packages/payload/src/fields/validations.ts @@ -192,12 +192,15 @@ export const email: EmailFieldValidation = ( /** * Disallows emails with double quotes (e.g., "user"@example.com, user@"example.com", "user@example.com") - * Rejects spaces anywhere in the email (e.g., user @example.com) - * Prevents consecutive dots (e.g., user..name@example.com) - * Ensures a valid domain (e.g., rejects user@example, user@example..com) - * Allows standard formats like user@example.com, user.name+alias@example.co.uk + * Rejects spaces anywhere in the email (e.g., user @example.com, user@ example.com, user name@example.com) + * Prevents consecutive dots in the local or domain part (e.g., user..name@example.com, user@example..com) + * Disallows domains that start or end with a hyphen (e.g., user@-example.com, user@example-.com) + * Allows standard email formats (e.g., user@example.com, user.name+alias@example.co.uk, user-name@example.org) + * Allows domains with consecutive hyphens as long as they are not leading/trailing (e.g., user@ex--ample.com) + * Supports multiple subdomains (e.g., user@sub.domain.example.com) */ - const emailRegex = /^(?!.*\.\.)[\w.%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i + const emailRegex = + /^(?!.*\.\.)[\w.%+-]+@[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*\.[a-z]{2,}$/i if ((value && !emailRegex.test(value)) || (!value && required)) { return t('validation:emailAddress') diff --git a/test/auth/int.spec.ts b/test/auth/int.spec.ts index 99fa758c49..32f639b30b 100644 --- a/test/auth/int.spec.ts +++ b/test/auth/int.spec.ts @@ -1,7 +1,15 @@ -import type { FieldAffectingData, Payload, User } from 'payload' +import type { + BasePayload, + EmailFieldValidation, + FieldAffectingData, + Payload, + SanitizedConfig, + User, +} from 'payload' import { jwtDecode } from 'jwt-decode' import path from 'path' +import { email as emailValidation } from 'payload/shared' import { fileURLToPath } from 'url' import { v4 as uuid } from 'uuid' @@ -969,4 +977,59 @@ describe('Auth', () => { ).rejects.toThrow('Token is either invalid or has expired.') }) }) + + describe('Email - format validation', () => { + const mockT = jest.fn((key) => key) // Mocks translation function + + const mockContext: Parameters[1] = { + // @ts-expect-error: Mocking context for email validation + req: { + payload: { + collections: {} as Record, + config: {} as SanitizedConfig, + } as unknown as BasePayload, + t: mockT, + }, + required: true, + siblingData: {}, + blockData: {}, + data: {}, + path: ['email'], + preferences: { fields: {} }, + } + it('should allow standard formatted emails', () => { + expect(emailValidation('user@example.com', mockContext)).toBe(true) + expect(emailValidation('user.name+alias@example.co.uk', mockContext)).toBe(true) + expect(emailValidation('user-name@example.org', mockContext)).toBe(true) + expect(emailValidation('user@ex--ample.com', mockContext)).toBe(true) + }) + + it('should not allow emails with double quotes', () => { + expect(emailValidation('"user"@example.com', mockContext)).toBe('validation:emailAddress') + expect(emailValidation('user@"example.com"', mockContext)).toBe('validation:emailAddress') + expect(emailValidation('"user@example.com"', mockContext)).toBe('validation:emailAddress') + }) + + it('should not allow emails with spaces', () => { + expect(emailValidation('user @example.com', mockContext)).toBe('validation:emailAddress') + expect(emailValidation('user@ example.com', mockContext)).toBe('validation:emailAddress') + expect(emailValidation('user name@example.com', mockContext)).toBe('validation:emailAddress') + }) + + it('should not allow emails with consecutive dots', () => { + expect(emailValidation('user..name@example.com', mockContext)).toBe('validation:emailAddress') + expect(emailValidation('user@example..com', mockContext)).toBe('validation:emailAddress') + }) + + it('should not allow emails with invalid domains', () => { + expect(emailValidation('user@example', mockContext)).toBe('validation:emailAddress') + expect(emailValidation('user@example..com', mockContext)).toBe('validation:emailAddress') + expect(emailValidation('user@example.c', mockContext)).toBe('validation:emailAddress') + }) + + it('should not allow domains starting or ending with a hyphen', () => { + expect(emailValidation('user@-example.com', mockContext)).toBe('validation:emailAddress') + expect(emailValidation('user@example-.com', mockContext)).toBe('validation:emailAddress') + }) + }) })