Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 40 additions & 7 deletions strr-base-web/app/utils/connect-validation/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,44 @@ export const validateEmailRfc6532Regex = (email: string): boolean => {
/^("(?:[!#-\[\]-\u{10FFFF}]|\\[\t -\u{10FFFF}])*"|[!#-'*+\-/-9=?A-Z\^-\u{10FFFF}](?:\.?[!#-'*+\-/-9=?A-Z\^-\u{10FFFF}])*)@([!#-'*+\-/-9=?A-Z\^-\u{10FFFF}](?:\.?[!#-'*+\-/-9=?A-Z\^-\u{10FFFF}])*|\[[!-Z\^-\u{10FFFF}]*\])$/u.test(email) // NOSONAR
}

export const getRequiredEmail = (message: string) =>
z.string().refine(validateEmailRfc6532Regex, message).refine(validateEmailRfc5322Regex, message)
export const getOptionalEmail = (message: string) => z.string().refine((email: string) => {
if (email) {
return validateEmailRfc6532Regex(email)
/**
* Tests an email against a stricter pattern that real world
* mail systems (and downstream notify providers) actually accept.
*
* RFC 5322/6532 allow many edge-case characters (e.g. `$` in a domain) that
* notify providers reject. Pairing those checks with this practical pattern
* keeps user input aligned with what the API can actually deliver.
*
* Rules enforced beyond the RFC checks:
* - domain must contain at least one dot (TLD required)
* - each domain label must start/end with an alphanumeric character
* - domain labels are restricted to letters, digits, and hyphens
* - top-level domain must be at least two letters
*/
export const validateEmailPractical = (email: string): boolean => {
if (!emailLengths(email)) {
return false
}
return true
}, message)
// eslint-disable-next-line no-useless-escape
return /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/.test(email)
}

const trimmedEmail = z
.string()
.transform(value => (typeof value === 'string' ? value.trim() : value))

export const getRequiredEmail = (message: string) =>
trimmedEmail
.refine(validateEmailRfc6532Regex, message)
.refine(validateEmailRfc5322Regex, message)
.refine(validateEmailPractical, message)

export const getOptionalEmail = (message: string) =>
trimmedEmail.refine((email: string) => {
if (email) {
return validateEmailRfc6532Regex(email) &&
validateEmailRfc5322Regex(email) &&
validateEmailPractical(email)
}
return true
}, message)
2 changes: 1 addition & 1 deletion strr-base-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "strr-base-web",
"private": true,
"type": "module",
"version": "0.0.44",
"version": "0.0.45",
"engines": {
"node": ">=24"
},
Expand Down
120 changes: 120 additions & 0 deletions strr-host-pm-web/tests/unit/email-validation.spec.ts
Comment thread
dimak1 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, it, expect } from 'vitest'

const VALID_EMAILS = [
'host@example.com',
'foo.bar@example.com',
'foo+bar@example.co.uk',
'foo_bar@example-host.com',
"user'name@example.com",
'a@b.co',
'123@example.com'
]

const INVALID_EMAILS = [
'',
'notanemail',
'no-at-sign.com',
'foo@',
'@example.com',
'foo@bar',
'foo@bar.',
'foo@.bar.com',
'foo@-bar.com',
'foo@bar-.com',
'bas@ba$.com',
'foo@bar..com',
'foo bar@example.com',
'foo@bar.c',
'foo@@bar.com',
`${'a'.repeat(64)}@example.com`,
`host@${'a'.repeat(250)}.com`
]

describe('validateEmailPractical', () => {
it.each(VALID_EMAILS)('accepts deliverable email: %s', (email) => {
expect(validateEmailPractical(email)).toBe(true)

Check failure on line 35 in strr-host-pm-web/tests/unit/email-validation.spec.ts

View workflow job for this annotation

GitHub Actions / strr-host-pm-ui-cd / unit-testing-pnpm (24, 10.0.0)

../tests/unit/email-validation.spec.ts > validateEmailPractical > accepts deliverable email: 123@example.com

ReferenceError: validateEmailPractical is not defined ❯ ../tests/unit/email-validation.spec.ts:35:5

Check failure on line 35 in strr-host-pm-web/tests/unit/email-validation.spec.ts

View workflow job for this annotation

GitHub Actions / strr-host-pm-ui-cd / unit-testing-pnpm (24, 10.0.0)

../tests/unit/email-validation.spec.ts > validateEmailPractical > accepts deliverable email: a@b.co

ReferenceError: validateEmailPractical is not defined ❯ ../tests/unit/email-validation.spec.ts:35:5

Check failure on line 35 in strr-host-pm-web/tests/unit/email-validation.spec.ts

View workflow job for this annotation

GitHub Actions / strr-host-pm-ui-cd / unit-testing-pnpm (24, 10.0.0)

../tests/unit/email-validation.spec.ts > validateEmailPractical > accepts deliverable email: user'name@example.com

ReferenceError: validateEmailPractical is not defined ❯ ../tests/unit/email-validation.spec.ts:35:5

Check failure on line 35 in strr-host-pm-web/tests/unit/email-validation.spec.ts

View workflow job for this annotation

GitHub Actions / strr-host-pm-ui-cd / unit-testing-pnpm (24, 10.0.0)

../tests/unit/email-validation.spec.ts > validateEmailPractical > accepts deliverable email: foo_bar@example-host.com

ReferenceError: validateEmailPractical is not defined ❯ ../tests/unit/email-validation.spec.ts:35:5

Check failure on line 35 in strr-host-pm-web/tests/unit/email-validation.spec.ts

View workflow job for this annotation

GitHub Actions / strr-host-pm-ui-cd / unit-testing-pnpm (24, 10.0.0)

../tests/unit/email-validation.spec.ts > validateEmailPractical > accepts deliverable email: foo+bar@example.co.uk

ReferenceError: validateEmailPractical is not defined ❯ ../tests/unit/email-validation.spec.ts:35:5

Check failure on line 35 in strr-host-pm-web/tests/unit/email-validation.spec.ts

View workflow job for this annotation

GitHub Actions / strr-host-pm-ui-cd / unit-testing-pnpm (24, 10.0.0)

../tests/unit/email-validation.spec.ts > validateEmailPractical > accepts deliverable email: foo.bar@example.com

ReferenceError: validateEmailPractical is not defined ❯ ../tests/unit/email-validation.spec.ts:35:5

Check failure on line 35 in strr-host-pm-web/tests/unit/email-validation.spec.ts

View workflow job for this annotation

GitHub Actions / strr-host-pm-ui-cd / unit-testing-pnpm (24, 10.0.0)

../tests/unit/email-validation.spec.ts > validateEmailPractical > accepts deliverable email: host@example.com

ReferenceError: validateEmailPractical is not defined ❯ ../tests/unit/email-validation.spec.ts:35:5
})

it.each(INVALID_EMAILS)('rejects invalid email: %s', (email) => {
expect(validateEmailPractical(email)).toBe(false)

Check failure on line 39 in strr-host-pm-web/tests/unit/email-validation.spec.ts

View workflow job for this annotation

GitHub Actions / strr-host-pm-ui-cd / unit-testing-pnpm (24, 10.0.0)

../tests/unit/email-validation.spec.ts > validateEmailPractical > rejects invalid email: no-at-sign.com

ReferenceError: validateEmailPractical is not defined ❯ ../tests/unit/email-validation.spec.ts:39:5

Check failure on line 39 in strr-host-pm-web/tests/unit/email-validation.spec.ts

View workflow job for this annotation

GitHub Actions / strr-host-pm-ui-cd / unit-testing-pnpm (24, 10.0.0)

../tests/unit/email-validation.spec.ts > validateEmailPractical > rejects invalid email: notanemail

ReferenceError: validateEmailPractical is not defined ❯ ../tests/unit/email-validation.spec.ts:39:5

Check failure on line 39 in strr-host-pm-web/tests/unit/email-validation.spec.ts

View workflow job for this annotation

GitHub Actions / strr-host-pm-ui-cd / unit-testing-pnpm (24, 10.0.0)

../tests/unit/email-validation.spec.ts > validateEmailPractical > rejects invalid email:

ReferenceError: validateEmailPractical is not defined ❯ ../tests/unit/email-validation.spec.ts:39:5
})
})

describe('getRequiredEmail', () => {
const schema = getRequiredEmail('Please enter a valid email')

it.each(VALID_EMAILS)('accepts valid email: %s', (email) => {
const result = schema.safeParse(email)
expect(result.success).toBe(true)
})

it.each(INVALID_EMAILS)('rejects invalid email: %s', (email) => {
const result = schema.safeParse(email)
expect(result.success).toBe(false)
})

it('rejects an undefined email', () => {
const result = schema.safeParse(undefined)
expect(result.success).toBe(false)
})

it('trims surrounding whitespace before validating', () => {
const result = schema.safeParse(' host@example.com ')
expect(result.success).toBe(true)
if (result.success) {
expect(result.data).toBe('host@example.com')
}
})

it('rejects whitespace-only input', () => {
const result = schema.safeParse(' ')
expect(result.success).toBe(false)
})

it('returns the configured error message for invalid input', () => {
const result = schema.safeParse('bas@ba$.com')
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues[0]?.message).toBe('Please enter a valid email')
}
})
})

