valid-email-checker 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/package.json +33 -0
- package/src/data/disposableDomains.js +1194 -0
- package/src/index.d.ts +156 -0
- package/src/index.js +335 -0
- package/src/validators/formatValidator.js +257 -0
- package/src/validators/mxValidator.js +143 -0
- package/src/validators/spamDetector.js +338 -0
- package/src/validators/tempMailDetector.js +205 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email Format Validator
|
|
3
|
+
* Validates email addresses according to RFC 5322 standards
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// RFC 5322 compliant email regex (simplified but comprehensive)
|
|
7
|
+
const EMAIL_REGEX = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i;
|
|
8
|
+
|
|
9
|
+
// Simple but effective email regex for common use cases
|
|
10
|
+
const SIMPLE_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
11
|
+
|
|
12
|
+
// Valid TLD list (common ones)
|
|
13
|
+
const VALID_TLDS = new Set([
|
|
14
|
+
'com', 'org', 'net', 'edu', 'gov', 'mil', 'int',
|
|
15
|
+
'co', 'io', 'ai', 'app', 'dev', 'tech', 'online', 'site', 'web',
|
|
16
|
+
'info', 'biz', 'name', 'pro', 'museum', 'coop', 'aero',
|
|
17
|
+
'uk', 'us', 'ca', 'au', 'de', 'fr', 'jp', 'cn', 'in', 'br', 'ru',
|
|
18
|
+
'es', 'it', 'nl', 'se', 'no', 'fi', 'dk', 'pl', 'cz', 'at', 'ch',
|
|
19
|
+
'be', 'ie', 'nz', 'sg', 'hk', 'kr', 'tw', 'th', 'my', 'ph', 'id',
|
|
20
|
+
'vn', 'za', 'eg', 'ng', 'ke', 'mx', 'ar', 'cl', 'co', 'pe', 've',
|
|
21
|
+
'xyz', 'club', 'shop', 'blog', 'news', 'live', 'store', 'email',
|
|
22
|
+
'me', 'tv', 'cc', 'ws', 'bz', 'fm', 'am', 'ly', 'to', 'gg', 'gl'
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validates email format
|
|
27
|
+
* @param {string} email - Email address to validate
|
|
28
|
+
* @param {object} options - Validation options
|
|
29
|
+
* @returns {object} Validation result
|
|
30
|
+
*/
|
|
31
|
+
function validateEmail(email, options = {}) {
|
|
32
|
+
const {
|
|
33
|
+
strict = false,
|
|
34
|
+
allowPlusAddressing = true,
|
|
35
|
+
checkTld = true,
|
|
36
|
+
maxLength = 254,
|
|
37
|
+
minLocalLength = 1,
|
|
38
|
+
maxLocalLength = 64
|
|
39
|
+
} = options;
|
|
40
|
+
|
|
41
|
+
const result = {
|
|
42
|
+
valid: false,
|
|
43
|
+
email: email,
|
|
44
|
+
normalized: null,
|
|
45
|
+
local: null,
|
|
46
|
+
domain: null,
|
|
47
|
+
errors: []
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Basic checks
|
|
51
|
+
if (!email || typeof email !== 'string') {
|
|
52
|
+
result.errors.push('Email is required and must be a string');
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Trim and normalize
|
|
57
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
58
|
+
result.normalized = normalizedEmail;
|
|
59
|
+
|
|
60
|
+
// Length checks
|
|
61
|
+
if (normalizedEmail.length > maxLength) {
|
|
62
|
+
result.errors.push(`Email exceeds maximum length of ${maxLength} characters`);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (normalizedEmail.length === 0) {
|
|
67
|
+
result.errors.push('Email cannot be empty');
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Split into local and domain parts
|
|
72
|
+
const atIndex = normalizedEmail.lastIndexOf('@');
|
|
73
|
+
if (atIndex === -1) {
|
|
74
|
+
result.errors.push('Email must contain @ symbol');
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const local = normalizedEmail.substring(0, atIndex);
|
|
79
|
+
const domain = normalizedEmail.substring(atIndex + 1);
|
|
80
|
+
|
|
81
|
+
result.local = local;
|
|
82
|
+
result.domain = domain;
|
|
83
|
+
|
|
84
|
+
// Local part validation
|
|
85
|
+
if (local.length < minLocalLength) {
|
|
86
|
+
result.errors.push(`Local part must be at least ${minLocalLength} character(s)`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (local.length > maxLocalLength) {
|
|
90
|
+
result.errors.push(`Local part exceeds maximum length of ${maxLocalLength} characters`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check plus addressing
|
|
94
|
+
if (!allowPlusAddressing && local.includes('+')) {
|
|
95
|
+
result.errors.push('Plus addressing is not allowed');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check for consecutive dots in local part
|
|
99
|
+
if (local.includes('..')) {
|
|
100
|
+
result.errors.push('Local part cannot contain consecutive dots');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check if local starts or ends with dot
|
|
104
|
+
if (local.startsWith('.') || local.endsWith('.')) {
|
|
105
|
+
result.errors.push('Local part cannot start or end with a dot');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Domain validation
|
|
109
|
+
if (!domain || domain.length === 0) {
|
|
110
|
+
result.errors.push('Domain is required');
|
|
111
|
+
} else {
|
|
112
|
+
// Check for valid domain format
|
|
113
|
+
if (domain.includes('..')) {
|
|
114
|
+
result.errors.push('Domain cannot contain consecutive dots');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (domain.startsWith('.') || domain.endsWith('.')) {
|
|
118
|
+
result.errors.push('Domain cannot start or end with a dot');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (domain.startsWith('-') || domain.endsWith('-')) {
|
|
122
|
+
result.errors.push('Domain cannot start or end with a hyphen');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Extract TLD
|
|
126
|
+
const domainParts = domain.split('.');
|
|
127
|
+
if (domainParts.length < 2) {
|
|
128
|
+
result.errors.push('Domain must have at least two parts');
|
|
129
|
+
} else {
|
|
130
|
+
const tld = domainParts[domainParts.length - 1];
|
|
131
|
+
result.tld = tld;
|
|
132
|
+
|
|
133
|
+
if (checkTld && tld.length < 2) {
|
|
134
|
+
result.errors.push('TLD must be at least 2 characters');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if TLD contains only letters
|
|
138
|
+
if (!/^[a-z]+$/i.test(tld)) {
|
|
139
|
+
result.errors.push('TLD must contain only letters');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Regex validation
|
|
145
|
+
const regex = strict ? EMAIL_REGEX : SIMPLE_EMAIL_REGEX;
|
|
146
|
+
if (!regex.test(normalizedEmail)) {
|
|
147
|
+
result.errors.push('Email format is invalid');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Set valid flag
|
|
151
|
+
result.valid = result.errors.length === 0;
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Quick validation - returns boolean only
|
|
158
|
+
* @param {string} email - Email address to validate
|
|
159
|
+
* @returns {boolean} True if valid
|
|
160
|
+
*/
|
|
161
|
+
function isValidEmail(email) {
|
|
162
|
+
return validateEmail(email).valid;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Normalize email address
|
|
167
|
+
* @param {string} email - Email to normalize
|
|
168
|
+
* @param {object} options - Normalization options
|
|
169
|
+
* @returns {string} Normalized email
|
|
170
|
+
*/
|
|
171
|
+
function normalizeEmail(email, options = {}) {
|
|
172
|
+
const {
|
|
173
|
+
lowercase = true,
|
|
174
|
+
removePlusAddressing = false,
|
|
175
|
+
removeDotsFromGmail = false
|
|
176
|
+
} = options;
|
|
177
|
+
|
|
178
|
+
if (!email || typeof email !== 'string') {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let normalized = email.trim();
|
|
183
|
+
|
|
184
|
+
if (lowercase) {
|
|
185
|
+
normalized = normalized.toLowerCase();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const atIndex = normalized.lastIndexOf('@');
|
|
189
|
+
if (atIndex === -1) {
|
|
190
|
+
return normalized;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let local = normalized.substring(0, atIndex);
|
|
194
|
+
const domain = normalized.substring(atIndex + 1);
|
|
195
|
+
|
|
196
|
+
// Remove plus addressing
|
|
197
|
+
if (removePlusAddressing) {
|
|
198
|
+
const plusIndex = local.indexOf('+');
|
|
199
|
+
if (plusIndex !== -1) {
|
|
200
|
+
local = local.substring(0, plusIndex);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Remove dots from Gmail local part (Gmail ignores dots)
|
|
205
|
+
if (removeDotsFromGmail && (domain === 'gmail.com' || domain === 'googlemail.com')) {
|
|
206
|
+
local = local.replace(/\./g, '');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return `${local}@${domain}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Extract domain from email
|
|
214
|
+
* @param {string} email - Email address
|
|
215
|
+
* @returns {string|null} Domain or null
|
|
216
|
+
*/
|
|
217
|
+
function extractDomain(email) {
|
|
218
|
+
if (!email || typeof email !== 'string') {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const atIndex = email.lastIndexOf('@');
|
|
223
|
+
if (atIndex === -1) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return email.substring(atIndex + 1).toLowerCase();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Extract local part from email
|
|
232
|
+
* @param {string} email - Email address
|
|
233
|
+
* @returns {string|null} Local part or null
|
|
234
|
+
*/
|
|
235
|
+
function extractLocal(email) {
|
|
236
|
+
if (!email || typeof email !== 'string') {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const atIndex = email.lastIndexOf('@');
|
|
241
|
+
if (atIndex === -1) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return email.substring(0, atIndex).toLowerCase();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = {
|
|
249
|
+
validateEmail,
|
|
250
|
+
isValidEmail,
|
|
251
|
+
normalizeEmail,
|
|
252
|
+
extractDomain,
|
|
253
|
+
extractLocal,
|
|
254
|
+
EMAIL_REGEX,
|
|
255
|
+
SIMPLE_EMAIL_REGEX,
|
|
256
|
+
VALID_TLDS
|
|
257
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MX Record Validator
|
|
3
|
+
* Validates email domains using DNS MX record lookups
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const dns = require('dns');
|
|
7
|
+
const { promisify } = require('util');
|
|
8
|
+
|
|
9
|
+
const resolveMx = promisify(dns.resolveMx);
|
|
10
|
+
const resolve = promisify(dns.resolve);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if domain has valid MX records
|
|
14
|
+
* @param {string} domain - Domain to check
|
|
15
|
+
* @param {object} options - Options
|
|
16
|
+
* @returns {Promise<object>} Result with MX records info
|
|
17
|
+
*/
|
|
18
|
+
async function checkMxRecords(domain, options = {}) {
|
|
19
|
+
const { timeout = 5000 } = options;
|
|
20
|
+
|
|
21
|
+
const result = {
|
|
22
|
+
hasMxRecords: false,
|
|
23
|
+
domain: domain,
|
|
24
|
+
mxRecords: [],
|
|
25
|
+
error: null
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (!domain || typeof domain !== 'string') {
|
|
29
|
+
result.error = 'Invalid domain provided';
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Create a promise that rejects after timeout
|
|
35
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
36
|
+
setTimeout(() => reject(new Error('DNS lookup timeout')), timeout);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Race between MX lookup and timeout
|
|
40
|
+
const mxRecords = await Promise.race([
|
|
41
|
+
resolveMx(domain),
|
|
42
|
+
timeoutPromise
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
if (mxRecords && mxRecords.length > 0) {
|
|
46
|
+
result.hasMxRecords = true;
|
|
47
|
+
result.mxRecords = mxRecords
|
|
48
|
+
.sort((a, b) => a.priority - b.priority)
|
|
49
|
+
.map(record => ({
|
|
50
|
+
exchange: record.exchange,
|
|
51
|
+
priority: record.priority
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// MX lookup failed, try A record as fallback
|
|
56
|
+
try {
|
|
57
|
+
const aRecords = await Promise.race([
|
|
58
|
+
resolve(domain, 'A'),
|
|
59
|
+
new Promise((_, reject) => {
|
|
60
|
+
setTimeout(() => reject(new Error('DNS lookup timeout')), timeout);
|
|
61
|
+
})
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
if (aRecords && aRecords.length > 0) {
|
|
65
|
+
result.hasMxRecords = true;
|
|
66
|
+
result.mxRecords = [{ exchange: domain, priority: 0, type: 'A' }];
|
|
67
|
+
result.fallbackToA = true;
|
|
68
|
+
}
|
|
69
|
+
} catch (aError) {
|
|
70
|
+
result.error = `DNS lookup failed: ${error.code || error.message}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate email with MX record check
|
|
79
|
+
* @param {string} email - Email to validate
|
|
80
|
+
* @param {object} options - Options
|
|
81
|
+
* @returns {Promise<object>} Validation result
|
|
82
|
+
*/
|
|
83
|
+
async function validateEmailMx(email, options = {}) {
|
|
84
|
+
const { extractDomain } = require('./formatValidator');
|
|
85
|
+
|
|
86
|
+
const result = {
|
|
87
|
+
valid: false,
|
|
88
|
+
email: email,
|
|
89
|
+
domain: null,
|
|
90
|
+
hasMxRecords: false,
|
|
91
|
+
mxRecords: [],
|
|
92
|
+
error: null
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const domain = extractDomain(email);
|
|
96
|
+
if (!domain) {
|
|
97
|
+
result.error = 'Could not extract domain from email';
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
result.domain = domain;
|
|
102
|
+
|
|
103
|
+
const mxResult = await checkMxRecords(domain, options);
|
|
104
|
+
|
|
105
|
+
result.hasMxRecords = mxResult.hasMxRecords;
|
|
106
|
+
result.mxRecords = mxResult.mxRecords;
|
|
107
|
+
result.error = mxResult.error;
|
|
108
|
+
result.valid = mxResult.hasMxRecords;
|
|
109
|
+
result.fallbackToA = mxResult.fallbackToA;
|
|
110
|
+
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Batch check MX records for multiple domains
|
|
116
|
+
* @param {string[]} domains - Domains to check
|
|
117
|
+
* @param {object} options - Options
|
|
118
|
+
* @returns {Promise<object>} Results mapped by domain
|
|
119
|
+
*/
|
|
120
|
+
async function batchCheckMxRecords(domains, options = {}) {
|
|
121
|
+
const { concurrency = 5 } = options;
|
|
122
|
+
const results = {};
|
|
123
|
+
|
|
124
|
+
// Process in batches to avoid overwhelming DNS
|
|
125
|
+
for (let i = 0; i < domains.length; i += concurrency) {
|
|
126
|
+
const batch = domains.slice(i, i + concurrency);
|
|
127
|
+
const batchResults = await Promise.all(
|
|
128
|
+
batch.map(domain => checkMxRecords(domain, options))
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
batch.forEach((domain, index) => {
|
|
132
|
+
results[domain] = batchResults[index];
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
checkMxRecords,
|
|
141
|
+
validateEmailMx,
|
|
142
|
+
batchCheckMxRecords
|
|
143
|
+
};
|