Skip to content
Start with 100 free verification credits
Qualisend
All articles
Engineering / June 13, 2026

How to validate an email address in C#

7 minutes read

Qualisend team
A C# code window validating an email through syntax, DNS, and SMTP layers

To validate an email address in C#, most developers reach for System.Net.Mail.MailAddress and move on. It's a fair first move — the type is built into the framework and rejects garbage without a hand-rolled regex — but it answers only the first of the three questions real validation asks. Is the address shaped correctly? Can its domain receive mail? Does the mailbox actually exist? The .NET base class library answers the first cleanly, needs one well-known NuGet package for the second, and leaves the third to a network problem worth handing off. This guide builds email validation in C# as three layers, with idiomatic async/await code for each, and shows exactly where MailAddress stops.

The short answer#

Use MailAddress.TryCreate for syntax, the DnsClient.NET package for the MX lookup, and a verification API for the SMTP mailbox check — cheapest first, short-circuiting as soon as one layer is decisive. Don't open raw SMTP sockets from your app to probe mailboxes yourself: outbound port 25 is blocked on most hosts, and the answer depends on sending-IP reputation and greylisting you don't want to reimplement. Each layer rules addresses out more cheaply than the last; only the final one can rule an address in. If you just want a verdict for a single address right now, paste it into the free email checker and skip the code — the rest of this guide is for wiring the check into an app. For the concepts behind each stage, see what email verification is.

Layer 1: syntax with MailAddress.TryCreate#

MailAddress is the right tool for layer one. Prefer the TryCreate overload added in .NET 5: it returns a bool instead of throwing a FormatException on bad input, so you can gate on it without wrapping every call in a try/catch.

using System.Net.Mail;

public static bool IsValidSyntax(string email)
{
    return !string.IsNullOrWhiteSpace(email)
        && email.Length <= 320
        && MailAddress.TryCreate(email, out var parsed)
        && parsed.Address == email;
}
IsValidSyntax("jane@example.com");           // true
IsValidSyntax("not-an-email");               // false
IsValidSyntax("Jane <jane@example.com>");    // false

That last comparison matters. MailAddress is a mail parser, not a strict validator — it happily accepts display-name forms like Jane <jane@example.com> and exposes the address portion on parsed.Address. Comparing parsed.Address back to the raw input rejects those forms and stray whitespace, which is what you want from a yes/no syntax gate on user input. The 320-character guard is belt-and-braces: nothing longer can be a real address, and it's cheaper to reject early than to hand a giant string to anything downstream.

On .NET Framework or any target older than .NET 5, TryCreate doesn't exist, so fall back to the constructor and catch the exception:

try { _ = new MailAddress(email); /* syntax ok */ }
catch (FormatException) { /* reject */ }

If you're already using data annotations on a model, the System.ComponentModel.DataAnnotations.EmailAddressAttribute ([EmailAddress]) does the same job declaratively and is a fine choice inside ASP.NET Core model validation. Whichever you pick, treat a pass as "worth checking properly," never as "valid." A syntax check knows nothing about DNS or mailboxes — it's the same reason regex email validation fails: shape and deliverability are different questions, one a fact about the string and the other a fact about the internet.

Layer 2: can the domain receive mail?#

A domain with no mail route can't accept mail for anyone, so this one lookup eliminates dead domains, misspelled company names, and invented TLDs before you ever touch the network for real. The catch: .NET has no built-in MX resolver. System.Net.Dns resolves A and AAAA records but not MX, so the idiomatic answer is DnsClient.NET — a widely used, well-maintained NuGet package that most .NET projects reach for when they need DNS beyond host lookups. Install it and query the MX record type:

using System.Linq;
using DnsClient; // dotnet add package DnsClient

// LookupClient is thread-safe and caches responses — create one and reuse it.
private static readonly LookupClient Dns = new();

public static async Task<bool> HasMailRouteAsync(string domain)
{
    try
    {
        var response = await Dns.QueryAsync(domain, QueryType.MX);
        return response.Answers.MxRecords().Any();
    }
    catch (DnsResponseException)
    {
        // No reachable resolver, SERVFAIL, and similar — treat as "check later".
        return false;
    }
}
await HasMailRouteAsync("gmail.com");               // true
await HasMailRouteAsync("company-that-folded.com"); // false

Two idioms worth copying. Create the LookupClient once and reuse it — it's thread-safe and caches responses, so a per-request instance just throws away that cache. And split the domain off the address at the last @ with a range expression so you never mis-parse an address that (legally) contains more than one:

var domain = email[(email.LastIndexOf('@') + 1)..];

Some domains accept mail on an A record with no MX (an implicit MX). If you want to honour that edge case, fall back to QueryType.A when the MX set is empty — but for the overwhelming majority of real addresses, an MX check is the right filter. One caution for a signup path: a DNS query blocks on a resolver, so a transient DNS hiccup shouldn't hard-reject a real customer. Treat a failed lookup as "check later," not "invalid."

Layer 3: does the mailbox actually exist?#

Layers one and two can only rule an address out. A domain can publish perfect MX records and still have no mailbox at the address you're holding — noreply-9f2x@gmail.com is valid syntax on a domain with a live mail route, and it's still a mailbox that was never created. Confirming that a specific mailbox exists means the SMTP delivery conversation: connect to the mail host, issue RCPT TO, read the reply, and disconnect before sending anything. How email verification works walks that full pipeline, including catch-all domains that accept every address and defeat a naive probe.

