Kontrola e-mailové adresy

Školení, která pořádám

Dopsal jsem knihu

Článek vyšel na serveru Root.cz.

Často po uživateli chceme, aby zadal svou e-mailovou adresu. Tuto adresu lze kontrolovat podle různých kritérií. Pro účely tohoto článku bude za e-mailovou adresu považován řetězec uživatel@doména, který obvykle chceme po uživateli zadat, a ne mnohem komplexnější řetězec, který lze zadat do hlaviček From, To a dalších.

Syntaktická kontrola

V první řadě tu máme syntaktickou kontrolu. Na webu najdete spoustu pokusů, jak tuto kontrolu provádět, já jsem se podílel např. na této, dodatečně jsem v ní ale stejně našel chybu. Proto uvádím řešení, o kterém si aktuálně myslím, že je správné:

<?php
/** Kontrola e-mailové adresy
* @param string e-mailová adresa
* @return bool syntaktická správnost adresy
* @copyright Jakub Vrána, http://php.vrana.cz/
*/
function check_email($email) {
    $atom = '[-a-z0-9!#$%&\'*+/=?^_`{|}~]'; // znaky tvořící uživatelské jméno
    $domain = '[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])'; // jedna komponenta domény
    return eregi("^$atom+(\\.$atom+)*@($domain?\\.)+$domain\$", $email);
}
?>

Ověření, zda adresa přijímá zprávy

Dále je možné kontrolovat, zda uvedená doména přijímá e-maily, což zjistí třeba funkce checkdnsrr. Podle RFC 2821 sice doména může poštu přijímat i bez MX záznamu, ale nebývá to zvykem.

Dále je možné server oťukat a zjistit, zda e-mail přijme. To je asi nejdůkladnější automatické řešení (opraveno):

<?php
/** Odeslání příkazů SMTP serveru
* @param resource otevřený socket k SMTP serveru
* @param array příkazy k odeslání
* @return bool false v případě, že některý příkaz nevrátí 250
*/
function smtp_commands($fp, $commands) {
    foreach ($commands as $command) {
        fwrite($fp, "$command\r\n");
        $s = fgets($fp);
        if (substr($s, 0, 3) != '250') {
            return false;
        }
        while ($s[3] == '-') {
            $s = fgets($fp);
        }
    }
    return true;
}

/** Ověření funkčnosti e-mailu
* @param string adresa příjemce
* @param string adresa odesílatele
* @return bool na adresu lze doručit zpráva, null pokud nejde ověřit
* @copyright Jakub Vrána, http://php.vrana.cz/
*/
function try_email($email, $from) {
    if (!function_exists('getmxrr')) {
        return null;
    }
    $domain = preg_replace('~.*@~', '', $email);
    getmxrr($domain, $mxs);
    if (!in_array($domain, $mxs)) {
        $mxs[] = $domain;
    }
    $commands = array(
        "HELO " . preg_replace('~.*@~', '', $from),
        "MAIL FROM: <$from>",
        "RCPT TO: <$email>",
    );
    $return = null;
    foreach ($mxs as $mx) {
        $fp = @fsockopen($mx, 25);
        if ($fp) {
            $s = fgets($fp);
            while ($s[3] == '-') {
                $s = fgets($fp);
            }
            if (substr($s, 0, 3) == '220') {
                $return = smtp_commands($fp, $commands);
            }
            fwrite($fp, "QUIT\r\n");
            fgets($fp);
            fclose($fp);
            if (isset($return)) {
                return $return;
            }
        }
    }
    return false;
}
?>

Toto řešení ale pochopitelně nebude fungovat u serverů aplikujících greylisting.

Ověření, zda má uživatel adresu pod kontrolou

Pokud zároveň chceme ověřit, že má uživatel adresu pod kontrolou, musíme mu poslat zprávu:

