To main content

Schlechte Idee: E-Mailadressen mit regex prüfen

Veröffentlicht von Benjamin Marwell am

Forbidden mail addressViele Internetseiten erfordern als Log-In die E-Mailadresse. Was passiert aber, wenn man seine elektronische Postadresse nicht in das Registrierungsfeld eingeben kann? Dann hat jemand wohl die RFC 2822 nicht gelesen. Denn viele Seiten lassen eigentlich erlaubte Sonderzeichen nicht zu.

Schuld daran könnte sein, dass diese Seiten die E-Mailadressen mit regex prüfen - das geht üblicherweise nicht gut. Reguläre Ausdrücke (kurz: regex) decken viele Anwendungsfälle ab - mit aber nur einem Ausdruck eine E-Mailadresse auf Konformität zu prüfen gehört aber meiner Meinung nicht dazu.

Beispiele für gültige Adressen

Plus-Adressen

Wie im Artikel Sichere E-Mailadressen für Facebook geschrieben, lassen sich Adressen von Google Mail mit einem Plus versehen. Das sind gültige Adressen nach RFC 2822.

  • user+example@gmail.com

  • my-address+extension@web.de

  • no-name-adress+suffix@gmx.de

Die letzten Beiden E-Mailadressen gehören aber nicht zum Konto my-adress@web.de oder no-name-adress@gmx.de. GMX und Web.de unterstützen diese Plus-Adressierung nicht. Ein anderer Anbieter, der diese Funktion unterstützt, ist etwa fastmail.fm.

Local Part in E-Mails

Der Teil vor dem @-Zeichen (»Klammeraffe«) kann nahezu vollständig beliebig gewählt werden. Es können sogar Zeichen verwendet werden, an die man zunächst gar nicht denkt - etwa das @-Zeichen selbst! Wichtig ist nur, wie man es einfügt.

