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-spec | RFC 5321 Mailbox | |
|---|---|---|
| Zastosowanie | adresy w nagłówkach wiadomości | adresy w komendach SMTP (MAIL FROM, RCPT TO) |
| Komentarze i zawijane białe znaki | dozwolone, np. john(work)@example.com | niedozwolone |
| Formy przestarzałe | akceptowane przy parsowaniu | nie należą do gramatyki |
| Domena | dot-atom albo luźno zdefiniowany literał domeny | etykiety 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:<…>lubRCPT 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@ci 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
atextlub kropka — przechodzą więca..b@example.comi.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
multiplepole 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
- Usuń białe znaki z początku i końca; odrzuć znaki nowego wiersza i znaki sterujące.
- Podziel adres na ostatnim
@. - Sprawdź długości w oktetach: część lokalna ≤ 64, cały adres ≤ 254.
- Przekonwertuj domenę do ASCII (IDNA) i sprawdź jej etykiety.
- 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.
- Podpowiadaj poprawki typowych literówek w domenach; pozwól użytkownikowi je zaakceptować.
- 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.
- 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
- RFC 5321 — Simple Mail Transfer Protocol
- RFC 5322 — Internet Message Format
- RFC 6531 — rozszerzenie SMTP dla umiędzynarodowionej poczty (SMTPUTF8)
- RFC 6532 — umiędzynarodowione nagłówki wiadomości
- RFC 5890 — IDNA: definicje i ramy dokumentów
- RFC 3696, erratum 1690 — maksymalna długość adresu
- WHATWG HTML — valid email address
- Google — kropki w adresach Gmail nie mają znaczenia
- OWASP — Regular expression Denial of Service (ReDoS)