<?php
/** Vygenerování náhodného řetězce
* @param int délka vráceného řetězce
* @param int použité znaky: <=10 číslice, <=36 +malá písmena, <=62 +velká písmena
* @return string náhodný řetězec
* @copyright Jakub Vrána, http://php.vrana.cz/
*/
function rand_chars($count = 8, $chars = 36) {
    $return = "";
    for ($i=0; $i < $count; $i++) {
        $rand = rand(0, $chars - 1);
        $return .= chr($rand + ($rand < 10 ? ord('0') : ($rand < 36 ? ord('a') - 10 : ord('A') - 36)));
    }
    return $return;
}

$rand_chars = rand_chars();
if (mysql_query("INSERT INTO emaily (email, rand_chars) VALUES ('" . mysql_real_escape_string($_POST["email"]) . "', '$rand_chars')")) {
    $zprava = "Pokud máte zájem o služby serveru $_SERVER[SERVER_NAME], tak prosím navštivte tento odkaz:
http://$_SERVER[SERVER_NAME]/email_overit.php?id=" . mysql_insert_id() . "&rand_chars=$rand_chars
Pokud o služby serveru zájem nemáte, tak tuto zprávu prosím ignorujte.";
    $hlavicky = "MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit";
    mail($_POST["email"], "Overeni adresy", $zprava, $hlavicky);
}

// email_overit.php:
mysql_query("UPDATE emaily SET overeno = 1 WHERE id = " . intval($_GET["id"]) . " AND rand_chars = '" . mysql_real_escape_string($_GET["rand_chars"]) . "'");
?>

Závěr

Záleží jen na vás, jak moc důkladně chcete kontrolu e-mailové adresy provádět. Nejčastěji se používají krajní varianty – buď pouhá syntaktická kontrola, nebo ověření, zda má uživatel adresu pod kontrolou.

Jakub Vrána, Řešení problému, 5.5.2006, diskuse: 43 (nové: 0)

Diskuse

Rick:

To vsechno je kontrola jedne adresy? Wow, jak vam mohli dat ridicak...
# 19.5.2006 02:35:22 reagovat

zjoova:

A jak vypada vase kratsi a elegantnejsi reseni?
Priznavam ze jsem zatim vsude videl jen podobne obludy.
# 29.5.2006 17:49:14 reagovat

Jakub:

Dělám PHP docela dlouho a jako špatné řešení se mi to nezdá. Jestli jsou tady takoví chytráci (takové mám nejraději) tak nám prosím předvěďte lepší kód. Rád se na něj podívám. TOhle přesně znám. Velká huba, žádné činy...
# 10.6.2006 17:52:13 reagovat

Zdendys:

Programuji PHP taky již nějaký rok a bohužel se při dokonalé kontrole nevyhnete ledajakým zákoutím a proto se script neůměrně zvětšuje.

Nepruďte a navrhněte kratši dokonalé řešení a budeme vá vzívat do programátorského nebe :-)
# 14.6.2006 00:40:23 reagovat

j.hlava:

Vzývat zásadně s y.
# 3.12.2008 12:55:57 reagovat

Fanda:

Naopak, oceňuji energii vloženou do tohoto skriptu a hlavně vůli podělit se. Díky moc za skript (proti kódu nemám námitek i když nepoužívám tak důkladnou kontrolu). Ještě jednou díky. A chytráci, až dozrají do téhle úrovně programování, potom pochopí...
# 14.6.2006 23:16:10 reagovat

dustcart:

Me se to naopak velmi libi. Nahodou na slozitost problemu je to velmi kratke a elegantni reseni, takze to vetsinou kritizuji jen blbecci, kteri tomu nerozumi.
# 2.11.2006 11:10:41 reagovat

Martin:

V druhej verzii mi try_email($email, $from) vracia 1 aj pri nezmyselnej adrese. Moze to byt nastavenim serveru alebo ...?
# 3.11.2006 13:26:46 reagovat

Dave:

v overeni mailu je potreba jeste dodat kontrolu pripony. pokud by tam nekdo napsal adresat@domena.xxxxxxxxxx, tak to projde. dal bych tam overeni tak na max 4-5 mist. to by mohlo stacit.
# 18.11.2006 12:57:51 reagovat

