Skip to content
Merged
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
14 changes: 14 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ export interface Options {
@default false
*/
readonly exact?: boolean;

/**
Allow emails with a domain that doesn't have a dot, such as `user@localhost` or `user@internal`.

@default true
*/
readonly allowSingleLabelDomain?: boolean;

/**
Allow the ampersand HTML entity `&` to correspond to an ampersand `&` in the email address.

@default false
*/
readonly allowAmpersandEntity?: boolean;
}

/**
Expand Down
45 changes: 42 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
const regex = '[^\\.\\s@:](?:[^\\s@:]*[^\\s@:\\.])?@[^\\.\\s@]+(?:\\.[^\\.\\s@]+)*';
export default function emailRegex(options) {
options = {
exact: false,
allowSingleLabelDomain: true,
allowAmpersandEntity: false,
...options
};

export default function emailRegex({exact} = {}) {
return exact ? new RegExp(`^${regex}$`) : new RegExp(regex, 'g');
// RFC 5322 (https://datatracker.ietf.org/doc/html/rfc5322)
const alpha = '[A-Za-z]';
const digit = String.raw`\d`;
const atext = String.raw`(?:${alpha}|${digit}|[!#$%&'*+\-/=?^_\`{|}~]${options.allowAmpersandEntity ? '|&' : ''})`;
const dotAtomText = String.raw`(?:${atext}+(?:\.${atext}+)+)`;
const dotAtom = `${dotAtomText}`;
const dquote = '"';
const sp = ' ';
const htab = String.raw`\u0009`;
const wsp = `(?:${sp}|${htab})`;
const cr = String.raw`\u000D`;
const lf = String.raw`\u000A`;
const crlf = `(?:${cr}${lf})`;
const obsFws = `(?:${wsp}+(?:${crlf}${wsp}+)*)`;
const fws = `(?:(?:(?:${wsp}*${crlf})?${wsp}+)|${obsFws})`;
const obsNoWsCtl = String.raw`(?:[\u0001-\u0008]|\u000B|\u000C|[\u000E-\u001F]|\u007F)`;
const obsQtext = `${obsNoWsCtl}`;
const qtext = String.raw`(?:!|[\u0023-\u005B]|[\u005D-\u007E]|${obsQtext})`;
const vchar = String.raw`[\u0021-\u007E]`;
const obsQp = String.raw`(?:\\(?:\u0000|${obsNoWsCtl}|${lf}|${cr}))`;
const quotedPair = String.raw`(?:(?:\\(?:${vchar}|${wsp}))|${obsQp})`;
const qcontent = `(?:${qtext}|${quotedPair})`;
const quotedString = `(?:${dquote}(?:${fws}?${qcontent})*${fws}?${dquote})`;
const atom = `${atext}+`;
const word = `(?:${atom}|${quotedString})`;
const obsLocalPart = String.raw`(?:${word}(?:\.${word})*)`;
const localPart = `(?:${dotAtom}|${quotedString}|${obsLocalPart})`;
const obsDtext = `(?:${obsNoWsCtl}|${quotedPair})`;
const dtext = String.raw`(?:[\u0021-\u005A]|[\u005E-\u007E]|${obsDtext})`;
const domainLiteral = String.raw`(?:\[(?:${fws}?${dtext})*${fws}?])`;
const obsDomain = String.raw`(?:${atom}(?:\.${atom})${options.allowSingleLabelDomain ? '*' : '+'})`;
const domain = `(?:${dotAtom}|${domainLiteral}|${obsDomain})`;
const addrSpec = `${localPart}@${domain}`;

return options.exact ? new RegExp(`^${addrSpec}$`) : new RegExp(addrSpec, 'g');
}
1 change: 1 addition & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ import emailRegex from './index.js';
expectType<RegExp>(emailRegex());
expectType<RegExp>(emailRegex({}));
expectType<RegExp>(emailRegex({exact: true}));
expectType<RegExp>(emailRegex({exact: true, allowAmpersandEntity: true, allowSingleLabelDomain: true}));
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
],
"devDependencies": {
"ava": "^3.15.0",
"tsd": "^0.14.0",
"tsd": "^0.21.0",
"typescript": "^4.9.5",
"xo": "^0.39.1"
}
}
14 changes: 14 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ Only match an exact string.

Useful with `RegExp#test` to check if a string is an email address.

##### allowSingleLabelDomain

Type: `boolean`\
Default: `true`

Allow emails with a domain that doesn't have a dot, such as `user@localhost` or `user@internal`.

##### allowAmpersandEntity

Type: `boolean`\
Default: `false`

Allow the ampersand HTML entity `&amp;` to correspond to an ampersand `&` in the email address.

## Important

If you run the regex against untrusted user input in a server context, you should [give it a timeout](https://github.com/sindresorhus/super-regex). It's also a good idea to limit the input to a reasonable length.
Expand Down
99 changes: 90 additions & 9 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,61 @@ const fixtures = [
'[email protected]',
'[email protected]',
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklm@sindresorhus.com',
'!#$%&amp;`*+/=?^`{|}[email protected]',
'[email protected]',
'a@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghikl.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefg.hij',
'[email protected]',
'"\\a"@sindresorhus.com',
String.raw`"\a"@sindresorhus.com`,
'""@sindresorhus.com',
'"test"@sindresorhus.com',
'"\\""@sindresorhus.com',
String.raw`"\""@sindresorhus.com`,
'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiklmn@sindresorhus.com',
'[email protected]',
'a@a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v',
'[email protected]',
'[email protected]',
'foo@[IPv6:2001:db8::2]'
'foo@[IPv6:2001:db8::2]',
// https://github.com/sindresorhus/email-regex/issues/2#issuecomment-404654677
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'email@[123.123.123.123]',
'"email"@example.com',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
// 'very.unusual.”@”.unusual.com',
'email@example',
'[email protected]',
'[email protected]',
'[email protected]'
];

const fixturesCustomMatch = new Map([
// https://github.com/sindresorhus/email-regex/issues/9#issue-569014279
['f="nr@context",c=e("gos")', 'nr@context'],
// https://github.com/sindresorhus/email-regex/issues/2#issuecomment-404654677
[String.raw`very.”(),:;<>[]”.VERY.”very@\ "very”[email protected]`, '[email protected]'],
['#@%^%#$@#$@#.com', '#@%^%#$'],
['Joe Smith [email protected]', '[email protected]'],
['email@[email protected]', 'email@example'],
['[email protected]', '[email protected]'],
['[email protected]', '[email protected]'],
['[email protected] (Joe Smith)', '[email protected]'],
['just”not”[email protected]', '[email protected]'],
[String.raw`this\ is"really"not\[email protected]`, '[email protected]']
]);

for (const [input, expected] of fixturesCustomMatch) {
// If they match, we can't use them as notFixtures
console.assert(input !== expected, `Custom match fixture "${input}" does not match expected "${expected}"`);
}

const fixturesNot = [
'@',
'@io',
Expand All @@ -39,25 +78,67 @@ const fixturesNot = [
'sindre@[email protected]',
'mailto:[email protected]',
'foo.example.com',
'[email protected]'
'[email protected]',
'!#$%&amp;`*+/=?^`{|}[email protected]',
// https://github.com/sindresorhus/email-regex/issues/9#issue-569014279
'f="nr@context",c=e("gos")',
// https://github.com/sindresorhus/email-regex/issues/2#issuecomment-404654677
'plainaddress',
'@example.com',
'email.example.com',
'[email protected]',
'[email protected]',
'あいうえお@example.com',
'[email protected]',
'”(),:;<>[]@example.com',
'"(),:;<>[]@example.com',
String.raw`much.”more\ unusual”@example.com`
];

function getFirstMatch(regex, text) {
const matches = regex.exec(text);

if (matches) {
return matches[0];
}
}

test('extract', t => {
for (const fixture of fixtures) {
t.is((emailRegex().exec(`foo ${fixture} bar`) || [])[0], fixture);
t.is(getFirstMatch(emailRegex(), `foo ${fixture} bar`), fixture);
}

t.is(emailRegex().exec('mailto:[email protected]')[0], '[email protected]');
for (const [input, expected] of fixturesCustomMatch) {
t.is(getFirstMatch(emailRegex(), input), expected, input); // eslint-disable-line ava/assertion-arguments
}

t.is(getFirstMatch(emailRegex(), 'mailto:[email protected]'), '[email protected]');
});

test('exact', t => {
for (const fixture of fixtures) {
t.true(emailRegex({exact: true}).test(fixture));
t.true(emailRegex({exact: true}).test(fixture), fixture); // eslint-disable-line ava/assertion-arguments
}
});

test('allowSingleLabelDomain', t => {
t.true(emailRegex({exact: true, allowSingleLabelDomain: true}).test('abc@sindresorhus'));
t.false(emailRegex({exact: true, allowSingleLabelDomain: false}).test('abc@sindresorhus'));
t.is(getFirstMatch(emailRegex({exact: false, allowSingleLabelDomain: true}), '#@%^%#$@#$@#.com'), '#@%^%#$');
t.is(getFirstMatch(emailRegex({exact: false, allowSingleLabelDomain: false}), '#@%^%#$@#$@#.com'), '#$@#.com');
});

test('allowAmpersandEntity', t => {
t.true(emailRegex({exact: true, allowAmpersandEntity: true}).test('!#$%&amp;`*+/=?^`{|}[email protected]'));
t.false(emailRegex({exact: true, allowAmpersandEntity: false}).test('!#$%&amp;`*+/=?^`{|}[email protected]'));
});

test('failures', t => {
for (const fixture of fixturesNot) {
t.false(emailRegex({exact: true}).test(fixture));
t.false(emailRegex({exact: true}).test(fixture), fixture); // eslint-disable-line ava/assertion-arguments
}

for (const input of fixturesCustomMatch.keys()) {
t.false(emailRegex({exact: true}).test(input), input); // eslint-disable-line ava/assertion-arguments
}
});