Skip to content

Commit da818fa

Browse files
committed
feat(pkg:string): added API String.isEmailAddress
1 parent fca89bc commit da818fa

File tree

4 files changed

+183
-0
lines changed

4 files changed

+183
-0
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- feat(pkg:async): added API `Async.autoRetry`, with exponential backoff and jitter supports.
1212
- fix(pkg:network): export API `Network.isValidIPv4Address`
1313
- feat(pkg:network): add API `Network.isValidMacAddress`
14+
- feat(pkg:string): add API `String.isEmailAddress`
1415

1516
## v1.2.1
1617

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import * as NodeTest from 'node:test';
2+
import * as NodeAssert from 'node:assert';
3+
import { isEmailAddress } from './IsEmailAddress';
4+
5+
NodeTest.describe('Function String.isEmailAddress', () => {
6+
7+
NodeTest.it('Should return true for valid email addresses', () => {
8+
9+
for (const email of [
10+
11+
12+
13+
14+
]) {
15+
16+
NodeAssert.strictEqual(isEmailAddress(email), true, `Email "${email}" should be valid.`);
17+
}
18+
});
19+
20+
NodeTest.it('Specific maximum length of email addresses should work', () => {
21+
22+
NodeAssert.strictEqual(
23+
isEmailAddress('[email protected]', { maxLength: 20, domains: [], }),
24+
true,
25+
);
26+
27+
NodeAssert.strictEqual(
28+
isEmailAddress('[email protected]', { maxLength: 10 }),
29+
false,
30+
);
31+
32+
NodeAssert.strictEqual(
33+
isEmailAddress('i'.repeat(244) + '@example.org', { maxLength: 256 }),
34+
false,
35+
'The maxLength should not exceed the hard limit of the maximum length 255.',
36+
);
37+
});
38+
39+
NodeTest.it('Specific allowed list of domains should work', () => {
40+
41+
NodeAssert.strictEqual(
42+
isEmailAddress('[email protected]', { domains: [
43+
'example.org',
44+
'example.io',
45+
] }),
46+
true,
47+
);
48+
49+
NodeAssert.strictEqual(
50+
isEmailAddress('[email protected]', { domains: [
51+
'example.org',
52+
'example.io',
53+
] }),
54+
true,
55+
);
56+
57+
NodeAssert.strictEqual(
58+
isEmailAddress('[email protected]', { domains: [
59+
'example.org',
60+
'example.IO',
61+
] }),
62+
false,
63+
'The domain will not be treated as case-insensitive.',
64+
);
65+
66+
NodeAssert.strictEqual(
67+
isEmailAddress('[email protected]', { domains: [
68+
'example.org',
69+
'example.io',
70+
] }),
71+
true,
72+
'The domain and email address should be case-insensitive.',
73+
);
74+
75+
NodeAssert.strictEqual(
76+
isEmailAddress('[email protected]', { domains: ['example.com'] }),
77+
false,
78+
);
79+
80+
NodeAssert.strictEqual(
81+
isEmailAddress('[email protected]', { domains: ['example.com'] }),
82+
false,
83+
'The domain and email address should be case-insensitive.',
84+
);
85+
});
86+
87+
NodeTest.it('Should return false for invalid email addresses', () => {
88+
89+
for (const email of [
90+
91+
92+
93+
'',
94+
]) {
95+
96+
NodeAssert.strictEqual(isEmailAddress(email), false, `Email "${email}" should be invalid.`);
97+
}
98+
});
99+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Copyright 2025 Angus.Fenying <[email protected]>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// eslint-disable-next-line max-len
18+
const EMAIL_REGEXP = /^[-a-z0-9_+][-a-z0-9+_.]{0,62}@[a-z0-9][-a-z0-9]{0,62}(\.[a-z0-9][-a-z0-9]{0,62})*\.[a-z]{2,26}$/i;
19+
20+
const MAX_EMAIL_LENGTH = 255;
21+
const MIN_EMAIL_LENGTH = 6; // 1 char before '@' + 1 char after '@' + 4 chars for domain
22+
23+
/**
24+
* The extra options for email validation.
25+
*/
26+
export interface IEmailValidationOptions {
27+
28+
/**
29+
* The expected maximum length of the email address.
30+
*
31+
* Useful when some systems have a hard limit on the length of email
32+
* addresses.
33+
*
34+
* If this value is larger than `MAX_EMAIL_LENGTH`, it will be ignored and
35+
* `MAX_EMAIL_LENGTH` will be used instead.
36+
*
37+
* @default MAX_EMAIL_LENGTH
38+
*/
39+
maxLength?: number;
40+
41+
/**
42+
* The allowed list of domains.
43+
*
44+
* If omitted or an empty array is provided, all domains are allowed.
45+
*
46+
* > NOTICE: The email address are compared in lower-case, but only
47+
* > lower-case domains are supported. The domains containing upper-case
48+
* > characters will not match.
49+
*
50+
* @default []
51+
*/
52+
domains?: string[];
53+
}
54+
55+
/**
56+
* Test if a string is a valid email address.
57+
*
58+
* @param email The string to test.
59+
* @param opts The extra options for email validation.
60+
*
61+
* @returns `true` if the string is a valid email address, `false` otherwise.
62+
*/
63+
export function isEmailAddress(email: string, opts?: IEmailValidationOptions): boolean {
64+
65+
const formatOk = email.length >= MIN_EMAIL_LENGTH &&
66+
email.length <= Math.min(opts?.maxLength ?? MAX_EMAIL_LENGTH, MAX_EMAIL_LENGTH) &&
67+
!email.includes('..') &&
68+
!email.includes('.@') &&
69+
EMAIL_REGEXP.test(email);
70+
71+
if (!formatOk) {
72+
73+
return false;
74+
}
75+
76+
if (opts?.domains?.length) {
77+
78+
return opts.domains.includes(email.split('@')[1].toLowerCase());
79+
}
80+
81+
return true;
82+
}

packages/partials/string/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export * from './Functions/EvilSpace';
1919
export * from './Functions/Random';
2020
export * from './Functions/RegexpEscape';
2121
export * from './Functions/Html';
22+
export * from './Functions/IsEmailAddress';
2223
export * from './Functions/ToChunks';
2324
export * from './Functions/NameCase';
2425
export * from './Classes/UnitParser';

0 commit comments

Comments
 (0)