Shuster:

<?php
if (!eregi('^[a-z0-9_]{1}[a-z0-9\-_]*(\.[a-z0-9\-_]+)*@[a-z0-9]{1}[a-z0-9\-_]*(\.[a-z0-9\-_]+)*\.[a-z]{2,4}$', $email))
{
    echo
"nespravny format adresy";
}

?>
# 17.12.2006 21:10:28 reagovat

ikona Jakub Vrána:

Zapomínáte na doménu .museum. V budoucnu můžou přijít i delší domény, v RFC to nijak omezené není.
# 3.1.2008 10:35:39 reagovat

Kepi:

Tak tak, .museum, .travel zatím jen 6 znaků, ale uvažuje se i o víc a hlavně jsou tu už náznaky, že bude možné registrovat si TLD za určitý poplatek, pak určitě vzniknou i více než 10 znakové koncovky.
# 17.7.2008 06:47:39 reagovat

ikona Jakub Hejda:

Vynikají cí článek! Je vidět že v PHP opravdu "myslíte".
# 10.10.2007 14:44:11 reagovat

francesco3:

Mám také jednu poznámku. Dovolil jsem si použít část kódu pro syntaktickou kontrolu v praxi a zjistil j6sem, že lze mít v emailové adrese tečku ( http://www.pilsen-ride.net/index.php?p=profil&jmeno=fast ) i bezprostředně před zavináčem. Což je situace, kterou fce check_email vyhodnotí negativně.
# 24.10.2007 00:09:16 reagovat

ikona Jakub Vrána:

RFC 2822 to nicméně jasně zakazuje.
# 24.10.2007 21:39:41 reagovat

ikona Web Designer:


Vrelá vďaka za podelenie sa o zaujímavé metódy overenia, zaujala ma časť "Ověření, zda adresa přijímá zprávy".

Nutné však podotknúť, že na zrejme ostrieľaného programátora je tu viac nedostatkov než sa patrí, a práve takéto navádzajú novým, snáď nádejných programátorov na zlé cesty.

málo spomeniem:
- Zbytočne nedokončené overovanie formátu (meno@domena.* ???). už bolo spomenuté. (aj keď sa mi ani nepáči dovolenie )
- Neprehľadné a spomalujúce vytváranie stringov v dvojitých úvodzovkách:
<?php
# pomaľšie a neprehľadné:
$str = "nieco sa $deje tam
dole v udoli"
;
# odporúčam:
$str  = 'nieco sa '.$deje;
$str .= 'dole v udoli';
?>
- Neošetrené vstupy $_POST, $_GET. Chápem alebo dúfam, že tento "ukážkový kód" má iba ilustračný charakter, tým však zlý edukačný dopad.
Takže správne pred takýmto použitím funkcie mail určite najprv overiť vstup, nehovoriac o SQL príkazoch. Minimálne takto:
<?php
if (check_email($email = $_POST['email'])) {
   
// ...
}

mysql_query('UPDATE emaily SET overeno = 1 WHERE id = '.intval($_GET[id]).' AND rand_chars = "'.mysql_real_escape_string($_GET[rand_chars]).'"');
?>
# 3.1.2008 03:59:15 reagovat

ikona Jakub Vrána:

Délka top-level domény není nijak omezena. Nyní máme doménu .museum, v budoucnu mohou přijít i delší.

Zápis "nieco sa $deje tam" je subjektivně podstatně přehlednější než 'nieco sa '.$deje (navíc bez mezer). Vámi uvedené dva příklady navíc nejsou ekvivalentní, v druhém případě chybí "\n" a to už jde přehlednost do háje úplně.

V patičce je jasně uvedeno, že skripty předpokládají zapnutou direktivu magic_quotes_gpc=On, jsou tedy zcela bezpečné.
# 3.1.2008 10:39:58 reagovat

ikona Jakub Vrána:

Kód jsem předělal na magic_quotes_gpc = Off a zmínil jsem to v patičce.
# 11.5.2009 05:31:18 reagovat

ikona Web Designer:

... píšm trochu neskoro, ešte aj ja som tam spravil pár preklepov ako to čítam.

To čo sa mi nepáči, sú povolené znaky podľa spomenutej normy. Nevidí sa mi to veľmi užitočné (ani bezpečné) a praktické ju dodržať striktne. Niečo ako navrhol komentátor Shuster sa mi zdá skoro dostačujúce, a určite bezpečnejšie. Ale to je iba taký môj názor na základe nočných pocitov.

Namiesto <?php $_GET[id]; ?> je určite slušnejšie <?php $_GET['id']; ?>, veď id predsa nie je žiadna definovaná konštanta, všakže.
# 3.1.2008 04:08:04 reagovat

ikona Jakub Vrána:

Dodržovat normu je naopak nutné. Jak by mohlo být něco nebezpečného na dodržování normy? Nebo neužitečného a nepraktického? Můžete si třeba říct, že rovnítko v e-mailové adrese obvykle není. Ale podle normy tam být může a některé konference ho skutečně používají.

<?php $_GET[id]; ?> je samozřejmě chybný zápis, <?php "$_GET[id]"; ?> je ale něco úplně jiného. Když už jste si to nepřečetl v manuálu, tak si to alespoň vyzkoušejte.
# 3.1.2008 10:44:08 reagovat

Vojtech R:

Reqular Expression Library, konkrétně:

http://regexlib.com/DisplayPatterns.aspx?…&categoryid=1&p=1
# 15.4.2008 12:57:54 reagovat

gogulux:

Kontrola, jestli ma uzivatel nad mailem kontrolu by mohla byt i poslani nahodneho hesla pro prihlaseni, s tim ze si ho uzivatel po prihlaseni zmeni.
# 21.5.2008 18:34:56 reagovat

b022d:

Nebo prosté, léta známé, odesílání tzv. aktivace účtu. Pokud to ale chci použít k jiným účelům nebo nechci otravovat uživatele se zbyečnými kontrolami, podobnému monstru se asi nevyhnu, bohužel. A tohle je navíc ještě poměrně elegantní monstrum...
# 14.9.2008 09:39:15 reagovat

Jan Suchánek:

Tak jsem využil vaší funkce a je bezva jen bych doplnil, šlo by ještě upravit před kontrolou $email tak aby i blbě zadaný byl opravený?

např. jmeno . prijmeni@ domena.cz

Proč o tom píš? Napadají Vás i další chyby? Mě třeba že bych na email mohl použít funkci která ho převede na ascii. Jen nevím třeba co když někdo zadá "Jméno Příjmení"<jmeno.prijmeni@domena.cz> existují ještě další varianty jak lidi mohou napsat svoují emailovu adresu?
# 4.3.2009 05:47:59 reagovat

Ofi:

Mě by zajímalo jak byste udělali to kdyby měl uživatel pouze den na ověření emailu (tzn. bud docasna data v MySQL nebo nejaky skript ktery se sposti v presnou dobu kazdy den a cisti db od zaznamu starsich nez den) - jde to nejak? a jak moc by to bylo praktické?
# 1.7.2009 15:13:52 reagovat

ikona Jakub Vrána:

Do tabulky by se ukládalo datum registrace a skript by to pravidelně promazával. Omezit to na jeden den mi ale praktické nepřijde.
# 2.7.2009 00:56:46 reagovat

Ofi:

no já se ptal na ten script který by to pravidelně promazával - ale už jsem to našel :) pro ty které by to zajímalo tak je to v mySQL (5.1) příkaz CREATE EVENT.
# 3.7.2009 04:13:06 reagovat

