177 lines
8.0 KiB
C#
177 lines
8.0 KiB
C#
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<string, (bool IsValid, DateTime CachedAt)> _mxCache = new();
|
|
private static readonly TimeSpan _mxCacheExpiry = TimeSpan.FromHours(24);
|
|
|
|
// Persistent bounce list - load from your DB or file
|
|
private static readonly HashSet<string> _bounceList = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Known disposable/fake email domains (expand this list as needed)
|
|
private static readonly HashSet<string> _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<string> _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"
|
|
};
|
|
|
|
/// <summary>
|
|
/// Fast multi-layer email validation without SMTP handshake.
|
|
/// Average time per email: ~5-50ms (MX lookup only on unknown domains, cached after first hit).
|
|
/// </summary>
|
|
public async Task<EmailValidationResult> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a batch of emails concurrently for performance.
|
|
/// </summary>
|
|
public async Task<List<EmailValidationResult>> ValidateBatchAsync(IEnumerable<string> emails)
|
|
{
|
|
var tasks = emails.Select(e => ValidateAsync(e));
|
|
var results = await Task.WhenAll(tasks);
|
|
return results.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call this after a send attempt — records bounced addresses so they're
|
|
/// skipped automatically in future sends.
|
|
/// </summary>
|
|
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<bool> 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;
|
|
}
|
|
}
|
|
}
|
|
}
|