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.
@@ -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
+ };