ikona Jakub Vrána:

Obvykle se to řeší cronem, ale CREATE EVENT je samozřejmě lepší volba (pokud je k dispozici).
# 3.7.2009 05:19:50 reagovat

Prdlořeznictví Krkovička, n. p.:

S verzí PHP 5.2 přibyla i možnost využití filterů:
<?php
function check_email($email){
return
filter_var($email, FILTER_VALIDATE_EMAIL)!=false;
}
?>
# 7.7.2009 22:16:02 reagovat

Prdlořeznictví Krkovička, n. p.:

popřípadě bez toho !=false protože "", "0" nebo 0 nemůžou být validní e-mailové adresy.
# 7.7.2009 22:18:05 reagovat

ikona Jakub Vrána:

Aby se to rozlišovalo, tak by tam stejně muselo být !==.
# 11.7.2009 17:48:44 reagovat

ikona Jakub Vrána:

Jen pozor na to, že FILTER_VALIDATE_EMAIL uznává i lokální domény. Takže třeba info@example validací projde. A pak mám taky za to, že starší verze kontrolovaly celý řetězec, který jde umístit do hlavičky (takže třeba i včetně jména), ale to už zdá se neplatí.
# 11.7.2009 17:53:04 reagovat

Michal:

V ofic. PHP manuálu je, že fce eregi v PHP 5.3.0 již nepůjde a v 6.0.0 bude odstraněna. Nemáte tedy k dispozici nějakou jinou, podobnou formu kontroly emailové adresy, jelikož tato funkce se mi velmi líbí ?
# 14.9.2009 15:15:13 reagovat