In principle you can script this in C# with a TcpClient and raw SMTP commands. In practice you shouldn't run it from your application server. Most cloud providers block outbound port 25, so a probe that works on your laptop quietly fails in production. The answer depends on the reputation of the IP you connect from, not just the address you're checking. And receiving servers greylist and rate-limit unfamiliar senders, so probing at any volume gets you deferred or blocklisted. This is the layer worth delegating to infrastructure built for it.

Calling a verification API from C##

Qualisend's verification endpoint runs the whole pipeline — syntax, DNS, and the SMTP mailbox probe — from reputation-managed infrastructure, and returns a verdict in real time. HttpClient plus System.Text.Json is all you need; model the response envelope as records and let the JSON source generator or reflection deserializer fill them:

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;

public record VerifyResponse(
    [property: JsonPropertyName("result")] VerifyResult Result);

public record VerifyResult(
    [property: JsonPropertyName("status")] string Status,
    [property: JsonPropertyName("score")] int Score,
    [property: JsonPropertyName("reason")] string? Reason,
    [property: JsonPropertyName("sub_flags")] IReadOnlyDictionary<string, bool> SubFlags);
// One HttpClient for the whole app — don't new one up per request.
private static readonly HttpClient Http = new()
{
    BaseAddress = new Uri("https://api.qualisend.com/v1/"),
};

public static async Task<VerifyResult> VerifyAsync(string email)
{
    using var request = new HttpRequestMessage(HttpMethod.Post, "verify")
    {
        Content = JsonContent.Create(new { email }),
    };
    request.Headers.Authorization = new AuthenticationHeaderValue(
        "Bearer", Environment.GetEnvironmentVariable("QUALISEND_API_KEY"));

    using var response = await Http.SendAsync(request);
    response.EnsureSuccessStatusCode();

    var body = await response.Content.ReadFromJsonAsync<VerifyResponse>();
    return body!.Result; // status: deliverable | risky | undeliverable | unknown
}

POST https://api.qualisend.com/v1/verify is a placeholder shape — check the API reference on /developers for the exact endpoint, the scoped key format, and every field. Read the key from an environment variable rather than hard-coding YOUR_API_KEY, and reuse a single HttpClient to avoid socket exhaustion. The response comes back inside a result envelope:

{
  "result": {
    "status": "deliverable",
    "score": 95,
    "reason": null,
    "sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false }
  }
}

status is your headline verdict — deliverable, risky, undeliverable, or unknown. score grades confidence, reason explains a negative result, and sub_flags breaks out signals like role, disposable, free, and catch-all so you can apply your own policy (reject undeliverable, hold risky for review, flag disposable at signup).

Putting the layers together to validate an email address in C##

Cheapest first, stop as soon as you have an answer:

public static async Task<VerifyResult> ValidateEmailAsync(string email)
{
    if (!IsValidSyntax(email))
        return new VerifyResult("undeliverable", 0, "invalid_email",
            new Dictionary<string, bool>());

    var domain = email[(email.LastIndexOf('@') + 1)..];
    if (!await HasMailRouteAsync(domain))
        return new VerifyResult("undeliverable", 0, "invalid_domain",
            new Dictionary<string, bool>());

    return await VerifyAsync(email); // deliverable | risky | undeliverable | unknown
}

The two local layers cost nothing and catch most junk instantly; the API layer runs only on addresses worth the network round-trip. That ordering is the whole trick — the same shape you'll find in the Node.js, Python, and PHP versions of this guide, because the layering, not the language, is what makes validation work.

Frequently asked questions#

Is MailAddress or EmailAddressAttribute enough to validate an email in C#?#

For syntax, yes — both are solid layer-one checks and a better bet than a hand-rolled regex, with MailAddress.TryCreate giving you a no-throw boolean. But they validate shape, not deliverability: neither resolves DNS or contacts a mail server, so a pass means "looks like an email," not "will deliver." Pair the syntax check with an MX lookup and an SMTP mailbox check before you trust the address.

Does .NET have a built-in way to look up MX records?#

No. System.Net.Dns resolves A and AAAA host records but has no MX support, which is why the standard approach is the DnsClient.NET NuGet package. Query QueryType.MX, reuse a single LookupClient, and treat an empty answer set — or a DnsResponseException — as no mail route rather than letting it throw.

Can I check whether a mailbox exists in C# without an API?#

Only partly. DnsClient.NET confirms the domain accepts mail, which rules out dead domains cheaply. Confirming the actual mailbox means an SMTP conversation you can attempt with a TcpClient but shouldn't run from your app server — outbound port 25 is widely blocked and the result depends on your sending IP's reputation. That's the layer a verification service exists to handle, and it's worth comparing providers before you build it yourself.

Should I validate emails at signup or when cleaning a list?#

Both, at different depths. Run syntax and the MX lookup synchronously at signup — they're fast enough to block the request and give instant feedback — and act on the immediate API verdict there too; the serverless signup guide shows the pattern end to end. Reserve deeper, batched verification for cleaning an existing list, where latency doesn't matter and thoroughness does.


Ready to add the SMTP layer? The API reference has the exact /verify shape, scoped keys, and copy-paste examples, or drop one address into the free email checker to watch a MailAddress-approved string come back undeliverable.

Your reputation, protected.

Clean your first list in minutes. 100 free credits, no card required.

Get started