E-Mail-Validierung in PHP: filter_var, MX und Umlaute

Aktualisiert: 5 Min. Lesezeit Redaktion The PHP Experts
Kurz gesagt

Für die E-Mail-Validierung in PHP nimmst du filter_var($email, FILTER_VALIDATE_EMAIL) für die Syntax, wandelst vorher den Domainteil mit idn_to_ascii() in Punycode um und prüfst per DNS, ob die Domain Mail annimmt, also MX oder A-Record ohne Null-MX. Ob das Postfach existiert, zeigt nur eine Bestätigungsmail.

Fast jede PHP-Anwendung hat ein E-Mail-Feld, und fast jede prüft es anders. Ich habe Regexe gesehen, die [email protected] ablehnen, Prüfungen, die .museum für zu lang hielten, und Formulare, die jede Adresse mit Umlaut-Domain als ungültig zurückweisen. Dabei ist eine solide Prüfung in PHP schnell gebaut, wenn man weiß, wo die eingebauten Funktionen aufhören.

E-Mail-Validierung in PHP: was filter_var leistet

filter_var($email, FILTER_VALIDATE_EMAIL) prüft die Syntax einer Adresse und gibt sie zurück oder false. Das deckt die realen Adressen der allermeisten Nutzer ab. Es gibt aber Grenzen, die man kennen sollte. Hier zehn Testfälle unter PHP 8.5, links filter_var() pur, rechts die erweiterte Funktion aus dem nächsten Abschnitt:

Eingabefilter_var()validateEmail()
[email protected]gültiggültig
[email protected]gültiggültig
jörg@müller.deungültiggültig, als jö[email protected]
user@localhostungültigabgelehnt
a@bungültigabgelehnt
[email protected]gültigabgelehnt (Domain existiert nicht)
"john doe"@example.comungültigabgelehnt
ü[email protected]ungültigabgelehnt (Null-MX)
[email protected]ungültigabgelehnt
[email protected].ungültigabgelehnt

Zwei Zeilen sind besonders lehrreich. [email protected] ist syntaktisch einwandfrei, die Domain existiert aber nicht. Und example.com hat einen Null-MX, nimmt also ausdrücklich keine Mail an. Beides erkennt filter_var() nicht, weil es nur auf die Zeichen schaut.

Umlaut-Domains und der Punycode-Schritt

filter_var() kennt keine internationalisierten Domains. jörg@müller.de fällt durch, obwohl die Domain existiert und Mail empfangen kann. Die Lösung: Domainteil abtrennen, mit idn_to_ascii() umwandeln, dann prüfen. Das Flag FILTER_FLAG_EMAIL_UNICODE erlaubt zusätzlich UTF-8 im lokalen Teil vor dem @.

Wichtig ist, am letzten @ zu trennen, nicht am ersten. Lokale Teile in Anführungszeichen dürfen ein @ enthalten, die Domain nie. Die Details zur Umwandlung, inklusive der Falle mit ß, stehen im Beitrag zu Punycode in PHP.

Die vollständige Prüffunktion mit DNS-Check

<?php
declare(strict_types=1);

function acceptsMail(string $asciiDomain): bool
{
    $mx = @dns_get_record($asciiDomain, DNS_MX);
    if ($mx === false) {
        return true; // DNS-Störung: im Zweifel nicht ablehnen
    }
    if ($mx !== []) {
        // Null MX (RFC 7505): genau ein Eintrag mit leerem Ziel
        return (count($mx) === 1 && $mx[0]['target'] === '') === false;
    }
    return checkdnsrr($asciiDomain, 'A') || checkdnsrr($asciiDomain, 'AAAA');
}

