Przejdź do treści
correct.email
Dla programistów

Jak walidować adresy e-mail i dlaczego regex nie wystarczy

Co standardy dopuszczają w adresie e-mail, limity 64 i 254, adresy w cudzysłowie i umiędzynarodowione oraz praktyczna strategia walidacji w formularzach.

Autor: Exmoor Software6 min czytania

Walidacja adresu e-mail odpowiada na jedno wąskie pytanie: czy ten ciąg znaków może być adresem? Nie powie Ci, czy skrzynka istnieje ani czy należy do osoby, która go wpisuje — to rozstrzyga dopiero wiadomość wysłana na ten adres. Dobra walidacja jest więc łagodna wobec składni, rygorystyczna wobec kilku twardych limitów i kończy się e-mailem z potwierdzeniem.

Który standard definiuje „poprawny” adres?

Składnię adresów opisują dwa dokumenty RFC i każdy służy czemu innemu:

RFC 5322 addr-specRFC 5321 Mailbox
Zastosowanieadresy w nagłówkach wiadomościadresy w komendach SMTP (MAIL FROM, RCPT TO)
Komentarze i zawijane białe znakidozwolone, np. john(work)@example.comniedozwolone
Formy przestarzałeakceptowane przy parsowaniunie należą do gramatyki
Domenadot-atom albo luźno zdefiniowany literał domenyetykiety nazwy hosta albo literał adresu

Dla pola formularza użyteczny jest RFC 5321, bo opisuje to, co da się faktycznie dostarczyć. Sam RFC 5322 zaznacza, że jego składnia domeny jest liberalna i że implementacje muszą trzymać się składni właściwej dla kontekstu, w którym adresu używają. Rdzeń gramatyki RFC 5321:

Mailbox     = Local-part "@" ( Domain / address-literal )
Local-part  = Dot-string / Quoted-string
Dot-string  = Atom *("." Atom)
Domain      = sub-domain *("." sub-domain)

Atom to ciąg co najmniej jednego znaku: litery, cyfry lub któregoś z tych znaków:

! # $ % & ' * + - / = ? ^ _ ` { | } ~

Poprawne są więc o'connor@example.com, user+news@example.com i first.last@mail.example. Bez cudzysłowu kropka nie może zaczynać ani kończyć części lokalnej, ani występować dwa razy z rzędu. Etykiety domeny składają się z liter, cyfr i łączników, ale nie mogą zaczynać się ani kończyć łącznikiem.

Limity długości: 64 i 254

RFC 5321 (sekcja 4.5.3.1) określa maksymalne rozmiary w oktetach:

  • część lokalna: 64 oktety;
  • domena: 255 oktetów;
  • ścieżka (path) — adres w MAIL FROM:<…> lub RCPT TO:<…> razem z nawiasami ostrymi: 256 oktetów.

Ścieżka to <, adres i >, więc sam adres może mieć najwyżej 256 − 2 = 254 oktety. Wciąż powtarzana liczba 320 (64 + 1 + 255) pochodzi z pierwotnego tekstu RFC 3696; jego zweryfikowane erratum 1690 ustala praktyczny limit na 254 właśnie z tego powodu.

Oktety to nie znaki. W UTF-8 litera ż zajmuje dwa oktety, więc 10-znakowy użytkownik ma 11 oktetów. Licz bajty, a nie długość napisu.

Część lokalna w cudzysłowie

W cudzysłowie część lokalna może zawierać spacje i znaki takie jak @ czy ,:

"john smith"@example.com
"a@b"@example.com

Oba adresy są poprawne według RFC 5321, ale ten sam dokument zaleca, by nie tworzyć skrzynek wymagających cudzysłowu, i nakazuje traktować wszystkie formy w cudzysłowie jako równoważne — "john"@example.com to ten sam adres co john@example.com. Zdecyduj świadomie, czy Twój system je przyjmuje; jeśli tak, upewnij się, że zapis w bazie i wysyłka sobie z nimi radzą. Niezależnie od decyzji nigdy nie dziel adresu na pierwszym @. Domena zaczyna się po ostatnim.

