using CPRNIMS.Infrastructure.Dto.SMTP; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace CPRNIMS.Infrastructure.Helper { public class EmailValidationService { // In-memory cache for MX lookups { domain -> (isValid, timestamp) } private static readonly Dictionary _mxCache = new(); private static readonly TimeSpan _mxCacheExpiry = TimeSpan.FromHours(24); // Persistent bounce list - load from your DB or file private static readonly HashSet _bounceList = new(StringComparer.OrdinalIgnoreCase); // Known disposable/fake email domains (expand this list as needed) private static readonly HashSet _disposableDomains = new(StringComparer.OrdinalIgnoreCase) { // Disposable/temp mail providers "mailinator.com", "guerrillamail.com", "tempmail.com", "throwaway.email", "yopmail.com", "sharklasers.com", "guerrillamailblock.com", "grr.la", "guerrillamail.info", "spam4.me", "trashmail.com", "trashmail.me", "trashmail.net", "trashmail.at", "trashmail.io", "trashmail.org", "fakeinbox.com", "mailnull.com", "spamgourmet.com", "spamgourmet.net", "spamgourmet.org", "maildrop.cc", "dispostable.com", "mailexpire.com", "spamex.com", "deadaddress.com", "spamfree24.org", "no-spam.ws", "discard.email", "despam.it", "emailsensei.com", "getonemail.com", "spamfree.eu", "spaml.com", "spammotel.com", "spamspot.com", "tempe-mail.com", "tempinbox.com", "temp-mail.org", "emailtemporanea.net", "crazymailing.com", "dispostable.com", "spamgob.com", "0-mail.com", "0815.ru", "0clickemail.com", "10minutemail.com", "20minutemail.com", "filzmail.com", "getnada.com", "incognitomail.com", "jetable.fr.nf", "lortemail.dk", "mytempemail.com", "noclickemail.com", "nowmymail.com", "objectmail.com", "odaymail.com", "onewaymail.com", "pookmail.com", "privacy.net", "proxymail.eu", "rcpt.at", "rklips.com", "shortmail.net", "sogetthis.com", "spamgob.com", "spaml.de", "speed.1s.fr", "supergreatmail.com", "suremail.info", "tempail.com", "tempemail.net", "temporarioemail.com.br", "thanksnospam.info", "thisisnotmyrealemail.com", "throwam.com", "trbvm.com", "truckmail.com", "tyldd.com", "uggsrock.com", "veryrealemail.com", "vidchart.com", "wegwerfmail.de", "wegwerfmail.net", "wegwerfmail.org", "wh4f.org", "whyspam.me", "willselfdestruct.com", "wronghead.com", "wuzupmail.net", "xagloo.com", "xemaps.com", "xents.com", "xmaily.com", "xoxy.net", "yep.it", "yogamaven.com", "yuurok.com", "zehnminutenmail.de", "zippymail.info", "zoaxe.com", "zoemail.net", "zoemail.org" }; // Known legitimate domains — skip MX lookup to save time private static readonly HashSet _trustedDomains = new(StringComparer.OrdinalIgnoreCase) { "gmail.com", "yahoo.com", "outlook.com", "hotmail.com", "live.com", "icloud.com", "me.com", "mac.com", "msn.com", "aol.com", "protonmail.com", "proton.me", "zoho.com", "yandex.com", "fastmail.com", "hey.com", "tutanota.com" }; /// /// Fast multi-layer email validation without SMTP handshake. /// Average time per email: ~5-50ms (MX lookup only on unknown domains, cached after first hit). /// public async Task ValidateAsync(string email) { email = email.Trim().ToLowerInvariant(); // LAYER 1: Regex format check (instant) if (!IsValidFormat(email)) return EmailValidationResult.Fail(email, "Invalid email format"); var domain = email.Split('@')[1]; // LAYER 2: Bounce list check (instant - known bad addresses from past sends) if (_bounceList.Contains(email)) return EmailValidationResult.Fail(email, "Previously bounced email address"); // LAYER 3: Disposable/fake domain check (instant - local hashset lookup) if (_disposableDomains.Contains(domain)) return EmailValidationResult.Fail(email, $"Disposable email domain: {domain}"); // LAYER 4: Trusted domain fast-pass (instant - skip DNS for known providers) if (_trustedDomains.Contains(domain)) return EmailValidationResult.Pass(email); // LAYER 5: MX Record check with caching (fast after first lookup) bool hasMx = await HasValidMxRecordAsync(domain); if (!hasMx) return EmailValidationResult.Fail(email, $"No MX record found for domain: {domain}"); return EmailValidationResult.Pass(email); } /// /// Validates a batch of emails concurrently for performance. /// public async Task> ValidateBatchAsync(IEnumerable emails) { var tasks = emails.Select(e => ValidateAsync(e)); var results = await Task.WhenAll(tasks); return results.ToList(); } /// /// Call this after a send attempt — records bounced addresses so they're /// skipped automatically in future sends. /// public void RecordBounce(string email) { _bounceList.Add(email.Trim().ToLowerInvariant()); Console.WriteLine($"[BounceTracker] Recorded bounced email: {email}"); // TODO: Persist to your database here // await _dbContext.BouncedEmails.AddAsync(new BouncedEmail { Address = email, Date = DateTime.UtcNow }); } private bool IsValidFormat(string email) { var regex = new Regex( @"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$", RegexOptions.IgnoreCase ); return regex.IsMatch(email); } private async Task HasValidMxRecordAsync(string domain) { // Return cached result if still fresh if (_mxCache.TryGetValue(domain, out var cached)) { if (DateTime.UtcNow - cached.CachedAt < _mxCacheExpiry) return cached.IsValid; } try { var processInfo = new System.Diagnostics.ProcessStartInfo("nslookup") { Arguments = $"-type=MX {domain}", RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; using var process = System.Diagnostics.Process.Start(processInfo); if (process == null) { _mxCache[domain] = (true, DateTime.UtcNow); // assume valid on error return true; } // Timeout nslookup after 5 seconds var outputTask = process.StandardOutput.ReadToEndAsync(); if (await Task.WhenAny(outputTask, Task.Delay(5000)) != outputTask) { Console.WriteLine($"[MX] DNS lookup timed out for {domain}, assuming valid."); _mxCache[domain] = (true, DateTime.UtcNow); return true; } string output = await outputTask; bool hasMx = Regex.IsMatch(output, @"mail exchanger", RegexOptions.IgnoreCase); _mxCache[domain] = (hasMx, DateTime.UtcNow); Console.WriteLine($"[MX] {domain} → {(hasMx ? "Valid MX" : "No MX found")} (cached)"); return hasMx; } catch (Exception ex) { Console.WriteLine($"[MX] Lookup error for {domain}: {ex.Message}"); _mxCache[domain] = (true, DateTime.UtcNow); // assume valid on error return true; } } } }