describe('getOptionalEmail', () => {
const schema = getOptionalEmail('Please enter a valid email')

it('accepts an empty string', () => {
expect(schema.safeParse('').success).toBe(true)
})

it('accepts a valid email', () => {
expect(schema.safeParse('host@example.com').success).toBe(true)
})

it('rejects an invalid email when provided', () => {
expect(schema.safeParse('bas@ba$.com').success).toBe(false)
})

it('trims and accepts whitespace-padded valid email', () => {
const result = schema.safeParse(' host@example.com ')
expect(result.success).toBe(true)
if (result.success) {
expect(result.data).toBe('host@example.com')
}
})
})

describe('legacy RFC validators (still exported for backward compatibility)', () => {
it('validateEmailRfc5322Regex accepts a typical email', () => {
expect(validateEmailRfc5322Regex('host@example.com')).toBe(true)
})

it('validateEmailRfc6532Regex accepts a typical email', () => {
expect(validateEmailRfc6532Regex('host@example.com')).toBe(true)
})

it('legacy RFC checks alone do not catch domains with `$` (which the practical check now blocks)', () => {
expect(validateEmailRfc5322Regex('bas@ba$.com')).toBe(true)
expect(validateEmailPractical('bas@ba$.com')).toBe(false)
})
})
Loading