Literały adresów

Zamiast nazwy domeny adres może zawierać adres IP w nawiasach kwadratowych:

user@[192.0.2.10]
user@[IPv6:2001:db8::25]

RFC 5321 zachowuje je dla hostów, których nie ma w DNS. Całkowicie omijają wyszukiwanie MX, a formularz rejestracji może je rozsądnie odrzucać z czytelnym komunikatem.

Adresy umiędzynarodowione

Chodzi tu o dwa odrębne mechanizmy.

Umiędzynarodowione nazwy domen. Domena taka jak bücher.example istnieje w DNS w postaci ASCII, jako A-label xn--bcher-kva.example (IDNA, RFC 5890). Po konwersji każdy system pocztowy może na nią dostarczać. Domenę waliduj, konwertując ją do ASCII i sprawdzając wynik, a do zapytań DNS używaj postaci A-label.

Część lokalna w UTF-8. RFC 6531 (SMTPUTF8) rozszerza gramatykę RFC 5321 tak, by część lokalna mogła zawierać znaki spoza ASCII, np. użytkownik@przykład.example. Dostarczenie działa tylko między serwerami, które ogłaszają rozszerzenie SMTPUTF8; bez niego klient nie może wysłać adresu umiędzynarodowionego. Taki adres może więc być poprawny, a mimo to niedostarczalny z Twojego systemu — zanim zaczniesz go przyjmować, sprawdź, czy Twoje oprogramowanie i dostawca wysyłki obsługują SMTPUTF8. Normalizuj Unicode do postaci NFC, jak zaleca RFC 6532; postaci NFKC nie należy używać.

Wielkość liter

RFC 5321 mówi, że część lokalną trzeba traktować jako wrażliwą na wielkość liter — dla niektórych hostów smith i Smith to różni użytkownicy — i w tym samym akapicie, że wykorzystywanie tego utrudnia współdziałanie systemów i jest odradzane. Nazwy domen nie rozróżniają wielkości liter.

W praktyce:

  • Zapisuj i wysyłaj adres w postaci, w jakiej go wpisano. Zamiana domeny na małe litery jest bezpieczna.
  • Porównuj bez rozróżniania wielkości liter, gdy szukasz duplikatów albo logujesz użytkownika, bo ludzie nie zawsze wpisują wielkie litery tak samo.
  • Nie normalizuj dalej, jeśli nie znasz dostawcy. Gmail ignoruje kropki w prywatnych adresach Gmail, ale według Google w kontach służbowych i szkolnych kropki mają znaczenie; znaczniki po + to konwencja części dostawców, a nie reguła. Usunięcie jednego lub drugiego może dać inny adres.

Dlaczego regex nie wystarczy

Typowe wzorce zawodzą w obie strony:

  • sprawdzenie domeny najwyższego poziomu w rodzaju [a-z]{2,4}$ odrzuca dłuższe i umiędzynarodowione domeny najwyższego poziomu;
  • klasa znaków w rodzaju [A-Za-z0-9._-] odrzuca + i ', które są poprawne i popularne;
  • luźne wzorce w rodzaju \S+@\S+ przepuszczają a@b@c i podobne ciągi;
  • regex nie policzy oktetów, nie przekonwertuje domeny IDN do postaci A-label i nie sprawdzi limitów dla poszczególnych części, nie stając się przy tym nieczytelny;
  • złożone wzorce mogą działać bardzo wolno dla spreparowanych danych — strona OWASP o ReDoS podaje jako przykład wyrażenie do walidacji adresów e-mail.

Żaden wzorzec nie powie też, czy skrzynka istnieje. Zamiast tego zastosuj kilka jawnych sprawdzeń.

Co robi input type="email"

