Um eine E-Mail-Adresse in C# zu validieren, greifen die meisten Entwickler zu
System.Net.Mail.MailAddress und machen weiter. Das ist ein vertretbarer erster
Schritt — der Typ ist im Framework eingebaut und weist Müll ohne selbst gebaute
Regex zurück —, aber er beantwortet nur die erste der drei Fragen, die echte
Validierung stellt. Ist die Adresse korrekt geformt? Kann ihre Domain Mail
empfangen? Existiert das Postfach tatsächlich? Die .NET-Basisklassenbibliothek
beantwortet die erste Frage sauber, benötigt für die zweite ein bekanntes
NuGet-Paket und überlässt die dritte einem Netzwerkproblem, das man besser
auslagert. Dieser Leitfaden baut E-Mail-Validierung in C# als drei Schichten auf,
jeweils mit idiomatischem async/await-Code, und zeigt genau, wo MailAddress
aufhört.
Die kurze Antwort#
Verwenden Sie MailAddress.TryCreate für die Syntax, das Paket DnsClient.NET für
den MX-Lookup und eine Verifizierungs-API für die SMTP-Postfachprüfung — das
Günstigste zuerst, mit Kurzschluss, sobald eine Schicht entscheidend ist. Öffnen
Sie keine rohen SMTP-Sockets aus Ihrer App, um Postfächer selbst zu prüfen: Der
ausgehende Port 25 ist auf den meisten Hosts blockiert, und die Antwort hängt von
der Reputation der sendenden IP und von Greylisting ab, das Sie nicht neu
implementieren wollen. Jede Schicht schließt Adressen günstiger aus als die
vorige; nur die letzte kann eine Adresse einschließen. Wenn Sie nur ein Verdikt
für eine einzelne Adresse brauchen, fügen Sie sie in den
kostenlosen E-Mail-Checker ein und überspringen Sie den
Code — der Rest dieses Leitfadens dient dazu, die Prüfung in eine App einzubauen.
Die Konzepte hinter jeder Stufe finden Sie unter
was E-Mail-Verifizierung ist.
Schicht 1: Syntax mit MailAddress.TryCreate#
MailAddress ist das richtige Werkzeug für Schicht eins. Bevorzugen Sie die in
.NET 5 hinzugefügte Überladung TryCreate: Sie gibt bei fehlerhafter Eingabe
einen bool zurück, statt eine FormatException zu werfen, sodass Sie darauf
prüfen können, ohne jeden Aufruf in ein try/catch zu verpacken.
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
Dieser letzte Vergleich ist wichtig. MailAddress ist ein Mail-Parser, kein
strenger Validator — er akzeptiert bereitwillig Anzeigename-Formen wie
Jane <jane@example.com> und legt den Adressteil über parsed.Address offen.
Wenn Sie parsed.Address mit der rohen Eingabe vergleichen, weisen Sie solche
Formen und versprengte Leerzeichen zurück — genau das, was Sie von einer
Ja/Nein-Syntaxprüfung auf Benutzereingaben erwarten. Der Schutz auf 320 Zeichen
ist doppelt abgesichert: Nichts Längeres kann eine echte Adresse sein, und es ist
günstiger, früh abzulehnen, als eine riesige Zeichenkette an nachgelagerte
Verarbeitung zu übergeben.
Auf .NET Framework oder jedem Ziel älter als .NET 5 existiert TryCreate nicht,
greifen Sie also auf den Konstruktor zurück und fangen Sie die Exception ab:
try { _ = new MailAddress(email); /* syntax ok */ }
catch (FormatException) { /* reject */ }
Wenn Sie bereits Data Annotations auf einem Modell verwenden, erledigt das
System.ComponentModel.DataAnnotations.EmailAddressAttribute ([EmailAddress])
dieselbe Aufgabe deklarativ und ist eine gute Wahl innerhalb der
Modellvalidierung von ASP.NET Core. Was auch immer Sie wählen: Behandeln Sie ein
Bestehen als „lohnt sich, richtig zu prüfen“, niemals als „gültig“. Eine
Syntaxprüfung weiß nichts über DNS oder Postfächer — aus demselben Grund
scheitert Regex-basierte E-Mail-Validierung:
Form und Zustellbarkeit sind unterschiedliche Fragen, die eine ist eine Tatsache
über die Zeichenkette, die andere eine Tatsache über das Internet.
Schicht 2: Kann die Domain Mail empfangen?#
Eine Domain ohne Mail-Route kann für niemanden Mail annehmen, dieser eine Lookup
eliminiert also tote Domains, falsch geschriebene Firmennamen und erfundene TLDs,
bevor Sie das Netzwerk überhaupt richtig berühren. Der Haken: .NET hat keinen
eingebauten MX-Resolver. System.Net.Dns löst A- und AAAA-Einträge auf, aber
kein MX, die idiomatische Antwort ist also DnsClient.NET — ein weit verbreitetes,
gut gepflegtes NuGet-Paket, zu dem die meisten .NET-Projekte greifen, wenn sie DNS
über Host-Lookups hinaus brauchen. Installieren Sie es und fragen Sie den
MX-Eintragstyp ab:
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
Zwei nachahmenswerte Idiome. Erstellen Sie den LookupClient einmal und
verwenden Sie ihn wieder — er ist thread-sicher und cacht Antworten, eine Instanz
pro Anfrage wirft diesen Cache also einfach weg. Und trennen Sie die Domain am
letzten @ mit einem Range-Ausdruck von der Adresse ab, damit Sie eine Adresse,
die (rechtmäßig) mehr als ein @ enthält, nie falsch parsen:
var domain = email[(email.LastIndexOf('@') + 1)..];
Manche Domains nehmen Mail über einen A-Eintrag ohne MX an (ein implizites MX).
Wenn Sie diesen Grenzfall berücksichtigen möchten, greifen Sie auf QueryType.A
zurück, wenn die MX-Menge leer ist — aber für die überwältigende Mehrheit echter
Adressen ist eine MX-Prüfung der richtige Filter. Eine Warnung für einen
Anmeldepfad: Eine DNS-Abfrage blockiert an einem Resolver, ein vorübergehender
DNS-Aussetzer sollte also keinen echten Kunden hart ablehnen. Behandeln Sie einen
fehlgeschlagenen Lookup als „später prüfen“, nicht als „ungültig“.
Schicht 3: Existiert das Postfach tatsächlich?#
Schicht eins und zwei können eine Adresse nur ausschließen. Eine Domain kann
perfekte MX-Einträge veröffentlichen und trotzdem kein Postfach an der Adresse
haben, die Sie in der Hand halten — noreply-9f2x@gmail.com ist gültige Syntax
auf einer Domain mit einer aktiven Mail-Route, und es ist trotzdem ein Postfach,
das nie angelegt wurde. Zu bestätigen, dass ein bestimmtes Postfach existiert,
bedeutet die SMTP-Zustellkonversation: sich mit dem Mailhost verbinden, RCPT TO
absetzen, die Antwort lesen und die Verbindung trennen, bevor irgendetwas
gesendet wird. Wie E-Mail-Verifizierung funktioniert
geht diese vollständige Pipeline durch, einschließlich Catch-all-Domains, die
jede Adresse annehmen und eine naive Prüfung aushebeln.
Im Prinzip können Sie das in C# mit einem TcpClient und rohen SMTP-Befehlen
skripten. In der Praxis sollten Sie es nicht von Ihrem Anwendungsserver aus
ausführen. Die meisten Cloud-Anbieter blockieren den ausgehenden Port 25, eine
Prüfung, die auf Ihrem Laptop funktioniert, scheitert also im Produktivbetrieb
still und leise. Die Antwort hängt von der Reputation der IP ab, von der aus Sie
sich verbinden, nicht nur von der Adresse, die Sie prüfen. Und empfangende Server
setzen unbekannte Absender auf Greylisting und Rate-Limits, Prüfen in jeglichem
Volumen bringt Ihnen also Verzögerungen oder einen Eintrag auf Sperrlisten ein.
Das ist die Schicht, die man an dafür gebaute Infrastruktur delegieren sollte.
Eine Verifizierungs-API aus C# aufrufen#
Der Verifizierungs-Endpunkt von Qualisend führt die gesamte Pipeline aus —
Syntax, DNS und die SMTP-Postfachprüfung — von reputationsverwalteter
Infrastruktur aus und liefert ein Verdikt in Echtzeit zurück. HttpClient plus
System.Text.Json ist alles, was Sie brauchen; modellieren Sie den
Antwort-Umschlag als Records und lassen Sie den JSON-Source-Generator oder den
Reflection-Deserializer sie füllen:
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading.Channels;
public record VerifyResponse(
[property: JsonPropertyName("job_id")] string JobId,
[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,
[property: JsonPropertyName("smtp")] string? Smtp = null); // pending | cached | skipped
// One HttpClient for the whole app — don't new one up per request.
private static readonly HttpClient Http = new()
{
BaseAddress = new Uri("https://app.qualisend.com/api/v1/"),
Timeout = TimeSpan.FromSeconds(5), // fail fast: the default is 100 s
};
// Fail open: when the API can't answer, let the address through and re-check it later.
private static readonly VerifyResult FailOpen =
new("unknown", 0, null, new Dictionary<string, bool>());
// (email, job id) pairs whose mailbox probe is still running. A background
// worker drains this channel; use a durable queue in production.
private static readonly Channel<(string Email, string JobId)> PendingProbes =
Channel.CreateUnbounded<(string Email, string JobId)>();
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"));
try
{
using var response = await Http.SendAsync(request);
var code = (int)response.StatusCode;
if (code == 402 || code == 429 || code >= 500)
return FailOpen; // out of credits, rate limited, or a server error
response.EnsureSuccessStatusCode(); // 400/401/403: fix the request or the key
var body = await response.Content.ReadFromJsonAsync<VerifyResponse>();
if (body!.Result.Smtp == "pending")
PendingProbes.Writer.TryWrite((email, body.JobId)); // finished off the request path
return body.Result; // status: deliverable | risky | undeliverable | unknown
}
catch (TaskCanceledException)
{
return FailOpen; // timed out
}
}
Das Beispiel ruft POST https://app.qualisend.com/api/v1/verify auf; die
API-Referenz unter /developers beschreibt das Format des
Scoped Keys und jedes Feld. Lesen Sie den Key aus einer
Umgebungsvariable, statt YOUR_API_KEY fest zu codieren, und verwenden Sie einen
einzigen HttpClient wieder, um Socket-Erschöpfung zu vermeiden. Auf einem
Anmeldeformular arbeitet VerifyAsync nach dem Fail-open-Prinzip: Ein 402
(keine Credits mehr), ein 429 (Rate-Limit), ein 5xx oder ein Timeout liefert
unknown, sodass ein echter Kunde durchkommt und Sie die Adresse später erneut
prüfen. Die Antwort kommt in einem result-Umschlag zurück:
{
"job_id": "3f9a6c1e-5b7d-4e2a-9c8f-1d2e3a4b5c6d",
"result": {
"status": "unknown",
"score": 61,
"reason": null,
"smtp": "pending",
"sub_flags": { "role": false, "disposable": false, "free_mail": false, "catch_all": false }
}
}
status ist Ihr Kernverdikt — deliverable, risky, undeliverable oder
unknown. score bewertet die Konfidenz, reason erklärt ein negatives
Ergebnis, und sub_flags schlüsselt Signale wie role, disposable,
free_mail und catch_all auf, sodass Sie Ihre eigene Policy anwenden können
(undeliverable ablehnen, risky zur Prüfung zurückhalten, disposable bei
der Anmeldung markieren).
Prüfen Sie smtp, bevor Sie status als endgültig behandeln: Bei cached oder
skipped ist das Verdikt bereits final. Bei pending läuft die Postfachprüfung
noch, status ist ein vorläufiges unknown, reason ist null, und das
endgültige Verdikt liefert GET /jobs/{job_id}. Deshalb reiht VerifyAsync
ausstehende Jobs in eine Warteschlange ein, statt auf sie zu warten. Ein
Hintergrund-Worker (etwa ein Hosted Service) liest PendingProbes und fragt
jeden Job alle paar Sekunden ab, abseits des Request-Pfads; /verify ruft er
für dieselbe Adresse nie erneut auf:
public record JobResponse(
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("results")] IReadOnlyList<VerifyResult>? Results);
// Runs in the background worker, never in the web request. Returns null while
// the probe is still running: call it again in a few seconds.
public static async Task<VerifyResult?> GetFinalVerdictAsync(string jobId)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"jobs/{jobId}?include=results");
request.Headers.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("QUALISEND_API_KEY"));
using var response = await Http.SendAsync(request);
response.EnsureSuccessStatusCode();
var job = await response.Content.ReadFromJsonAsync<JobResponse>();
return job!.Status switch
{
"completed" => job.Results?.FirstOrDefault(), // the final, post-SMTP verdict
"queued" or "processing" => null, // not finished yet
_ => throw new InvalidOperationException($"Job {jobId} {job.Status}"), // canceled | failed
};
}
Die Schichten zusammensetzen, um eine E-Mail-Adresse in C# zu validieren#
Das Günstigste zuerst, aufhören, sobald Sie eine Antwort haben:
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
}
Die beiden lokalen Schichten kosten nichts und fangen den meisten Müll sofort ab; die API-Schicht läuft nur auf Adressen, die den Netzwerk-Roundtrip wert sind. Diese Reihenfolge ist der ganze Trick — dieselbe Form finden Sie in den Versionen dieses Leitfadens für Node.js, Python und PHP, denn es ist die Schichtung, nicht die Sprache, die Validierung funktionieren lässt.
Häufig gestellte Fragen
01Reicht MailAddress oder EmailAddressAttribute aus, um eine E-Mail in C# zu validieren?
Für die Syntax ja - beide sind solide Prüfungen der ersten Schicht und die bessere Wahl gegenüber einer selbst gebauten Regex, wobei MailAddress.TryCreate Ihnen einen Boolean ohne Exception liefert. Sie validieren aber die Form, nicht die Zustellbarkeit: Keines von beiden löst DNS auf oder kontaktiert einen Mailserver, ein Bestehen bedeutet also „sieht aus wie eine E-Mail“, nicht „wird zugestellt“. Kombinieren Sie die Syntaxprüfung mit einem MX-Lookup und einer SMTP-Postfachprüfung, bevor Sie der Adresse vertrauen.
02Bietet .NET eine eingebaute Möglichkeit, MX-Einträge abzufragen?
Nein. System.Net.Dns löst A- und AAAA-Host-Einträge auf, unterstützt aber kein MX, weshalb der Standardansatz das NuGet-Paket DnsClient.NET ist. Fragen Sie QueryType.MX ab, verwenden Sie einen einzigen LookupClient wieder und behandeln Sie eine leere Antwortmenge - oder eine DnsResponseException - als fehlende Mail-Route, statt sie eine Exception werfen zu lassen.
03Kann ich in C# ohne API prüfen, ob ein Postfach existiert?
Nur teilweise. DnsClient.NET bestätigt, dass die Domain Mail annimmt, was tote Domains kostengünstig ausschließt. Das tatsächliche Postfach zu bestätigen bedeutet eine SMTP-Konversation, die Sie mit einem TcpClient versuchen können, aber nicht von Ihrem App-Server aus ausführen sollten - der ausgehende Port 25 ist weitverbreitet blockiert und das Ergebnis hängt von der Reputation Ihrer sendenden IP ab. Das ist die Schicht, für die es einen Verifizierungsdienst gibt, und es lohnt sich, Anbieter zu vergleichen, bevor Sie es selbst bauen.
04Sollte ich E-Mails bei der Anmeldung oder beim Bereinigen einer Liste validieren?
Beides, in unterschiedlicher Tiefe. Führen Sie Syntax und MX-Lookup bei der Anmeldung synchron aus - sie sind schnell genug, um die Anfrage zu blockieren und sofortiges Feedback zu geben - und handeln Sie auch dort nach dem unmittelbaren API-Verdikt; der Leitfaden zur serverlosen Anmeldung zeigt das Muster durchgängig. Reservieren Sie die tiefere, gebündelte Verifizierung für das Bereinigen einer bestehenden Liste, wo Latenz keine Rolle spielt, Gründlichkeit aber schon.
Bereit, die SMTP-Schicht hinzuzufügen? Die API-Referenz enthält
die genaue /verify-Form, Scoped Keys und Copy-and-paste-Beispiele, oder werfen
Sie eine Adresse in den kostenlosen E-Mail-Checker, um zu
beobachten, wie eine von MailAddress genehmigte Zeichenkette als
undeliverable zurückkommt.