ikona Jakub Vrána:

Funkce eregi("$s", $email) se dá v tomto případě nahradit za preg_match("($s)i", $email).
# 14.9.2009 18:11:05 reagovat

Miloš Němec:

Nebo lze použít case sensitivní ereg a doplnit regulární výrazy pro uživatelské jméno a doménu o velká písmena.
<?php
    $atom
= '[-a-zA-Z0-9!#$%&\'*+/=?^_`{|}~]'; // znaky tvořící uživatelské jméno
   
$domain = '[a-zA-Z0-9]([-a-zA-Z0-9]{0,61}[a-zA-Z0-9])'; // jedna komponenta domény
   
return ereg("^$atom+(\\.$atom+)*@($domain?\\.)+$domain\$", $email);

?>
# 25.11.2009 11:03:52 reagovat

Viktor:

Toto neni moje vlastni reseni. Jen jsem ho nekde drive nasel a nejakou dobu k plne spokojenosti vyuzivam. Je to vicemene synonymum k te prvni variante. Myslis, ze zbytecne slozite nebo je na nem neco zasadne spatne?

<?php
   
function ZkontrolujEmail($email) {
       
// First, we check that there's one @ symbol, and that the lengths are right
       
if (!ereg("^[^@]{1,64}@[^@]{1,255}$", $email))
            return
false; // Email invalid because wrong number of characters in one section, or wrong number of @ symbols.
        // Split it into sections to make life easier
       
$email_array = explode("@", $email);
       
$local_array = explode(".", $email_array[0]);
        for (
$i = 0; $i < sizeof($local_array); $i++)
            if (!
ereg("^(([A-Za-z0-9!#$%&'*+/=?^_`{|}~-][A-Za-z0-9!#$%&'*+/=?^_`{|}~\.-]{0,63})|(\"[^(\\|\")]{0,62}\"))$", $local_array[$i]))
                return
false;
        if (!
ereg("^\[?[0-9\.]+\]?$", $email_array[1])) { // Check if domain is IP. If not, it should be valid domain name
           
$domain_array = explode(".", $email_array[1]);
            if (
sizeof($domain_array) < 2) return false; // Not enough parts to domain
           
for ($i = 0; $i < sizeof($domain_array); $i++)
                if (!
ereg("^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]+))$", $domain_array[$i])) return false;
        }
        return
true;
    }
?>
# 14.9.2009 18:07:12 reagovat

ikona Jakub Vrána:

Je to zbytečně složité.
# 14.9.2009 18:09:18 reagovat

Jakub Laušman:

Zdravim,

jen jsem ti chtěl poděkovat za rychlé a elegantní řešení mého problému s ověřováním formátu uvedeného emailu...

Tvůj kód mi ušetřil fůrmu času ;-)