Laut Wikipedia sind auch andere Zeichen gültig. Zu den erlaubten Zeichen in E-Mailadressen gehören etwa:

  • Ausrufezeichen (!)

  • Apostroph-Ersatzzeichen (')

  • Fragezeichen (?)

  • Gleichheitszeichen (=)

  • Geschweifte Klammern ({ })

  • Zirkumflex (^)

  • Dollar-Währungssymbol ($)

  • Sternchen (*)

  • Prozent (%)

  • Kaufmanns-Und/Ampersand (&)

  • Maskierte Leerzeichen (\ oder " ")

  • Maskierte Anführungszeichen (\")

  • Maskierter Klammeraffe/At-Zeichen (\@ oder "@")

  • Maskiertes Komma (\, oder ",")

Ein regulärer Ausdruck müsste also den Local Part vom Rest trennen und separat prüfen - das ist außerordentlich schwierig. Hinzu kommt noch, dass Kommentare in Klammern erlaubt sind, in denen wiederum weitere Zeichen verwendet werden dürfen. Der reguläre Ausdruck bläht sich hiermit noch weiter auf. Kommen noch maskierte Zeichen hinzu, etwa durch den Backslash oder durch Anführungszeichen, so ist der Reguläre Ausdruck alleine für den Local Part fast unmöglich zu erzeugen.

Reguläre Ausdrücke

Schlechte, übliche Versuche

Ein sehr schlechter regulärer Ausdruck zum Prüfen einer E-Mailadresse wäre etwa folgender (bitte nicht nutzen!):

^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$

Warum ist dieser Ausdruck schlecht? Ganz einfach: Dieser reguläre Ausdruck lässt keine Plus-Zeichen zu, aber auch viele andere eigentlich erlaubte Zeichen nicht - siehe oben. Außerdem erlaubt sie ungültige Domainnamen, wie etwa example..com (man beachte die beiden Punkte).

Variante von DevShed

Eine weitere Variante in PHP sieht vielleicht so aus (Quelle: http://www.devshed.com/c/a/PHP/Email-Address-Verification-with-PHP/2):

function checkEmail($email) {
  if (preg_match("/^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/", $email)) {
    list($username,$domain)=split('@',$email);

    if(!checkdnsrr($domain,'MX')) {
      return false;
    }

    return true;
  }

  return false;
}

Auch diese Variante begeht größere Fehler:

  1. Es werden abermals erlaubte Zeichen nicht zugelassen, etwa das Prozent (%).

  2. Die Unterteilung in User und Mail ist zwar an sich nicht dumm, aber die Routine scheitert an einem maskierten (und erlaubten!) \@ im Usernamen.

  3. Die DNS-Prüfung klingt zunächst sinnvoll - aber eine gültige Absender-Domain muss nicht zwingend ihren MX-Record veröffentlichen.

Interessanterweise erhielt diese Variante trotzdem viele 4-Sterne-Bewertungen. Es soll also kein Affront gegen den Autor dieses Codes sein. Trotzdem auch diese Variante bitte nicht nutzen.

Beispiele für Webseiten mit schlechten E-Mail-Prüfungen

Viele Webseiten verwenden solche falschen Validatoren. Ein paar Beispiele möchte ich hier auflisten (Stand: 2013). Dabei soll das kein Angriff auf die Seitenbetreiber sein - ich möchte lediglich darstellen, dass diese Seiten nicht alle theoretisch gültigen Adressen annehmen.

GMail

Bei Google’s E-Maildienst GMail lassen sich E-Mails nicht an alle gültigen Adressen verschicken, wie auf dem Bildschirmfoto zu sehen ist. Dabei fallen etwa Kommentare, Anführungszeichen und maskierte Klammeraffen (@) unter den Tisch. Gültig ist aber eine Mail à la user+word%tag@gmail.com.

Facebook

Facebook unterstützt die Angabe von Plus-Zeichen, so dass man etwa bei Google Mail ein eigenes Schlagwort reservieren kann (user+facebook@gmail.com). Bei Prozentzeichen macht Facebook aber nicht mit und lehnt diese als ungültig ab.

Piwik

Auch bei der Open-Source-Self-Hosting-Webanalyselösung Piwik ist nicht alles machbar. Ähnlich wie bei Facebook und Google sind zwar +-Zeichen erlaubt, aber auch hier verschluckt sich der Filter am Prozent-Zeichen (%).

Deutsche Post E-Filiale

Man könnte und sollte meinen, ein Unternehmen, welche schon Deutsch Post heißt, kennt sich auch damit besonders gut aus. Leider ist genau das Gegenteil der Fall. Die Deutsche Post schneidet schlechter ab, als viele anderen Dienstleister. Hier wird sogar das Plus-Zeichen verboten, so dass ein einfaches Setzen von Filtern in z.B. GMail nicht möglich ist.

Am 8. Februar 2013 habe ich die Post per Mail darauf aufmerksam gemacht, dass die E-Mailprüfung gültige E-Mailadressen ausschließe. Leider erhielt ich als Antwort ein schwaches "wir kümmern uns". Seitdem habe ich nichts mehr seitens der Post gehört. Ein echtes Ärgernis!

Courier & Sendmail

Interessanterweise konnte ich das Senden per Terminal nur schwerlich testen. Für die Mailweiterleitung per .courier-Dateien habe ich entsprechende Weiterleitungen angelegt. Zurück kam immer ein Fehler bei Senden. Ob Courier also RFC 2822-Konform ist, kann ich durch meinen Test nicht sagen. Aber das Tool "mail" in seinen zahlreichen Variationen (z.B. GNU mailutils) ist es sicherlich nicht.

Beispiele für bessere Mail-Prüfungen

Wordpress.org

Wer ein selbstinstalliertes (selbstgehostetes) Wordpress-System nutzt, darf sich freuen: Jeder Benutzer kann hier E-Mailadressen mit z.B. Prozentzeichen und Plus hinterlegen. Allerdings werden auch hier eigentlich erlaubte Adressen, wie "Benutzer mit Leerzeichen"@gmail.com nicht zugelassen.

Gallerie

facebook nopercent
Figure 1. Facebook akzeptiert kein Prozentzeichen
mail do gmail
Figure 2. In Gmail würde das Prozentzeichen prinzipiell funktionieren
mail not gmail
Figure 3. … aber maskierte @-Zeichen mag GMail auch nicht.
terminal courier
Figure 4. Auch in Piwik lässt sich nicht einmal das Prozentzeichen angeben.
post mail plus fail
Figure 5. Fail: Nichtmal die Post weiß, wie E-Mailadressen funktionieren. Peinlich!
piwik nopercent mail
Figure 6. Auch die GNU Mailutils scheitern beim Parsen der Adresse.
wordpress works
Figure 7. Gut: In Wordpress lassen sich immerhin Prozentzeichen und Pluszeichen angeben.
dropbox space
Figure 8. Dropbox nutzt Kicksends Mailcheck und scheitert am Leerzeichen.
dropbox at
Figure 9. Dropbox nutzt Kicksends Mailcheck und scheitert am maskierten @.

Wann ist eine E-Mail gültig?

Anforderungen aus RFC 2822

Folgende Anforderungen ergeben sich aus der RFC 2822:

  1. Eine E-Mailadresse besteht aus dem Local Part und der Domain, durch ein @-Zeichen separiert (RFC 2822 3.4.1).

  2. Der Local Part besteht aus Buchstaben, Zahlen und den oben angegebenen Zeichen, mit Punkten (.) als Trennzeichen, aber ohne Punkt am Anfang, am Ende, oder zwei nebeneinander (RFC 2822 3.2.4).

  3. Der Local Part darf darf aus einem Text in Anführungszeichen bestehen. Das bedeutet, alle möglichen Zeichen - auch Leerzeichen - dürfen in doppelten Anführungszeichen (") eingeschlossen werden (RFC 2822 3.2.5).

  4. Maskierte Paare (etwa \@) sind gültige Komponenten des Local Parts einer E-Mailadresse. Sie sind als Altlast aus RFC 822 mit aufgenommen worden (RFC 2822 4.4).

  5. Der Local Part darf 64 Zeichen nicht überschreiten (RFC 2821 4.5.3.1).

  6. Eine Domain besteht aus Bezeichnern, die über Punkte getrennt werden (RFC 1035 2.3.1).

  7. Domainbeezeichner fangen mit einem alphanumerischen Zeichen an, gefolgt von null oder oder mehr alphabetischen, numerischen Zeichen, oder dem Bindestrich (-). Sie enden alphanumerisch (RFC 1035 2.3.1).

  8. Ein einziges Label innerhalb der Domain darf 63 Zeichen nicht überschreiten (RFC 1035 2.3.1).

  9. Die gesamtlänge der Domain darf 255 Zeichen nicht überschreiben (RFC 2821 4.5.3.1).

  10. Die Domain muss vollqualifiziert sein, und auf einen DNS-Ressource Record des Typs A oder MX auflösbar sein (RFC 2821 3.6).

Ein besserer E-Mail-Validator

Aus den oben genannten 10 Punkten lässt sich ein besserer E-Mailvalidator basteln. Eine erste Version kommt vom Linux-Journal, 2007. Aus dem bereitgestellten Code habe ich Funktionen extrahiert, sie auf Basis der dortigen Kommentare erweitert und als eine Klasse neu zusammengestellt. Die Prüfung über DNS-Einträge berücksichtigt nun auch IPv6, und die Domain wird erst auf prinzipielle Gültigkeit geprüft, bevor ein DNS-Check erfolgt. Das spart sehr viele Ressourcen. Download beider Dateien (Klasse mit statischer Methode und Testcase): mailcheck.tar.bz2.

Hier nocheinmal der Quellcode ausgeschrieben:

Mailcheck.php

<?php

class MailChecker
{
    private static function mail_check_local($local)
    {
    $local_errors = 0;
    $localLen = strlen($local);

    if ($localLen &lt; 1 || $localLen &gt; 64)
    {
        // local part length exceeded
        $local_errors |= 1;
    }
    else if ($local[0] == '.' || $local[$localLen-1] == '.')
    {
        // local part starts or ends with '.'
        $local_errors |= 2;
    }
    else if (strpos($local, '..'))
    {
        // local part has two consecutive dots
        $local_errors |= 4;
    }

    return $local_errors;
    }

    private static function mail_check_domain($domain)
    {
    $domain_errors = 0;
    $domainLen = strlen($domain);

    if ($domainLen &lt; 1 || $domainLen &gt; 255)
    {
        // domain part length exceeded
        $domain_errors |= 8;
    }
    else if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain))
    {
        // character not valid in domain part
        $domain_errors |= 16;
    }
    else if (strpos($domain, ',,'))
    {
        // domain part has two consecutive dots
        $domain_errors |= 32;
    }
    else if (!strpos($domain, '.'))
    {
        // there is no dot at all?
        $domain_errors |= 64;
    }

    return $domain_errors;
    }

    private static function mail_check_quoted($local)
    {
    $quoted_errors = 0;

    if (
        !preg_match('/^(\\\\.|[A-Za-z0-9!#%&amp;`_=\\/$\'*+?^{}|~.-])+$/',
        str_replace("\\\\","",$local))
    )
    {
        // character not valid in local part unless
        // local part is quoted
        if (!preg_match('/^"(\\\\"|[^"])+"$/',
        str_replace("\\\\","",$local)))
        {
        $quoted_errors |= 128;
        }
    }

    return $quoted_errors;
    }

    private static function mail_check_dns($domain)
    {
    $domain_errors = 0;

    if (
        !(
        checkdnsrr($domain, "MX") ||
        checkdnsrr($domain, "A") ||
        checkdnsrr($domain, "AAAA")
        )
    )
    {
        // domain not found in DNS
        $domain_errors |= 256;
    }

    return $domain_errors;
    }

    /*
     * Validate an email adress.
     * Provide an email adress by raw input.
     * Returns 0 (true) if email adress has an
     * RFC 2822 / 2821 / 822 conform format
     * and the domain does exist with
     * either A, AAAA or MX records.
     */
    public static function isValid($email)
    {
    $mail_errors = 0;
    $atIndex = strrpos($email, "@");

    if (is_bool($atIndex) &amp;&amp; !$atIndex)
    {
        // no "@"? Return false immediatly.
        return false;
    }

    $domain = substr($email, $atIndex+1);
    $local = substr($email, 0, $atIndex);

    // 1.) Check local part for common errors
    $mail_errors |= self::mail_check_local($local);
    // 2.) Check domain for common errors
    $mail_errors |= self::mail_check_domain($domain);
    // 3.) Check local part for quotation errors
    $mail_errors |= self::mail_check_quoted($local);
    // 4.) Check, if domain exists.
    if ($mail_errors == 0)
    {
        $mail_errors |= self::mail_check_dns($domain);
    }

    return $mail_errors;
    }

}
?>

mailchecktest.php

<?php
require("mailcheck.php"); // your favorite here

function testEmail($email)
{
  echo $email;
  $pass = MailChecker::isValid($email);
  if ($pass == 0)
  {
    echo " is valid.\n";
  }
  else
  {
    echo " is not valid because of $pass\n";
  }

  return $pass;
}

$pass = 0;

echo "All of these should succeed:\n";
$pass |= testEmail("dclo@us.ibm.com");
$pass |= testEmail("abc\\@def@example.com");
$pass |= testEmail("abc\\\\@example.com");
$pass |= testEmail("Fred\\ Bloggs@example.com");
$pass |= testEmail("Joe.\\\\Blow@example.com");
$pass |= testEmail("\"Abc@def\"@example.com");
$pass |= testEmail("\"Fred Bloggs\"@example.com");
$pass |= testEmail("customer/department=shipping@example.com");
$pass |= testEmail("\$A12345@example.com");
$pass |= testEmail("!def!xyz%abc@example.com");
$pass |= testEmail("_somename@example.com");
$pass |= testEmail("user+mailbox@example.com");
$pass |= testEmail("peter.piper@example.com");
$pass |= testEmail("Doug\\ \\\"Ace\\\"\\ Lovell@example.com");
$pass |= testEmail("\"Doug \\\"Ace\\\" L.\"@example.com");

echo "\nAll of these should fail:\n";
$pass |= !testEmail("abc@def@example.com");
$pass |= !testEmail("abc\\\\@def@example.com");
$pass |= !testEmail("abc\\@example.com");
$pass |= !testEmail("@example.com");
$pass |= !testEmail("doug@");
$pass |= !testEmail("\"qu@example.com");
$pass |= !testEmail("ote\"@example.com");
$pass |= !testEmail(".dot@example.com");
$pass |= !testEmail("dot.@example.com");
$pass |= !testEmail("two..dot@example.com");
$pass |= !testEmail("\"Doug \"Ace\" L.\"@example.com");
$pass |= !testEmail("Doug\\ \\\"Ace\\\"\\ L\\.@example.com");
$pass |= !testEmail("hello world@example.com");
$pass |= !testEmail("gatsby@f.sc.ot.t.f.i.tzg.era.l.d.");
$pass |= !testEmail("hello@world");
$pass |= !testEmail("hello@gmail");

echo "\nThe email validation ";

if ($pass == 0)
{
   echo "passes all tests.\n";
}
else
{
   echo "is deficient.\n";
}
?>

Alternativen

Die Prüfung kann auch in JavaScript erfolgen, etwa mittels des oben erwähnten mailcheck.js. Allerdings hat auch dieses Fehler, wie bereits beobachtet, und verwirft eigentlich gültige RFC 2822-Adressen. Die meisten »scheinbaren« Alternativen (siehe Codebeispiele weiter oben) sind aber sehr schlecht - und lassen trotzdem ungültige Mailadressen durch. Das ist ein noch viel größeres Ärgernis!

Fazit

Die Frage ist, ob man als Seitenbetreiber überhaupt alle gültigen Adressen zulassen möchte. Ein User sollte wenigstens ein Plus und ein Prozentzeichen eingeben können - daher meine Tests in der Galerie. Aber ob man maskierte @-Zeichen und Anführungszeichen, Kommentare etc. wirklich braucht ist fraglich. Immerhin kann man diese nicht einmal per Google Mail oder bei anderen Anbietern empfangen. Gerade die Pluszeichen sind bei GMail äußerst praktisch. Daher weise ich einige Dienste ab und zu auf deren "Fehler" hin. Aber meistens ist es für die Katz, den Anbietern ist es egal.