Die E-Mail-Validierung in PHP beginnt mit einer eingebauten Funktion, filter_var($email, FILTER_VALIDATE_EMAIL), und in vielen Projekten endet sie dort auch. Diese Funktion prüft das Format und sonst nichts: Eine Adresse auf einer Domain, die nicht existiert, oder für ein Postfach, das nie angelegt wurde, besteht. Diese Anleitung ergänzt die fehlenden Prüfungen in der Reihenfolge, in der sie laufen, mit Code, den wir unter PHP 8.5 ausgeführt haben: eine strengere Formatprüfung, eine DNS-Prüfung, eine Postfachprüfung über eine API und dieselben Schritte als Laravel-Validierungsregel.
1. Formatprüfung mit filter_var
<?php
$email = is_string($_POST['email'] ?? null) ? trim($_POST['email']) : '';
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
http_response_code(422);
exit('Bitte geben Sie eine gültige E-Mail-Adresse ein.');
}
Entfernen Sie Leerzeichen vor der Prüfung: In unserem Test lehnte filter_var eine Adresse mit einem Leerzeichen davor ab, und Nutzer fügen Adressen oft mit Leerzeichen ein. Die Prüfung mit is_string() verhindert, dass eine Anfrage mit email[]=… ein Array liefert, bei dem trim() einen TypeError wirft.
Laut PHP-Handbuch prüft FILTER_VALIDATE_EMAIL die Adresssyntax aus RFC 822, ohne Kommentare, Whitespace-Folding und Domainnamen ohne Punkt. In der Praxis bleiben ein paar Lücken (alle Ergebnisse in der Tabelle in Schritt 2):
- Es akzeptiert Top-Level-Domains mit einem Buchstaben (
jane@example.c) und IP-Adressen in eckigen Klammern (jane@[192.0.2.10]). - Es lehnt internationale Domains in Unicode ab (
jane@bücher.example), solange Sie sie nicht vorher in Punycode umwandeln. - Mit dem Flag
FILTER_FLAG_EMAIL_UNICODEnimmt es auch lokale Teile mit Nicht-ASCII-Zeichen wiejosé@example.coman, die nur Mailserver mit SMTPUTF8-Unterstützung erreichen.
2. Eine strengere Prüfung mit preg_match
Für eine Regel, die Sie selbst in der Hand haben, nutzen Sie das getestete Muster aus unserem englischen Leitfaden zur E-Mail-Regex. normalizeEmail entfernt Leerzeichen am Rand und wandelt eine internationale Domain mit idn_to_ascii() aus der intl-Erweiterung in Punycode um:
<?php // validate.php
const EMAIL_REGEX = '/\A(?=.{1,254}\z)(?=.{1,64}@)[A-Za-z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&\'*+\/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+(?:[A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})\z/';
function isValidEmail(string $email): bool
{
return preg_match(EMAIL_REGEX, $email) === 1;
}
// Entfernt Leerzeichen am Rand und wandelt eine internationale Domain in Punycode um (braucht ext-intl).
function normalizeEmail(string $input): string
{
$email = trim($input);
$at = strrpos($email, '@');
if ($at === false || $at === 0) {
return $email; // lehnt isValidEmail ab
}
$domain = idn_to_ascii(substr($email, $at + 1), IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
return $domain === false ? $email : substr($email, 0, $at + 1) . $domain;
}
Das Muster ist mit \A und \z verankert. Mit ^ und $ akzeptiert preg_match eine Adresse mit nachfolgendem Zeilenumbruch, weil $ auch vor einem abschließenden Newline passt.
Jede Adresse unten lief unter PHP 8.5.12 durch jede Prüfung, die beiden Laravel-Spalten mit illuminate/validation 13.39. ✓ heißt, die Prüfung nimmt die Adresse an, ✗, dass sie sie ablehnt.
| Adresse | Soll bestehen? | filter_var | Regex | normalizeEmail + Regex | Laravel email | Laravel email:strict,filter |
|---|---|---|---|---|---|---|
jane@example.com | Ja | ✓ | ✓ | ✓ | ✓ | ✓ |
jane.doe+news@ | Ja | ✓ | ✓ | ✓ | ✓ | ✓ |
o'brien@ | Ja | ✓ | ✓ | ✓ | ✓ | ✓ |
jane@ | Ja | ✓ | ✓ | ✓ | ✓ | ✓ |
jane@ | Nach IDN-Umwandlung | ✗ | ✗ | ✓ | ✓ | ✗ |
jane | Nein | ✗ | ✗ | ✗ | ✗ | ✗ |
jane..doe@ | Nein | ✗ | ✗ | ✗ | ✗ | ✗ |
jane@example | Nein | ✗ | ✗ | ✗ | ✓ | ✗ |
jane@example.c | Nein | ✓ | ✗ | ✗ | ✓ | ✓ |
jane@-example.com | Nein | ✗ | ✗ | ✗ | ✗ | ✗ |
jane@ | Nein | ✗ | ✗ | ✗ | ✗ | ✗ |
jane doe@example.com | Nein | ✗ | ✗ | ✗ | ✗ | ✗ |
"jane doe"@example.com | Nein (laut RFC gültig) | ✗ | ✗ | ✗ | ✓ | ✗ |
jane@[192 | Nein (laut RFC gültig) | ✓ | ✗ | ✗ | ✓ | ✗ |
josé@example.com | Nein (braucht SMTPUTF8) | ✗ | ✗ | ✗ | ✓ | ✗ |
aaa…a@ (65 a) | Nein | ✗ | ✗ | ✗ | ✓ | ✗ |
jane@example.com mit Leerzeichen am Rand | Nach dem Trimmen | ✗ | ✗ | ✓ | ✗ | ✗ |
jane@example.com + Zeilenumbruch | Nein | ✗ | ✗ | ✓ (getrimmt) | ✗ | ✗ |
normalizeEmail entfernt Leerzeichen und Zeilenumbruch, was Sie bei eingefügten Adressen wollen. Die einfache Laravel-Regel email ist die großzügigste Prüfung in der Tabelle; Schritt 6 zeigt, welche Optionen sie strenger machen.
3. Nimmt die Domain E-Mails an?
Die übliche PHP-Antwort ist checkdnsrr($domain, 'MX'), und sie hat eine Lücke: Sie fragt nur, ob ein MX-Eintrag existiert. Eine Domain, die keine E-Mails empfängt, kann das mit einem Null-MX sagen, einem einzelnen MX-Eintrag mit dem Ziel . (RFC 7505). checkdnsrr('example.com', 'MX') lieferte in unserem Test true, obwohl example.com genau diesen Eintrag veröffentlicht, und getmxrr() gab ihn als einen Host mit leerem Namen zurück. Lesen Sie die Einträge deshalb mit dns_get_record():
<?php // mx.php
// true: die Domain nimmt E-Mails an; false: nicht; null: DNS gab keine klare Antwort.
function acceptsMail(string $domain): ?bool
{
$mx = @dns_get_record($domain, DNS_MX);
if ($mx === false) {
return null; // die Abfrage schlug fehl: unbekannt, nicht ablehnen
}
if ($mx !== []) {
// Ein einzelner MX-Eintrag mit leerem Ziel ist ein Null-MX (RFC 7505): keine E-Mails.
return !(count($mx) === 1 && $mx[0]['target'] === '');
}
// Kein MX-Eintrag: Absender stellen an den A- oder AAAA-Eintrag der Domain zu (impliziter MX).
$addresses = @dns_get_record($domain, DNS_A | DNS_AAAA);
return $addresses === false ? null : $addresses !== [];
}
Gegen das öffentliche DNS:
| Domain | DNS | checkdnsrr($d, 'MX') | acceptsMail |
|---|---|---|---|
gmail.com | Fünf MX-Einträge | true | true |
example.com | Null-MX | true | false |
neverssl.com | A-Eintrag, kein MX | false | true |
| Eine Domain, die nicht existiert | NXDOMAIN | false | false |
dnssec-failed.org | SERVFAIL | false | false |
Die letzte Zeile zeigt eine Grenze der DNS-Funktionen von PHP: Sie nutzen den Resolver des Systems, haben keine Einstellung für Zeitlimits, und dns_get_record() lieferte in unserem Test für ein SERVFAIL dasselbe leere Array wie für eine nicht existierende Domain. false kann also gelegentlich „DNS ist ausgefallen“ bedeuten. Nutzen Sie das Ergebnis, um offensichtliche Tippfehler in der Domain abzufangen, und überlassen Sie die Entscheidung der Postfachprüfung, die eine tote Domain als invalid mit dem Grund invalid_mx meldet. Mit dem MX-Lookup prüfen Sie eine Domain von Hand.
4. Das Postfach per API prüfen
Nur der Mailserver des Empfängers weiß, ob anna@example.org existiert. Ihn selbst zu fragen, braucht ausgehenden Port 25, den die meisten Cloud-Anbieter sperren, und Catch-all-Domains nehmen ohnehin jede Adresse an. Die E-Mail-Validierungs-API führt Syntax-, DNS-, MX- und Postfachprüfung in einer Anfrage aus, ohne eine E-Mail zu senden. Mit der cURL-Erweiterung von PHP:
<?php // check-mailbox.php
// Liefert das Ergebnis der API oder null, wenn die Prüfung nicht möglich war
// (API nicht erreichbar oder langsam, falscher Schlüssel, Kontingent aufgebraucht). Braucht ext-curl.
function checkMailbox(string $email): ?array
{
$key = getenv('EMAILVALIDATION_KEY');
if ($key === false || $key === '') {
throw new RuntimeException('EMAILVALIDATION_KEY is not set');
}
$url = 'https://api.emailvalidation.io/v1/info?' . http_build_query(['email' => $email, 'timeout' => 8]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 10, // die API antwortet innerhalb von 8 Sekunden
CURLOPT_HTTPHEADER => ['apikey: ' . $key],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status !== 200) {
error_log(sprintf('email check skipped: HTTP %d %s', $status, $body === false ? curl_error($ch) : $body));
return null;
}
return json_decode($body, true, flags: JSON_THROW_ON_ERROR);
}
Was der Code erledigt:
- Der Schlüssel kommt aus einer Umgebungsvariablen und steht im Header
apikey, er bleibt also aus Ihrem Repository, Ihren URLs und Ihren Logs heraus. http_build_querykodiert das+injane+news@example.orgals%2B; bei einfacher String-Verkettung käme das Plus als Leerzeichen an.- Zeitlimits: Der API-Parameter
timeout(3 bis 60 Sekunden, Standard 30) begrenzt, wie lange die API auf einen langsamen Mailserver wartet, undCURLOPT_TIMEOUTgibt der Anfrage zwei Sekunden mehr, wie es die Dokumentation (Englisch) empfiehlt. - Fehler: Ein falscher Schlüssel (
401), ein aufgebrauchtes Kontingent oder zu viele parallele Prüfungen (429) und eine Zeitüberschreitung liefernnullund hinterlassen eine Zeile im Fehlerlog. In unserem Test gab der Aufruf bei der Zeitüberschreitung nach 10,0 Sekunden auf. - Kein
curl_close(): Die Funktion bewirkt seit PHP 8.0 nichts mehr und ist in PHP 8.5 als veraltet markiert.
Die Antwort hat 21 Felder. Entscheiden Sie anhand von status:
status | Beispiele für sub_status | recommended_action | Was tun |
|---|---|---|---|
valid | null, role_based, possible_typo | send, send_ | Annehmen; did_you_mean anzeigen, wenn es nicht leer ist. |
invalid | mailbox_, no_dns_entries | suppress | Ablehnen und um Prüfung der Adresse bitten. |
catch_all | accept_all | send_ | Annehmen. Die Domain nimmt jede Adresse an, das Postfach lässt sich nicht bestätigen. |
risky | mailbox_full | retry_later | Annehmen; das Postfach ist gerade voll. |
do_not_mail | disposable | suppress | Nach einer dauerhaften Adresse fragen. |
unknown | timeout_exceeded, greylisted | retry_later | Annehmen. Der Mailserver gab keine klare Antwort. |
Die älteren Felder state und reason liefern dasselbe Urteil in drei Zuständen; die Seite Result Statuses (Englisch) ordnet sie einander zu, und Catch-all-E-Mail-Adressen erklärt, warum sich manche Domains nicht Postfach für Postfach prüfen lassen.
Das offizielle SDK (composer require everapi/emailvalidation-php, auf Basis von Guzzle) sendet dieselbe Anfrage: (new \Emailvalidation\EmailvalidationClient($key, ['timeout' => 10]))->validate($email). In Version 1.0 wirft es bei Netzwerkfehlern und Zeitüberschreitungen eine EmailvalidationException, gibt den Fehlertext einer 401- oder 429-Antwort aber als normales Array zurück. Prüfen Sie also, ob das Ergebnis ein Feld status hat, bevor Sie es verwenden.
5. Alles zusammen in einem Anmeldeskript
<?php // signup.php
require __DIR__ . '/validate.php';
require __DIR__ . '/check-mailbox.php';
$email = normalizeEmail(is_string($_POST['email'] ?? null) ? $_POST['email'] : '');
if (!isValidEmail($email)) {
http_response_code(422);
exit('Bitte geben Sie eine gültige E-Mail-Adresse ein.');
}
$result = checkMailbox($email); // null: Die API konnte nicht antworten, also Adresse annehmen
if (($result['status'] ?? null) === 'invalid') {
http_response_code(422);
exit('Diese E-Mail-Adresse existiert nicht. Bitte prüfen Sie sie.');
}
if (($result['status'] ?? null) === 'do_not_mail') {
http_response_code(422);
exit('Bitte verwenden Sie eine dauerhafte E-Mail-Adresse.');
}
// Konto anlegen ...
http_response_code(201);
echo 'Danke für Ihre Anmeldung.';
Wir haben das Skript mit dem eingebauten Webserver von PHP ausgeliefert, checkMailbox auf eine lokale Nachbildung der API mit dem dokumentierten Antwortformat gerichtet und Formulardaten gesendet:
Gesendetes email | Antwort |
|---|---|
anna@example.org | 201 |
anna@example.org mit Leerzeichen am Rand | 201 |
jane@ | 201, an die API gesendet als jane@ |
jane@example | 422, „Bitte geben Sie eine gültige E-Mail-Adresse ein.“ |
Eine Adresse, die die API als invalid meldet | 422, „Diese E-Mail-Adresse existiert nicht. Bitte prüfen Sie sie.“ |
Eine Adresse, die die API als do_not_mail meldet | 422, „Bitte verwenden Sie eine dauerhafte E-Mail-Adresse.“ |
| Eine Adresse auf einer Catch-all-Domain | 201 |
Kein Feld email oder email[]=… | 422 |
| Beliebige Adresse, während die API 429 antwortet | 201, protokolliert |
Die API-Prüfung lässt im Zweifel absichtlich durch: Ist die API langsam, nicht erreichbar oder das Kontingent aufgebraucht, geht die Anmeldung durch. Ein Ausfall der Validierung soll Sie nie einen Kunden kosten.
6. E-Mail-Validierung in Laravel
Die Laravel-Regel email nimmt eine Liste von Validatoren, die alle bestehen müssen. Die wichtigen:
| Regel | Was sie ergänzt |
|---|---|
email oder email:rfc | RFC-Prüfung (egulias/email-validator). Der Standard. |
email:strict | RFC-Prüfung ohne die Warnungen, die sie sonst toleriert: In unserem Test lehnte sie jane@example, lokale Teile in Anführungszeichen, IP-Adressen in eckigen Klammern und lokale Teile mit 65 Zeichen ab. |
email:filter | PHPs filter_var wie in Schritt 1. |
email:dns | Prüft die MX-Einträge der Domain; sie lehnte example.com (Null-MX) ab und nahm neverssl.com (nur A-Eintrag) an. |
email:spoof | Lehnt Adressen mit irreführenden Unicode-Zeichen ab, die andere Buchstaben nachahmen (braucht die intl-Erweiterung). |
email:strict,filter,dns kombiniert die strenge Formatprüfung mit einer DNS-Prüfung. Um auch das Postfach zu prüfen, ergänzen Sie eine eigene Regel:
<?php // app/Rules/Deliverable.php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class Deliverable implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
$result = Http::withHeaders(['apikey' => config('services.emailvalidation.key')])
->connectTimeout(5)
->timeout(10)
->get('https://api.emailvalidation.io/v1/info', ['email' => $value, 'timeout' => 8])
->throw()
->json();
} catch (ConnectionException|RequestException $e) {
Log::warning('email check skipped: ' . $e->getMessage());
return; // API ausgefallen, langsam oder Kontingent aufgebraucht: Adresse durchlassen
}
match ($result['status'] ?? null) {
'invalid' => $fail('Diese E-Mail-Adresse existiert nicht. Bitte prüfen Sie sie.'),
'do_not_mail' => $fail('Bitte verwenden Sie eine dauerhafte E-Mail-Adresse.'),
default => null,
};
}
}
Legen Sie den Schlüssel als EMAILVALIDATION_KEY in .env ab und lesen Sie ihn in config/services.php aus, damit config:cache weiter funktioniert:
'emailvalidation' => [
'key' => env('EMAILVALIDATION_KEY'),
],
Dann nutzen Sie die Regel in einem Controller oder Form Request. bail stoppt bei der ersten fehlgeschlagenen Regel, eine Adresse mit falschem Format erreicht die API also nie:
$request->validate([
'email' => ['bail', 'required', 'email:strict,filter', new Deliverable],
]);
Wir haben die Regel mit illuminate/validation und illuminate/http 13.39 gegen die Nachbildung ausgeführt: Eine existierende Adresse bestand, Adressen mit invalid und do_not_mail scheiterten mit den beiden Meldungen, eine Catch-all-Adresse bestand, jane@example scheiterte an der Formatprüfung ohne API-Anfrage, und eine API, die 429 antwortete oder länger als 10 Sekunden brauchte, ließ die Adresse mit einer Warnung im Log durch. Die Regel braucht Laravel 10 oder neuer, wo ValidationRule eingeführt wurde.
WordPress: WooCommerce und Gravity Forms
Derselbe API-Aufruf funktioniert in WordPress-Plugins. Für Shops ergänzt die WooCommerce-E-Mail-Prüfung beim Checkout (Englisch) ihn im klassischen Checkout und im Checkout-Block; für Kontakt- und Lead-Formulare zeigt der Gravity-Forms-Filter (Englisch) einen Fehler am E-Mail-Feld, bevor der Eintrag gespeichert wird.
Empfohlener Aufbau
- Bei der Eingabe:
normalizeEmail, dannisValidEmail, oder in Laravelemail:strict,filter. - Optional eine DNS-Prüfung mit
acceptsMailoderemail:dns, um tote Domains ohne API-Aufruf abzufangen. - Beim Absenden: die Postfachprüfung. Blockieren Sie
invalidunddo_not_mail, nehmen Sie alles andere an, und nehmen Sie die Adresse an, wenn die API nicht erreichbar ist. - Für eine bestehende Liste nutzen Sie den Bulk-E-Mail-Validator oder den Bulk-Endpunkt, statt
/v1/infoin einer Schleife aufzurufen: Der kostenlose Tarif erlaubt 10 Anfragen pro Minute und 5 gleichzeitige Prüfungen, bezahlte Tarife 25 gleichzeitig ohne Minutenlimit.
Der kostenlose Tarif enthält 100 Prüfungen im Monat; mehr unter Preise der E-Mail-Validierungs-API. Dieselben Prüfungen in anderen Sprachen: E-Mail-Validierung in JavaScript und E-Mail-Validierung in Python.
Quellen
Geprüft am 29. Oktober 2026: PHP-Handbuch: Filter-Konstanten, checkdnsrr, dns_get_record, idn_to_ascii, curl_close; Laravel-Dokumentation: Validierung, Regel email, eigene Regeln, HTTP-Client; RFC 7505 (Null-MX); API-Dokumentation von emailvalidation.io: Validierungs-Endpunkt, Rate Limits. Code ausgeführt mit PHP 8.5.12 (Erweiterungen cURL und intl), illuminate/validation und illuminate/http 13.39, gegen das öffentliche DNS und eine lokale Nachbildung der API.