Pole e-mail w HTML sprawdza wartość według definicji „valid email address” ze specyfikacji WHATWG, którą sama specyfikacja nazywa świadomym naruszeniem RFC 5322 („willful violation”). Według specyfikacji:

  • przed walidacją przeglądarka usuwa znaki nowego wiersza oraz białe znaki na początku i na końcu;
  • część lokalna to co najmniej jeden znak atext lub kropka — przechodzą więc a..b@example.com i .a@example.com, choć SMTP ich nie dopuszcza;
  • domena to co najmniej jedna etykieta z liter, cyfr i łączników, każda do 63 znaków, a kropka nie jest wymagana — przechodzi user@localhost;
  • części lokalne w cudzysłowie, literały adresów i części lokalne spoza ASCII są odrzucane;
  • przeglądarki powinny wyświetlać etykiety domeny w punycode jako Unicode i konwertować je z powrotem w wartości pola — sprawdź, jak z wpisanymi domenami IDN radzą sobie przeglądarki, które obsługujesz;
  • z atrybutem multiple pole przyjmuje listę adresów rozdzielonych przecinkami.

Traktuj to jako udogodnienie — szybka informacja zwrotna, często klawiatura do wpisywania adresów na telefonie — a nie jako walidację. Żądania mogą ominąć przeglądarkę, więc serwer musi sprawdzić adres jeszcze raz.

Praktyczna strategia dla formularzy

  1. Usuń białe znaki z początku i końca; odrzuć znaki nowego wiersza i znaki sterujące.
  2. Podziel adres na ostatnim @.
  3. Sprawdź długości w oktetach: część lokalna ≤ 64, cały adres ≤ 254.
  4. Przekonwertuj domenę do ASCII (IDNA) i sprawdź jej etykiety.
  5. Ustal zasady dla rzadkich form — części lokalnej w cudzysłowie, literałów adresów, części lokalnej w UTF-8 — i stosuj je z czytelnym komunikatem.
  6. Podpowiadaj poprawki typowych literówek w domenach; pozwól użytkownikowi je zaakceptować.
  7. Opcjonalnie sprawdź, czy domena istnieje i nie publikuje null MX, korzystając z rekordów MX (Wyszukiwanie MX). Nie odrzucaj adresu z powodu przekroczenia czasu odpowiedzi DNS.
  8. Wyślij e-mail z potwierdzeniem. To jest właściwa walidacja.

W pseudokodzie:

function checkAddress(input):
    s = trim(input)
    if s is empty or contains a line break or control character:
        return invalid("empty or invalid characters")
    at = lastIndexOf(s, "@")
    if at < 1 or at == length(s) - 1:
        return invalid("missing local part or domain")
    local  = s[0 .. at-1]
    domain = s[at+1 ..]
    if utf8Length(local) > 64 or utf8Length(s) > 254:
        return invalid("too long")
    if domain starts with "[":
        return applyPolicy("address literal")      # usually reject
    ascii = idnaToAscii(domain)       # bücher.example -> xn--bcher-kva.example
    if ascii failed or not validHostLabels(ascii):
        return invalid("domain is not a valid host name")
    if isQuoted(local) or hasNonAscii(local):
        return applyPolicy("quoted or UTF-8 local part")
    if not isDotAtom(local):          # atext characters, single dots inside
        return invalid("local part has invalid characters")
    return valid(suggestion = typoSuggestion(ascii))  # gmial.com -> gmail.com

Walidator składni e-mail realizuje te sprawdzenia — RFC 5321 z RFC 6531 i IDNA — opisuje każdy znaleziony problem i podpowiada poprawki literówek, więc możesz porównać z nim przypadki brzegowe we własnej implementacji. Nie sprawdza, czy skrzynka istnieje; żadne sprawdzenie składni tego nie potrafi.

Po walidacji

Poprawny adres to dopiero kandydat. Wyślij wiadomość z potwierdzeniem, dopisuj adres do list mailingowych dopiero po potwierdzeniu i pozwól, by odbicia powiedziały Ci, kiedy przestał działać — zobacz higienę listy adresowej. Jeśli potwierdzenie wysyła Twoja aplikacja, uwierzytelnienie i konfigurację opisuje poradnik o poczcie transakcyjnej z aplikacji.

Źródła