function validateEmail(string $input, bool $checkDns = true): ?string
{
    $email = trim($input);
    $at    = strrpos($email, '@');
    if ($at === false || strlen($email) > 254) {
        return null;
    }
    $local  = substr($email, 0, $at);
    $domain = substr($email, $at + 1);

    // Domainteil nach Punycode, damit auch jörg@müller.de geprüft werden kann
    $asciiDomain = idn_to_ascii($domain, IDNA_NONTRANSITIONAL_TO_ASCII | IDNA_USE_STD3_RULES, INTL_IDNA_VARIANT_UTS46);
    if ($asciiDomain === false) {
        return null;
    }
    $normalized = $local . '@' . $asciiDomain;

    if (filter_var($normalized, FILTER_VALIDATE_EMAIL, FILTER_FLAG_EMAIL_UNICODE) === false) {
        return null;
    }
    if ($checkDns && acceptsMail($asciiDomain) === false) {
        return null;
    }
    return $normalized;
}

Die Funktion gibt die normalisierte Adresse zurück statt nur true. So speicherst du immer dieselbe Schreibweise der Domain und vermeidest doppelte Konten für jörg@müller.de und jö[email protected].

Bei einem DNS-Fehler lässt acceptsMail() die Adresse bewusst durch. Ein kurzer Resolver-Ausfall soll keine Anmeldungen blockieren, die Bestätigungsmail fängt echte Fehler später ab. Wenn du lieber streng bist, gib dort false zurück. Warum der Null-MX-Check nötig ist und wie der Fallback auf A-Records funktioniert, erklärt der Artikel zum MX-Record prüfen mit PHP.

Strenger prüfen mit egulias/email-validator

Wenn du die RFCs genauer abbilden willst, ist egulias/email-validator das Standardpaket. Laravel und Symfony bauen ihre E-Mail-Regeln darauf auf. Es kennt mehrere Prüfstufen, die du kombinieren kannst:

<?php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Egulias\EmailValidator\EmailValidator;
use Egulias\EmailValidator\Validation\DNSCheckValidation;
use Egulias\EmailValidator\Validation\MultipleValidationWithAnd;
use Egulias\EmailValidator\Validation\RFCValidation;

$validator = new EmailValidator();
$rules     = new MultipleValidationWithAnd([new RFCValidation(), new DNSCheckValidation()]);

foreach (['[email protected]', 'jörg@müller.de', '"john doe"@php.net', '[email protected]', '[email protected]'] as $email) {
    $ok = $validator->isValid($email, $rules);
    printf("%-34s %s\n", $email, $ok ? 'gültig' : 'ungültig: ' . str_replace("\n", ' | ', trim((string) $validator->getError()?->description())));
}

Mit Version 4.0.4 kam heraus:

[email protected]                       gültig
jörg@müller.de                     gültig
"john doe"@php.net                 gültig
[email protected]  ungültig: No MX or A DNS record was found for this email
[email protected]                   ungültig: No MX or A DNS record was found for this email | Domain accepts no mail (Null MX, RFC7505)

Zwei Unterschiede zu filter_var() fallen auf: Das Paket akzeptiert Umlaut-Domains ohne eigene Umwandlung und erlaubt den lokalen Teil in Anführungszeichen mit Leerzeichen, der nach RFC 5322 gültig ist. Ob du solche Adressen wirklich zulassen willst, ist eine Produktentscheidung. Manche Systeme im weiteren Ablauf, etwa CRM- oder Newsletter-Tools, kommen damit nicht klar.

Was keine Validierung prüfen kann

Keine dieser Prüfungen sagt dir, ob das Postfach existiert oder der Nutzer es liest. Tippfehler wie gmial.com gehen durch, wenn die Tippfehler-Domain selbst Mail annimmt, und solche Domains gibt es. Was bleibt:

  • Bestätigungsmail mit Link, das ist der einzige echte Beweis.
  • Optional ein Hinweis bei bekannten Vertippern der großen Anbieter („Meintest du gmail.com?“), ohne die Eingabe abzulehnen.
  • Keine SMTP-Probe mit RCPT TO. Sie ist unzuverlässig und sieht für Mailserver aus wie Adresssammeln.

