MX-Record prüfen mit PHP: getmxrr, Null-MX, Fallback

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

Einen MX-Record prüfst du in PHP mit dns_get_record($domain, DNS_MX) oder getmxrr(). Sortiere die Einträge nach pri, niedrigster Wert zuerst. Ein einzelner Eintrag mit leerem Ziel ist ein Null-MX nach RFC 7505: Die Domain nimmt bewusst keine Mail an. Fehlt der MX ganz, wird laut RFC 5321 an die A- oder AAAA-Adresse zugestellt.

Die meisten MX-Checks, die ich in PHP-Projekten finde, bestehen aus einer Zeile: checkdnsrr($domain, 'MX'). Das stimmt in gut neun von zehn Fällen. Im zehnten gibt es zwei Lücken, die sich gegenseitig ergänzen: Domains ohne MX, die trotzdem Mail annehmen, und Domains mit MX, die ausdrücklich keine Mail wollen.

MX-Record prüfen in PHP: die drei Funktionen

PHP bietet drei Wege, und sie liefern unterschiedlich viel Information:

FunktionRückgabeUnterscheidet Fehler von „kein MX“?
checkdnsrr($d, 'MX')true/falsenein
getmxrr($d, $hosts, $weights)true/false, Arrays per Referenznein
dns_get_record($d, DNS_MX)Array oder falseja

Für eine Ja-Nein-Frage reicht checkdnsrr(). Sobald du wissen willst, wohin Mail geht, oder Resolver-Ausfälle nicht als „kein MX“ werten willst, nimmst du dns_get_record(). Die Details zum Rückgabeformat stehen im Beitrag zu dns_get_record in PHP.

Null-MX: die Domain, die keine Mail will

RFC 7505 definiert einen Null-MX: genau ein MX-Record mit Priorität 0 und dem Ziel ., also der DNS-Wurzel. Er sagt Absendern: Hier wird nie Mail angenommen, versuch es gar nicht erst. example.com hat genau so einen Eintrag. PHP gibt ihn so zurück:

array(1) {
  [0]=>
  array(6) {
    ["host"]=>
    string(11) "example.com"
    ["class"]=>
    string(2) "IN"
    ["ttl"]=>
    int(444)
    ["type"]=>
    string(2) "MX"
    ["pri"]=>
    int(0)
    ["target"]=>
    string(0) ""
  }
}

Das Ziel ist ein leerer String, nicht .. Und getmxrr('example.com', $h, $w) gibt true zurück, mit $h = [""]. Ein Check, der nur auf „mindestens ein MX vorhanden“ prüft, hält example.com also für eine funktionierende Maildomain.

Kein MX: der Fallback auf A und AAAA

Der umgekehrte Fall steht in RFC 5321, Abschnitt 5.1: Gibt es keinen MX-Record, behandelt der Absender den Domainnamen selbst als impliziten MX mit Priorität 0 und stellt an dessen A- oder AAAA-Adresse zu. Das ist alt, aber gültiges Verhalten. Ob ein Webserver ohne Mailserver dahinter die Mail annimmt, ist eine andere Frage, formal ist die Domain aber erreichbar.

Für die Praxis heißt das: Eine Domain ohne MX, aber mit A-Record, solltest du in einem Anmeldeformular nicht hart ablehnen. Markiere sie lieber intern als „ungewöhnlich“ und verlass dich auf die Bestätigungsmail. Kommt die nicht an, hat sich die Frage von selbst erledigt. Hart ablehnen kannst du dagegen Domains mit Null-MX und solche, die weder MX noch Adresse haben.

Eine vollständige Funktion mit vier Ergebnissen

Die folgende Funktion bildet alle Fälle ab und gibt sie als Enum zurück. Ein Resolver-Fehler wird zur Exception, weil „wissen wir nicht“ nicht dasselbe ist wie „gibt es nicht“.

<?php
declare(strict_types=1);

enum MailRouting: string
{
    case Mx       = 'mx';        // reguläre MX-Records
    case Implicit = 'implicit';  // kein MX, aber A/AAAA (RFC 5321, Abschnitt 5.1)
    case NullMx   = 'null-mx';   // "MX 0 ." nach RFC 7505: nimmt bewusst keine Mail an
    case None     = 'none';      // weder MX noch Adresse
}