Ahoj Kuba
# 9.10.2009 19:18:21 reagovat

Petr Konůpek:

Díky Jakube za schůdné řešení tohoto problému. Já vyvýjím aplikace ve Windowsovém prostředí a tam bohužel do verze   5.3 PHP nepodporuje funkci getmxrr. Na PHP.net jsem ale našel v komentářích alternativu, jak si tam tuto funkci dodat sám. Zde přeposílám:

<?php

// getmxrr() support for Windows by HM2K <php [spat] hm2k.org>
function win_getmxrr($hostname, &$mxhosts, &$mxweight=false) {
    if (
strtoupper(substr(PHP_OS, 0, 3)) != 'WIN') return;
    if (!
is_array ($mxhosts) ) $mxhosts = array();
    if (empty(
$hostname)) return;
   
$exec='nslookup -type=MX '.escapeshellarg($hostname);
    @
exec($exec, $output);
    if (empty(
$output)) return;
   
$i=-1;
    foreach (
$output as $line) {
       
$i++;
        if (
preg_match("/^$hostname\tMX preference = ([0-9]+), mail exchanger = (.+)$/i", $line, $parts)) {
         
$mxweight[$i] = trim($parts[1]);
         
$mxhosts[$i] = trim($parts[2]);
        }
        if (
preg_match('/responsible mail addr = (.+)$/i', $line, $parts)) {
         
$mxweight[$i] = $i;
         
$mxhosts[$i] = trim($parts[1]);
        }
    }
    return (
$i!=-1);
}
?>
Neznám funkci getmxrr() na tak dokonalé úrovni, abych byl schopen posoudit, zda-li jí tato dokáže plně nahradit, ale pokud ano, pak by to Widlákům usnadnilo život...
# 18.12.2009 17:09:28 reagovat

ikona IvoSn:

Chtělo by to výraz upravit kvůli novým vymoženostem:
- ereg depreceated
- domény s diakritikou
Místo eregi můžeme použít mb_eregi, ale jak upravit ten regulár?
# 5.1.2010 16:34:48 reagovat

Jaroslav Kavan:

zdravim v php se vůbec nevyznám,ale na jednom serveru se nám vysktl problém.Jedná se oto že na stránce zadáte jméno účtu a email a tím se posílá nové heslo na email. Vím že je to programování v php a chtěl bys se zeptat jestly by jste sem někdo nemohl ukázat jak by to mnělo vypadat aby to fungovalo.
Protože když jsem chtěl zaslat nové heslo z té stránky napsalo mi to že heslo bylo odesláno ale mail mi nepřišel.
# 31.3.2010 04:10:25 reagovat

Honza:

A nedal by se obejít ten graylisting pokud by se to samé provedlo dvakrát po sobě?
# 2.7.2010 11:35:33 reagovat

Michal Bláha:

Ahoj,
kvůli zrušení podpory POSIX výrazů v PHP 5.3 si dovoluji přidat verzi pro preg rozšířenou o kontrolu existenci domény.

<?php
   
public function checkMailAddres($addres) {

       
// preg pattern for user name
        // http://tools.ietf.org/html/rfc2822#section-3.2.4
       
$atext = "[a-z0-9\!\#\$\%\&\'\*\+\-\/\=\?\^\_\`\{\|\}\~]";
       
$atom = "$atext+(\.$atext+)*";

       
// preg pattern for domain
        // http://tools.ietf.org/html/rfc1034#section-3.5
       
$dtext = "[a-z0-9]";
       
$dpart = "$dtext+(-$dtext+)*";
       
$domain = "$dpart+(\.$dpart+)+";

        if(
preg_match("/^$atom@$domain$/i", $addres)) {
            list(
$username, $host)=split('@', $addres);
            if(
checkdnsrr($host,'MX')) {
                return
TRUE;
            }
        }
        return
FALSE;
    }
?>
# 26.7.2010 10:27:21 reagovat

Vložit příspěvek

Používejte diakritiku. Nelze používat HTML značky, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

© 2005-2010 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.