Bei der Länge gibt es übrigens eine feste Grenze: 254 Zeichen für die ganze Adresse, 64 für den lokalen Teil. Die erste prüft die Funktion oben selbst, die zweite erledigt filter_var().

Adressen normalisieren, ohne sie zu verfälschen

Nach der Validierung kommt die Frage, was du speicherst. Der Domainteil ist nach RFC 4343 unabhängig von Groß- und Kleinschreibung, den kannst du gefahrlos kleinschreiben. Der lokale Teil ist laut RFC 5321 dagegen streng genommen case-sensitiv. Praktisch behandelt kaum ein Mailserver Max@ und max@ unterschiedlich, für die Zustellung solltest du die Schreibweise des Nutzers trotzdem unverändert lassen. Für Vergleiche, etwa beim Login oder bei der Dublettenprüfung, eignet sich eine zweite Spalte mit mb_strtolower() der ganzen Adresse.

Finger weg von weitergehenden „Bereinigungen“. Punkte im lokalen Teil zu entfernen oder alles ab einem + abzuschneiden, funktioniert bei einzelnen Anbietern, bei anderen sind max.mustermann@ und maxmustermann@ zwei verschiedene Personen. Plus-Adressen sind außerdem ein bewusst genutztes Werkzeug: Wer sich mit max+shop@ anmeldet, will sehen, woher Mails kommen. Ein Formular, das das Pluszeichen ablehnt, verärgert genau die Nutzer, die sich am meisten Gedanken über ihre Adresse machen.

Validierung an der richtigen Stelle

Die Syntaxprüfung gehört in jedes Formular, der DNS-Check eher in den Server-Request nach dem Absenden, weil dns_get_record() keinen Timeout kennt. Die gleiche Logik für den Domainteil kannst du für reine Domain-Felder wiederverwenden; dafür gibt es den Beitrag Domain validieren mit Regex.

Und falls du gerade eine eigene Maildomain für dein Projekt suchst: In der Suche von betadomain siehst du sofort, ob der Name frei ist. Eine .email-Endung kostet 29,99 € im Jahr, die klassische .de-Domain 2,99 €.

Domain gefunden? Prüfen und registrieren kannst du sie direkt bei betadomain – Jahrespreis ab 2,99 €.

Häufige Fragen

Ist filter_var mit FILTER_VALIDATE_EMAIL zuverlässig?

Für die allermeisten realen Adressen ja. Es lehnt einige exotische, aber gültige Formen ab, etwa lokale Teile in Anführungszeichen mit Leerzeichen, und kennt ohne Vorarbeit keine Umlaut-Domains. Für Anmeldeformulare ist das meist ein vertretbarer Kompromiss.

Sollte ich per SMTP prüfen, ob ein Postfach existiert?

Nein. Viele Mailserver akzeptieren beim RCPT TO jede Adresse oder arbeiten mit Greylisting, das Ergebnis ist also unzuverlässig. Wer das in großer Zahl macht, landet außerdem schnell auf Sperrlisten. Eine Bestätigungsmail mit Link ist der einzige saubere Weg.

Dürfen E-Mail-Adressen Umlaute enthalten?

In der Domain ja, sie wird dann per Punycode übertragen. Im lokalen Teil vor dem @ erlaubt RFC 6531 UTF-8, aber nur, wenn alle beteiligten Mailserver SMTPUTF8 unterstützen. In der Praxis ist das bei großen Anbietern üblich, bei kleinen Servern nicht garantiert.

Was ist egulias/email-validator?

Ein Composer-Paket, das Adressen streng nach den RFCs prüft und optional DNS-Checks durchführt. Es erkennt unter anderem Null-MX nach RFC 7505 und liefert verständliche Fehlerbeschreibungen. Laravel und Symfony nutzen es für ihre E-Mail-Validierung.