/** @return array{routing: MailRouting, hosts: list<string>} */
function mailRouting(string $domain): array
{
    $ascii = idn_to_ascii($domain, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
    if ($ascii === false) {
        throw new InvalidArgumentException("Ungültige Domain: $domain");
    }

    $mx = @dns_get_record($ascii, DNS_MX);
    if ($mx === false) {
        throw new RuntimeException("DNS-Fehler bei $ascii, Ergebnis unbekannt");
    }

    if ($mx !== []) {
        usort($mx, fn(array $a, array $b) => $a['pri'] <=> $b['pri']);
        $hosts = array_column($mx, 'target');
        if (count($hosts) === 1 && ($hosts[0] === '' || $hosts[0] === '.')) {
            return ['routing' => MailRouting::NullMx, 'hosts' => []];
        }
        return ['routing' => MailRouting::Mx, 'hosts' => $hosts];
    }

    if (checkdnsrr($ascii, 'A') || checkdnsrr($ascii, 'AAAA')) {
        return ['routing' => MailRouting::Implicit, 'hosts' => [$ascii]];
    }
    return ['routing' => MailRouting::None, 'hosts' => []];
}

foreach (['php.net', 'denic.de', 'example.com', 'gibt-es-sicher-nicht-4711.de', 'bücher.de', 'github.com'] as $d) {
    $r = mailRouting($d);
    printf("%-30s %-9s %s\n", $d, $r['routing']->value, implode(', ', $r['hosts']));
}

Ausgeführt mit PHP 8.5:

php.net                        mx        php-smtp4-ip4.php.net
denic.de                       mx        mxext4.mailbox.org, mxext1.mailbox.org, mxext2.mailbox.org, mxext3.mailbox.org
example.com                    null-mx
gibt-es-sicher-nicht-4711.de   none
bücher.de                      implicit  xn--bcher-kva.de
github.com                     mx        github-com.mail.protection.outlook.com

Zwei Dinge fallen auf. bücher.de hat keinen MX, aber eine Adresse, landet also im Fallback. Und bei denic.de haben alle vier Server die Priorität 10. usort() ist bei gleichen Werten nicht an eine Reihenfolge gebunden, deshalb stehen sie nicht alphabetisch. Das ist korrekt: Gleiche Priorität heißt, der Absender wählt zufällig, und genau so ist es gedacht.

Prüfen, ob die MX-Ziele auflösen

Ein MX-Record ist nur so gut wie der Hostname, auf den er zeigt. Nach einem Anbieterwechsel bleibt gern ein Eintrag stehen, dessen Ziel längst gelöscht ist. Absender versuchen es dann der Reihe nach und verlieren Zeit, bevor sie beim funktionierenden Server ankommen. Die folgende Ergänzung prüft jedes Ziel auf eine A- oder AAAA-Adresse:

<?php
declare(strict_types=1);

/** @param list<string> $hosts @return array<string, bool> */
function resolvableTargets(array $hosts): array
{
    $result = [];
    foreach ($hosts as $host) {
        $result[$host] = checkdnsrr($host, 'A') || checkdnsrr($host, 'AAAA');
    }
    return $result;
}

print_r(resolvableTargets(mailRouting('denic.de')['hosts']));

Für denic.de lieferte das vier Mal true. Ein false in der Liste ist ein Fall fürs Ticket, nicht für eine Ablehnung im Formular: Die Domain kann über die übrigen Server trotzdem erreichbar sein.

Die Priorität verdient einen Satz. Niedrigere Werte werden zuerst versucht, und Einträge mit gleichem Wert teilen sich die Last zufällig. Ein Backup-MX mit Priorität 20 springt erst ein, wenn alle Server mit 10 nicht antworten. Wer den Backup-Server höher priorisiert als den Hauptserver, merkt das oft erst an Mails, die über Umwege und mit Verzögerung ankommen.

Was ein MX-Check nicht beweist

Ein vorhandener MX sagt nichts darüber, ob [email protected] existiert. Manche Entwickler gehen deshalb einen Schritt weiter, bauen eine SMTP-Verbindung zum Mailserver auf und probieren RCPT TO. Davon rate ich ab. Viele Server antworten auf jede Adresse mit 250, andere führen Greylisting durch, und wer das massenhaft macht, landet schnell auf Sperrlisten, weil das Muster dem von Spammern gleicht.

Für Formulare ist die Kombination aus Syntaxprüfung, MX-Check mit Null-MX-Erkennung und Bestätigungsmail das Maximum, das sinnvoll ist. Wie das in eine E-Mail-Validierung passt, zeigt der Beitrag zur E-Mail-Validierung in PHP.

MX-Records beim Umzug im Blick behalten

MX-Probleme entstehen selten beim Einrichten, sondern bei Änderungen: Wechsel des Mailanbieters, Umzug der Domain, aufgeräumte DNS-Zone. Ein MX-Check im regelmäßigen DNS-Monitoring mit PHP meldet so etwas, bevor die ersten Mails zurückkommen. Prüfe dabei auch, ob die MX-Ziele selbst auflösen. Ein MX, der auf einen gelöschten Hostnamen zeigt, ist schlimmer als gar keiner, weil Absender dann erst nach Stunden aufgeben.

Ein häufiger Fehler bei der Einrichtung ist übrigens ein MX, der auf eine IP-Adresse statt auf einen Hostnamen zeigt. Das ist nach RFC 5321 nicht zulässig, und manche Absender verweigern dann die Zustellung.

Du richtest gerade eine eigene Maildomain ein? Bei betadomain verwaltest du MX und alle anderen Records selbst im Portal. Prüf deinen Wunschnamen, sieh dir die Jahrespreise aller Endungen an oder nimm direkt eine .email-Domain für 29,99 € im Jahr.

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

Häufige Fragen

Was ist der Unterschied zwischen getmxrr und dns_get_record?

getmxrr() füllt zwei Arrays mit Hostnamen und Gewichten und gibt true oder false zurück. dns_get_record() liefert vollständige Records inklusive TTL und unterscheidet zwischen keinem Ergebnis und einem Resolver-Fehler. Für neuen Code ist dns_get_record() die bessere Wahl.

Reicht ein vorhandener MX-Record, damit eine E-Mail-Adresse existiert?

Nein. Der MX-Record zeigt nur, dass die Domain Mail empfangen kann. Ob das Postfach existiert, weiß nur der Mailserver, und viele Server verraten das aus Spamschutzgründen nicht. Eine Bestätigungsmail ist der einzige verlässliche Test.

Wie sieht ein Null-MX in PHP aus?

Im DNS steht „MX 0 .“. dns_get_record() liefert dafür einen Eintrag mit pri 0 und target als leeren String, getmxrr() gibt true und einen leeren Hostnamen zurück. Ein Test auf ein nicht leeres Array reicht deshalb nicht.

Sollte ich bei fehlendem MX die Adresse ablehnen?

Nicht automatisch. Nach RFC 5321 darf Mail an den A-Record zugestellt werden, wenn kein MX existiert. In der Praxis ist das selten gewollt, aber eine Ablehnung ist strenger als der Standard. Ablehnen solltest du, wenn weder MX noch A noch AAAA existieren.