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,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spam Email Detector
|
|
3
|
+
* Detects spam patterns in email addresses
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { extractLocal, extractDomain } = require('./formatValidator');
|
|
7
|
+
|
|
8
|
+
// Spam patterns in local part of email
|
|
9
|
+
const SPAM_LOCAL_PATTERNS = [
|
|
10
|
+
// Adult content patterns
|
|
11
|
+
/^(xxx|sex|porn|adult|nude|naked|hot|sexy)/i,
|
|
12
|
+
/(xxx|sex|porn|adult|nude|naked)$/i,
|
|
13
|
+
|
|
14
|
+
// Scam patterns
|
|
15
|
+
/^(lottery|winner|prize|cash|money|profit|income)/i,
|
|
16
|
+
/(lottery|winner|prize|cash|money|profit)$/i,
|
|
17
|
+
/^(free|bonus|discount|deal|offer|promo)/i,
|
|
18
|
+
|
|
19
|
+
// Suspicious number patterns
|
|
20
|
+
/^\d{8,}$/, // Only numbers, 8+ digits
|
|
21
|
+
/^[a-z]\d{6,}$/i, // Letter followed by many numbers
|
|
22
|
+
/^\d{3,}[a-z]{1,2}\d{3,}$/i, // Numbers-letters-numbers pattern
|
|
23
|
+
|
|
24
|
+
// Random looking patterns
|
|
25
|
+
/^[a-z]{1,2}\d{4,}[a-z]{1,2}$/i, // a1234b pattern
|
|
26
|
+
/^[bcdfghjklmnpqrstvwxyz]{5,}$/i, // Only consonants
|
|
27
|
+
|
|
28
|
+
// Bot/automated patterns
|
|
29
|
+
/^(bot|robot|auto|system|noreply|no-reply|donotreply)/i,
|
|
30
|
+
/^(admin|administrator|support|help|info|contact)$/i,
|
|
31
|
+
|
|
32
|
+
// Test patterns
|
|
33
|
+
/^(test|testing|demo|sample|example|dummy)/i,
|
|
34
|
+
/^(asdf|qwerty|zxcv|aaa|bbb|abc123)/i,
|
|
35
|
+
|
|
36
|
+
// Suspicious keywords
|
|
37
|
+
/^(click|subscribe|unsubscribe|confirm|verify|validate)/i,
|
|
38
|
+
/(marketing|newsletter|promo|sales|advertising)/i,
|
|
39
|
+
|
|
40
|
+
// Impersonation patterns
|
|
41
|
+
/^(paypal|amazon|google|microsoft|apple|facebook|instagram|twitter)\d/i,
|
|
42
|
+
/^(support|help|service)[-_]?(paypal|amazon|google|microsoft)/i,
|
|
43
|
+
|
|
44
|
+
// Excessive special characters in local part
|
|
45
|
+
/[._-]{3,}/, // Three or more consecutive special chars
|
|
46
|
+
/^[._-]/, // Starting with special char
|
|
47
|
+
/[._-]$/, // Ending with special char
|
|
48
|
+
|
|
49
|
+
// Gibberish patterns
|
|
50
|
+
/([a-z])\1{4,}/i, // Same letter repeated 5+ times
|
|
51
|
+
/\d{10,}/, // 10+ consecutive digits
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Spam patterns in domain
|
|
55
|
+
const SPAM_DOMAIN_PATTERNS = [
|
|
56
|
+
// Suspicious TLD patterns
|
|
57
|
+
/\.(xyz|top|wang|win|bid|stream|download|racing|date|accountant|science|party|loan|trade|webcam|gdn|men|work|click)$/i,
|
|
58
|
+
|
|
59
|
+
// IP-like domains
|
|
60
|
+
/^\d{1,3}[-_.]\d{1,3}[-_.]\d{1,3}[-_.]\d{1,3}/,
|
|
61
|
+
|
|
62
|
+
// Very long domains
|
|
63
|
+
/^[a-z0-9-]{40,}\./i,
|
|
64
|
+
|
|
65
|
+
// Random-looking domains
|
|
66
|
+
/^[a-z]{15,}\.(com|net|org)$/i,
|
|
67
|
+
|
|
68
|
+
// Typosquatting patterns for popular services
|
|
69
|
+
/^(g00gle|googl3|go0gle|gooogle|googlee)/i,
|
|
70
|
+
/^(faceb00k|facebo0k|facebok|faceboook)/i,
|
|
71
|
+
/^(paypall|paypa1|payp4l|paypa|paypl)/i,
|
|
72
|
+
/^(amaz0n|amazn|amazzon|amazonn)/i,
|
|
73
|
+
/^(micr0soft|mircosoft|microsft|m1crosoft)/i,
|
|
74
|
+
/^(app1e|aple|applle|4pple)/i,
|
|
75
|
+
|
|
76
|
+
// Suspicious patterns
|
|
77
|
+
/-(secure|login|verify|account|update|confirm)\./i,
|
|
78
|
+
/\.(secure|login|verify)-/i,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// Known spam/phishing domain keywords
|
|
82
|
+
const SPAM_DOMAIN_KEYWORDS = [
|
|
83
|
+
'phishing',
|
|
84
|
+
'scam',
|
|
85
|
+
'malware',
|
|
86
|
+
'virus',
|
|
87
|
+
'hack',
|
|
88
|
+
'crack',
|
|
89
|
+
'warez',
|
|
90
|
+
'pirate',
|
|
91
|
+
'torrent',
|
|
92
|
+
'download-free',
|
|
93
|
+
'free-download',
|
|
94
|
+
'win-prize',
|
|
95
|
+
'get-rich',
|
|
96
|
+
'make-money',
|
|
97
|
+
'earn-cash',
|
|
98
|
+
'crypto-profit',
|
|
99
|
+
'bitcoin-profit',
|
|
100
|
+
'forex-profit',
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// Suspicious email characteristics scores
|
|
104
|
+
const SPAM_SCORES = {
|
|
105
|
+
suspiciousLocalPattern: 15,
|
|
106
|
+
suspiciousDomainPattern: 20,
|
|
107
|
+
suspiciousDomainKeyword: 25,
|
|
108
|
+
excessiveNumbers: 10,
|
|
109
|
+
excessiveSpecialChars: 10,
|
|
110
|
+
randomLookingLocal: 15,
|
|
111
|
+
suspiciousTld: 15,
|
|
112
|
+
typosquatting: 30,
|
|
113
|
+
veryLongLocal: 10,
|
|
114
|
+
veryLongDomain: 10,
|
|
115
|
+
impersonation: 25,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if email matches spam patterns
|
|
120
|
+
* @param {string} email - Email address to check
|
|
121
|
+
* @param {object} options - Options for checking
|
|
122
|
+
* @returns {object} Result with isSpam flag and details
|
|
123
|
+
*/
|
|
124
|
+
function isSpamEmail(email, options = {}) {
|
|
125
|
+
const {
|
|
126
|
+
customPatterns = [],
|
|
127
|
+
threshold = 30, // Score threshold to consider as spam
|
|
128
|
+
strictMode = false
|
|
129
|
+
} = options;
|
|
130
|
+
|
|
131
|
+
const result = {
|
|
132
|
+
isSpam: false,
|
|
133
|
+
email: email,
|
|
134
|
+
score: 0,
|
|
135
|
+
maxScore: 100,
|
|
136
|
+
reasons: [],
|
|
137
|
+
details: {
|
|
138
|
+
localPart: null,
|
|
139
|
+
domain: null,
|
|
140
|
+
matches: []
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (!email || typeof email !== 'string') {
|
|
145
|
+
result.reasons.push('Invalid email provided');
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const local = extractLocal(email);
|
|
150
|
+
const domain = extractDomain(email);
|
|
151
|
+
|
|
152
|
+
if (!local || !domain) {
|
|
153
|
+
result.reasons.push('Could not parse email');
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
result.details.localPart = local;
|
|
158
|
+
result.details.domain = domain;
|
|
159
|
+
|
|
160
|
+
let score = 0;
|
|
161
|
+
|
|
162
|
+
// Check local part patterns
|
|
163
|
+
for (const pattern of SPAM_LOCAL_PATTERNS) {
|
|
164
|
+
if (pattern.test(local)) {
|
|
165
|
+
score += SPAM_SCORES.suspiciousLocalPattern;
|
|
166
|
+
result.reasons.push(`Local part matches spam pattern: ${pattern.toString()}`);
|
|
167
|
+
result.details.matches.push({ type: 'local_pattern', pattern: pattern.toString() });
|
|
168
|
+
break; // Only count first match
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check custom patterns
|
|
173
|
+
for (const pattern of customPatterns) {
|
|
174
|
+
if (pattern instanceof RegExp && pattern.test(email)) {
|
|
175
|
+
score += SPAM_SCORES.suspiciousLocalPattern;
|
|
176
|
+
result.reasons.push('Matches custom spam pattern');
|
|
177
|
+
result.details.matches.push({ type: 'custom_pattern', pattern: pattern.toString() });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check domain patterns
|
|
182
|
+
for (const pattern of SPAM_DOMAIN_PATTERNS) {
|
|
183
|
+
if (pattern.test(domain)) {
|
|
184
|
+
// Check if it's a typosquatting pattern
|
|
185
|
+
if (pattern.toString().includes('g00gle') ||
|
|
186
|
+
pattern.toString().includes('faceb00k') ||
|
|
187
|
+
pattern.toString().includes('paypall') ||
|
|
188
|
+
pattern.toString().includes('amaz0n') ||
|
|
189
|
+
pattern.toString().includes('micr0soft') ||
|
|
190
|
+
pattern.toString().includes('app1e')) {
|
|
191
|
+
score += SPAM_SCORES.typosquatting;
|
|
192
|
+
result.reasons.push('Domain appears to be typosquatting a known brand');
|
|
193
|
+
} else {
|
|
194
|
+
score += SPAM_SCORES.suspiciousDomainPattern;
|
|
195
|
+
result.reasons.push(`Domain matches spam pattern: ${pattern.toString()}`);
|
|
196
|
+
}
|
|
197
|
+
result.details.matches.push({ type: 'domain_pattern', pattern: pattern.toString() });
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check domain keywords
|
|
203
|
+
for (const keyword of SPAM_DOMAIN_KEYWORDS) {
|
|
204
|
+
if (domain.includes(keyword)) {
|
|
205
|
+
score += SPAM_SCORES.suspiciousDomainKeyword;
|
|
206
|
+
result.reasons.push(`Domain contains spam keyword: ${keyword}`);
|
|
207
|
+
result.details.matches.push({ type: 'domain_keyword', keyword });
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check for excessive numbers in local part
|
|
213
|
+
const numberCount = (local.match(/\d/g) || []).length;
|
|
214
|
+
if (numberCount > local.length * 0.6) {
|
|
215
|
+
score += SPAM_SCORES.excessiveNumbers;
|
|
216
|
+
result.reasons.push('Local part contains excessive numbers');
|
|
217
|
+
result.details.matches.push({ type: 'excessive_numbers', count: numberCount });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check for very long local part
|
|
221
|
+
if (local.length > 40) {
|
|
222
|
+
score += SPAM_SCORES.veryLongLocal;
|
|
223
|
+
result.reasons.push('Local part is unusually long');
|
|
224
|
+
result.details.matches.push({ type: 'long_local', length: local.length });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check for very long domain
|
|
228
|
+
const domainWithoutTld = domain.split('.')[0];
|
|
229
|
+
if (domainWithoutTld.length > 30) {
|
|
230
|
+
score += SPAM_SCORES.veryLongDomain;
|
|
231
|
+
result.reasons.push('Domain name is unusually long');
|
|
232
|
+
result.details.matches.push({ type: 'long_domain', length: domainWithoutTld.length });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Strict mode: Additional checks
|
|
236
|
+
if (strictMode) {
|
|
237
|
+
// Check for low-quality TLDs
|
|
238
|
+
const tld = domain.split('.').pop();
|
|
239
|
+
const lowQualityTlds = ['xyz', 'top', 'wang', 'win', 'bid', 'stream', 'download', 'racing', 'date', 'click', 'link', 'gdn', 'men'];
|
|
240
|
+
if (lowQualityTlds.includes(tld)) {
|
|
241
|
+
score += SPAM_SCORES.suspiciousTld;
|
|
242
|
+
result.reasons.push(`Uses potentially suspicious TLD: .${tld}`);
|
|
243
|
+
result.details.matches.push({ type: 'suspicious_tld', tld });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check for impersonation patterns
|
|
247
|
+
const impersonationPatterns = [
|
|
248
|
+
/^(support|help|service|account|secure|verify)[-_]?/i,
|
|
249
|
+
/[-_]?(support|help|service|account|secure|verify)$/i
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
for (const pattern of impersonationPatterns) {
|
|
253
|
+
if (pattern.test(local)) {
|
|
254
|
+
const knownBrands = ['paypal', 'amazon', 'google', 'microsoft', 'apple', 'facebook', 'instagram', 'twitter', 'netflix', 'bank'];
|
|
255
|
+
for (const brand of knownBrands) {
|
|
256
|
+
if (domain.toLowerCase().includes(brand) && !domain.endsWith(`${brand}.com`)) {
|
|
257
|
+
score += SPAM_SCORES.impersonation;
|
|
258
|
+
result.reasons.push(`Possible impersonation of ${brand}`);
|
|
259
|
+
result.details.matches.push({ type: 'impersonation', brand });
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
result.score = Math.min(score, 100); // Cap at 100
|
|
269
|
+
result.isSpam = result.score >= threshold;
|
|
270
|
+
|
|
271
|
+
// Classify spam level
|
|
272
|
+
if (result.score >= 70) {
|
|
273
|
+
result.spamLevel = 'high';
|
|
274
|
+
} else if (result.score >= 40) {
|
|
275
|
+
result.spamLevel = 'medium';
|
|
276
|
+
} else if (result.score >= 20) {
|
|
277
|
+
result.spamLevel = 'low';
|
|
278
|
+
} else {
|
|
279
|
+
result.spamLevel = 'none';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Quick check - returns boolean only
|
|
287
|
+
* @param {string} email - Email to check
|
|
288
|
+
* @param {number} threshold - Score threshold
|
|
289
|
+
* @returns {boolean} True if spam
|
|
290
|
+
*/
|
|
291
|
+
function isSpam(email, threshold = 30) {
|
|
292
|
+
return isSpamEmail(email, { threshold }).isSpam;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get spam score for an email
|
|
297
|
+
* @param {string} email - Email to check
|
|
298
|
+
* @returns {number} Spam score (0-100)
|
|
299
|
+
*/
|
|
300
|
+
function getSpamScore(email) {
|
|
301
|
+
return isSpamEmail(email).score;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Check if domain is suspicious
|
|
306
|
+
* @param {string} domain - Domain to check
|
|
307
|
+
* @returns {boolean} True if suspicious
|
|
308
|
+
*/
|
|
309
|
+
function isSuspiciousDomain(domain) {
|
|
310
|
+
if (!domain) return false;
|
|
311
|
+
|
|
312
|
+
// Check domain patterns
|
|
313
|
+
for (const pattern of SPAM_DOMAIN_PATTERNS) {
|
|
314
|
+
if (pattern.test(domain)) {
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Check domain keywords
|
|
320
|
+
for (const keyword of SPAM_DOMAIN_KEYWORDS) {
|
|
321
|
+
if (domain.includes(keyword)) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
module.exports = {
|
|
330
|
+
isSpamEmail,
|
|
331
|
+
isSpam,
|
|
332
|
+
getSpamScore,
|
|
333
|
+
isSuspiciousDomain,
|
|
334
|
+
SPAM_LOCAL_PATTERNS,
|
|
335
|
+
SPAM_DOMAIN_PATTERNS,
|
|
336
|
+
SPAM_DOMAIN_KEYWORDS,
|
|
337
|
+
SPAM_SCORES
|
|
338
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporary/Disposable Email Detector
|
|
3
|
+
* Detects if an email is from a known disposable email service
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { DISPOSABLE_DOMAINS, DISPOSABLE_PATTERNS } = require('../data/disposableDomains');
|
|
7
|
+
const { extractDomain } = require('./formatValidator');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if email domain is a known disposable email service
|
|
11
|
+
* @param {string} email - Email address to check
|
|
12
|
+
* @param {object} options - Options for checking
|
|
13
|
+
* @returns {object} Result with isTemporary flag and details
|
|
14
|
+
*/
|
|
15
|
+
function isTemporaryEmail(email, options = {}) {
|
|
16
|
+
const {
|
|
17
|
+
customDomains = [],
|
|
18
|
+
customPatterns = [],
|
|
19
|
+
strictMode = false
|
|
20
|
+
} = options;
|
|
21
|
+
|
|
22
|
+
const result = {
|
|
23
|
+
isTemporary: false,
|
|
24
|
+
email: email,
|
|
25
|
+
domain: null,
|
|
26
|
+
reason: null,
|
|
27
|
+
confidence: 'none',
|
|
28
|
+
matchType: null
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (!email || typeof email !== 'string') {
|
|
32
|
+
result.reason = 'Invalid email provided';
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const domain = extractDomain(email);
|
|
37
|
+
if (!domain) {
|
|
38
|
+
result.reason = 'Could not extract domain from email';
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
result.domain = domain;
|
|
43
|
+
|
|
44
|
+
// Check custom domains first
|
|
45
|
+
const customDomainSet = new Set(customDomains.map(d => d.toLowerCase()));
|
|
46
|
+
if (customDomainSet.has(domain)) {
|
|
47
|
+
result.isTemporary = true;
|
|
48
|
+
result.reason = 'Domain matches custom disposable domain list';
|
|
49
|
+
result.confidence = 'high';
|
|
50
|
+
result.matchType = 'custom_domain';
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check known disposable domains
|
|
55
|
+
if (DISPOSABLE_DOMAINS.has(domain)) {
|
|
56
|
+
result.isTemporary = true;
|
|
57
|
+
result.reason = 'Known disposable email domain';
|
|
58
|
+
result.confidence = 'high';
|
|
59
|
+
result.matchType = 'known_domain';
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check subdomain of known disposable domain
|
|
64
|
+
const domainParts = domain.split('.');
|
|
65
|
+
if (domainParts.length > 2) {
|
|
66
|
+
const parentDomain = domainParts.slice(-2).join('.');
|
|
67
|
+
if (DISPOSABLE_DOMAINS.has(parentDomain)) {
|
|
68
|
+
result.isTemporary = true;
|
|
69
|
+
result.reason = `Subdomain of known disposable email domain (${parentDomain})`;
|
|
70
|
+
result.confidence = 'high';
|
|
71
|
+
result.matchType = 'subdomain';
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check custom patterns
|
|
77
|
+
for (const pattern of customPatterns) {
|
|
78
|
+
if (pattern instanceof RegExp && pattern.test(domain)) {
|
|
79
|
+
result.isTemporary = true;
|
|
80
|
+
result.reason = 'Domain matches custom disposable pattern';
|
|
81
|
+
result.confidence = 'medium';
|
|
82
|
+
result.matchType = 'custom_pattern';
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check known disposable patterns
|
|
88
|
+
for (const pattern of DISPOSABLE_PATTERNS) {
|
|
89
|
+
if (pattern.test(domain)) {
|
|
90
|
+
result.isTemporary = true;
|
|
91
|
+
result.reason = `Domain matches disposable pattern: ${pattern.toString()}`;
|
|
92
|
+
result.confidence = 'medium';
|
|
93
|
+
result.matchType = 'pattern';
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Strict mode: Additional heuristic checks
|
|
99
|
+
if (strictMode) {
|
|
100
|
+
// Check for suspicious TLDs commonly used by disposable services
|
|
101
|
+
const suspiciousTlds = ['tk', 'ml', 'ga', 'cf', 'gq', 'cc', 'pw'];
|
|
102
|
+
const tld = domainParts[domainParts.length - 1];
|
|
103
|
+
|
|
104
|
+
if (suspiciousTlds.includes(tld)) {
|
|
105
|
+
// Check if domain looks auto-generated
|
|
106
|
+
const domainWithoutTld = domainParts.slice(0, -1).join('.');
|
|
107
|
+
if (/^[a-z]{3,6}\d{2,}$/i.test(domainWithoutTld) ||
|
|
108
|
+
/^\d+[a-z]+$/i.test(domainWithoutTld)) {
|
|
109
|
+
result.isTemporary = true;
|
|
110
|
+
result.reason = 'Domain appears auto-generated with suspicious TLD';
|
|
111
|
+
result.confidence = 'low';
|
|
112
|
+
result.matchType = 'heuristic';
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check for very long random-looking domains
|
|
118
|
+
if (domain.length > 30 && /^[a-z0-9-]+\.[a-z]{2,4}$/i.test(domain)) {
|
|
119
|
+
const entropy = calculateEntropy(domainParts[0]);
|
|
120
|
+
if (entropy > 3.5) {
|
|
121
|
+
result.isTemporary = true;
|
|
122
|
+
result.reason = 'Domain appears randomly generated';
|
|
123
|
+
result.confidence = 'low';
|
|
124
|
+
result.matchType = 'heuristic';
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Calculate Shannon entropy of a string
|
|
135
|
+
* Higher entropy = more random
|
|
136
|
+
* @param {string} str - String to calculate entropy for
|
|
137
|
+
* @returns {number} Entropy value
|
|
138
|
+
*/
|
|
139
|
+
function calculateEntropy(str) {
|
|
140
|
+
const len = str.length;
|
|
141
|
+
const frequencies = {};
|
|
142
|
+
|
|
143
|
+
for (const char of str) {
|
|
144
|
+
frequencies[char] = (frequencies[char] || 0) + 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let entropy = 0;
|
|
148
|
+
for (const char in frequencies) {
|
|
149
|
+
const p = frequencies[char] / len;
|
|
150
|
+
entropy -= p * Math.log2(p);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return entropy;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Quick check - returns boolean only
|
|
158
|
+
* @param {string} email - Email to check
|
|
159
|
+
* @returns {boolean} True if temporary
|
|
160
|
+
*/
|
|
161
|
+
function isTempEmail(email) {
|
|
162
|
+
return isTemporaryEmail(email).isTemporary;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get the domain from an email and check if it's disposable
|
|
167
|
+
* @param {string} domain - Domain to check
|
|
168
|
+
* @returns {boolean} True if disposable
|
|
169
|
+
*/
|
|
170
|
+
function isDisposableDomain(domain) {
|
|
171
|
+
if (!domain) return false;
|
|
172
|
+
const normalizedDomain = domain.toLowerCase().trim();
|
|
173
|
+
return DISPOSABLE_DOMAINS.has(normalizedDomain);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Add custom disposable domains to check
|
|
178
|
+
* @param {string[]} domains - Array of domains to add
|
|
179
|
+
*/
|
|
180
|
+
function addDisposableDomains(domains) {
|
|
181
|
+
if (Array.isArray(domains)) {
|
|
182
|
+
domains.forEach(domain => {
|
|
183
|
+
if (typeof domain === 'string') {
|
|
184
|
+
DISPOSABLE_DOMAINS.add(domain.toLowerCase().trim());
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get count of known disposable domains
|
|
192
|
+
* @returns {number} Count of disposable domains
|
|
193
|
+
*/
|
|
194
|
+
function getDisposableDomainCount() {
|
|
195
|
+
return DISPOSABLE_DOMAINS.size;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
isTemporaryEmail,
|
|
200
|
+
isTempEmail,
|
|
201
|
+
isDisposableDomain,
|
|
202
|
+
addDisposableDomains,
|
|
203
|
+
getDisposableDomainCount,
|
|
204
|
+
calculateEntropy
|
|
205
|
+
};
|