Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More flexible parsing options and a stricter default #92

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Validates email addresses based on regex, common typos, disposable email blacklists, DNS records and SMTP server response.

- Validates email looks like an email i.e. contains an "@" and a "." to the right of it.
- Identify strings that looks like an email using the `isEmail` function (i.e. contains an "@" and a "." to the right of it).
- Validates email address using a regex with options to change parsing strictness, see [parse options](#parse-options).
- Validates common typos e.g. example@gmaill.com using [mailcheck](https://github.com/mailcheck/mailcheck).
- Validates email was not generated by disposable email service using [disposable-email-domains](https://github.com/ivolo/disposable-email-domains).
- Validates MX records are present on DNS.
Expand Down Expand Up @@ -84,6 +85,27 @@ await validate({
```
For a list of TLDs that are supported by default you can see [here](https://github.com/mailcheck/mailcheck/blob/afca031b4ce1cdc6e3ecbe88198f41b4835f81e3/src/mailcheck.js#L31).

> [!IMPORTANT]
> You must enable `validateRegex` for other validations to be reliable.

## Parse Options

The email address specification is quite complex and there are multiple conflicting standards, the default options are based on WhatWG recommendation which is the reference for form validation in most modern browsers. However depending on your use case you may want to override these options:

```typescript
export type ParseEmailOptions = {
// Allow RFC 5322 angle address such as '"Name" <email@domain>'
// use this option if you want to parse emails from headers or envelope addresses
allowAngle?: boolean,
// Allow RFC 5322 quoted email address such as '"this+is+my+personal+email+address@me.invalid"@gmail.com'
// use this option if you want to accept lesser known email address formats
allowQuoted?: boolean,
// Reject addresses containing "+", which is used for subaddressing
// use this option to enforce one email per user
rejectSubaddressing?: boolean,
};
```

##

[Default options can be found here](https://github.com/mfbx9da4/deep-email-validator/blob/8bbd9597a7ce435f0a77889a45daccdd5d7c3488/src/options/options.ts#L1)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@
"ts-node": "^10.4.0",
"typescript": "^3.8.3"
}
}
}
24 changes: 16 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isEmail } from './regex/regex'
import { isEmail, parseEmail } from './regex/regex'
import { checkTypo } from './typo/typo'
import { getBestMx } from './dns/dns'
import { checkSMTP } from './smtp/smtp'
Expand All @@ -9,20 +9,28 @@ import './types'

export async function validate(emailOrOptions: string | ValidatorOptions): Promise<OutputFormat> {
const options = getOptions(emailOrOptions)
const email = options.email

if (options.validateRegex) {
const regexResponse = isEmail(email)
if (regexResponse) return createOutput('regex', regexResponse)
const emailRaw = options.email

const regexResponse = parseEmail(emailRaw, {
allowQuoted: options.allowQuoted,
allowAngle: options.allowAngle,
rejectSubaddressing: options.rejectSubaddressing,
})
if (options.validateRegex && 'error' in regexResponse) return createOutput('regex', regexResponse.error)
// fallback to the naive domain extraction if the user specifically opted out of format validation
const domain = 'domain' in regexResponse ? regexResponse.domain : emailRaw.split('@')[1]
const email = 'effectiveAddr' in regexResponse ? regexResponse.effectiveAddr : emailRaw.trim()

// prevent SMTP injection
if (email.indexOf('\r') !== -1 || email.indexOf('\n') !== -1) {
return createOutput('sanitization', 'Email cannot contain newlines')
}

if (options.validateTypo) {
const typoResponse = await checkTypo(email, options.additionalTopLevelDomains)
if (typoResponse) return createOutput('typo', typoResponse)
}

const domain = email.split('@')[1]

if (options.validateDisposable) {
const disposableResponse = await checkDisposable(domain)
if (disposableResponse) return createOutput('disposable', disposableResponse)
Expand Down
4 changes: 3 additions & 1 deletion src/options/options.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ParseEmailOptions } from '../regex/regex'

const defaultOptions: ValidatorOptionsFinal = {
email: 'name@example.org',
sender: 'name@example.org',
Expand All @@ -15,7 +17,7 @@ type Options = {
validateTypo: boolean
validateDisposable: boolean
validateSMTP: boolean
}
} & ParseEmailOptions

type MailCheckOptions = {
additionalTopLevelDomains?: string[]
Expand Down
2 changes: 1 addition & 1 deletion src/output/output.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ElementType } from '../types'

const OrderedLevels = ['regex', 'typo', 'disposable', 'mx', 'smtp'] as const
const OrderedLevels = ['sanitization', 'regex', 'typo', 'disposable', 'mx', 'smtp'] as const

export type SubOutputFormat = {
valid: boolean
Expand Down
91 changes: 80 additions & 11 deletions src/regex/regex.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,84 @@
export const isEmail = (email: string): string | undefined => {
email = (email || '').trim()
if (email.length === 0) {
return 'Email not provided'
// The RFC 5322 3.4.1 quoted flavor of email addresses which accepts more characters
const emailRegexAddrSpecRFC5322Quoted =
/^"([\x21\x23-\x5B\x5D-\x7E]+)"@([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])?)*)$/

// The WhatWG standard for email addresses, this is usually what you want for web forms.
// https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
const emailRegexAddrSpecWhatWG =
/^([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])?)*)$/

export type ParseEmailOptions = {
// Allow RFC 5322 angle address such as '"Name" <email@domain>'
// use this option if you want to parse emails from headers or envelope addresses
allowAngle?: boolean
// Allow RFC 5322 quoted email address such as '"this+is+my+personal+email+address@me.invalid"@gmail.com'
// use this option if you want to accept lesser known email address formats
allowQuoted?: boolean
// Reject addresses containing "+", which is used for subaddressing
// use this option to enforce one email per user
rejectSubaddressing?: boolean
}

export const parseEmail = (
email: string,
options: ParseEmailOptions = {}
): { local: string; domain: string; effectiveAddr: string } | { error: string } => {
email = email.trim()

if (email.endsWith('>')) {
if (!options.allowAngle) {
return { error: 'Angle address is not allowed' }
}

const match = email.match(new RegExp('^[^<]*<([^>]+)>$'))
if (!match) {
return { error: 'Invalid angle address' }
}

email = match[1]
}

if (email.indexOf('@') === -1) {
return { error: 'Email does not contain "@".' }
}
const split = email.split('@')
if (split.length < 2) {
return 'Email does not contain "@".'
} else {
const [domain] = split.slice(-1)
if (domain.indexOf('.') === -1) {
return 'Must contain a "." after the "@".'

if (email.startsWith('"')) {
if (!options.allowQuoted) {
return { error: 'Quoted email addresses are not allowed' }
}
const match = email.match(emailRegexAddrSpecRFC5322Quoted)
if (!match) {
return { error: 'Invalid quoted email address' }
}
const [, local, domain] = match

if (options.rejectSubaddressing && local.includes('+')) {
return { error: 'Subaddressing is not allowed' }
}

return { local, domain, effectiveAddr: `"${local}"@${domain}` }
}

const match = email.match(emailRegexAddrSpecWhatWG)
if (!match) {
return { error: 'Invalid email address' }
}

const [, local, domain] = match

if (options.rejectSubaddressing && local.includes('+')) {
return { error: 'Subaddressing is not allowed' }
}

return { local, domain, effectiveAddr: `${local}@${domain}` }
}

// Left for backwards compatibility
export const isEmail = (email: string): string | undefined => {
const response = parseEmail(email, { allowQuoted: true, allowAngle: true })
if ('error' in response) {
return response.error
}

return undefined
}
Loading