Wyrażenia regularne w praktyce – walidacja adresu email

Dzisiaj zajmiemy się jednym z częściej spotykanych zastosowań wyrażeń regularnych, czyli walidacją adresu e-mail. Przypadek ten najczęściej dotyczy adresów wpisywanych w formularzu, zanim zostanie on przesłany na serwer.

W artykule omówimy dwa rozwiązania tego problemu. Pierwsze jest rozwiązaniem, które często spotykam w sieci na forach i w poradnikach online, a nawet książkach, oraz drugie, które stanowi moją zmodyfikowaną wersję pierwszego wyrażenia.

Na wstępie należy jednak pamiętać o jednej, ważne zasadzie: Walidacja danych wprowadzanych przez użytkownika ZAWSZE, ale to bez wyjątku ZAWSZE musi odbywać się na serwerze. Walidacja po stronie klienta jest dobrym rozwiązaniem gdyż pozwala szybko poinformować użytkownika o ewentualnych błędach bez konieczności komunikacji z serwerem (Ajax) ale pamiętajmy, że kod JavaScript może zostać zmodyfikowany, dlatego walidacja ta stanowi jedynie dodatek do właściwej walidacji serwerowej.

Istnieje wiele skryptów z gotowymi metodami do walidacji e-maili, w tym również dodatki do biblioteki jQuery. My jednak zajmiemy się samodzielną walidacją z wykorzystaniem wyrażeń regularnych.

Co to właściwie znaczy „poprawny” adres e-mail?

Zasady budowy adresów e-mail są zawarte w standardach RFC i najogólniej mówiąc można przyjąć, że poprawny adres składa się z:

  • dużych i małych liter alfabetu łacińskiego [A-Za-z],
  • cyfr [0-9],
  • znaku kropki (co najmniej jednego),
  • znaku „@” (dokładnie jednego),
  • znaków myślnika i podkreśleń dolnych.

Standard RFC dopuszcza jeszcze co prawda stosowanie w adresach znaków specjalnych jak !#$%… tylko pytanie, czy na pewno chcemy dopuszczać takie adresy jako poprawne?

W praktyce nikt nie założy sobie świadomie adresu zawierającego znaki specjalne gdyż po pierwsze jest on trudny do zapamiętania, a po drugie nie przejdzie walidacji w wielu witrynach co uniemożliwi np. wysyłanie formularzy, składanie zamówień itp.

Osobiście uważam, że tego typu adresy w tradycyjnych formularzach kontaktowych powinny zostać odrzucone jako błędne, gdyż z dużym prawdopodobieństwem można przyjąć, że będzie to adres wygenerowany w sposób losowy np. przez robota chcącego wysłać jakiś spam.

W moich przykładach uznaję zatem, że poprawny adres to taki, który zawiera tylko ww. elementy (litery, cyfry, kropki, myślnik, podkreślenie). Ponad to należy zwrócić uwagę na jeszcze jeden drobny szczegół. Adres e-mail (a dokładniej nazwa użytkownika) musi zaczynać się od litery [a-z] lub cyfry – na początku nie może znajdować się żaden inny znak.

Jeśli walidacja byłaby przeprowadzana na potrzeby komunikacji z innymi aplikacjami poczty elektronicznej to można by rozważyć ewentualne dopuszczenie stosowania w adresie nawiasów okrągłych „(…)”. Niektóre programy pocztowe mogą przy ich użyciu wysyłać tzw. komentarze zawarte w adresie e-mail. My rozważamy jednak „tradycyjną” witrynę czy aplikację on-line i nie dopuszczamy tego typu konstrukcji adresu.

No to zaczynamy…

W czasie pisania kodu produkcyjnego na początku należałoby rozpocząć od stworzenia solidnego zestawu testów jednostkowych, które pozwolą dokładnie zweryfikować tworzone przez nas wyrażenie. My jednak testami zajmiemy się za chwilę, po omówieniu pierwszego wyrażenia regularnego.

Na pierwszy ogień pójdzie wyrażenie, które często spotykam w wielu poradnikach. Jest ono dość proste w budowie lecz niestety posiada pewne wady, które za chwilę omówimy.

var reg = /^[-\w\.]+@([-\w]+\.)+[a-z]+$/i

Zanim przejdziemy do testowania naszego wyrażenia regularnego spróbujemy rozłożyć je na mniejsze fragmenty:

/^ ... $/i

Znaki „^” oraz „$” oznaczają granice dopasowania, w tym wypadku całe wyrażenie. Znak „^” odnosi się do początku analizowanego ciągu, a znak dolara do jego końca. Ponieważ wyrażeniem testujemy zarówno początek jak i koniec to nie ma sensu stosowanie dodatkowej flagi „g” oznaczającej dopasowanie globalne (w naszym wypadku nie interesuje nas wyszukiwanie wieloliniowe, więc można przyjąć takie właśnie założenie).

[-\w\.]+@

Zapis ten oznacza, że na początku ciągu musi znajdować się myślnik, kropka lub dowolny znak reprezentujący litery alfabetu amerykańskiego lub cyfry. Symbol „\w” oznacza [A-Za-z0-9_]. Zwróć więc uwagę, że poza literami i cyframi zezwalamy również na dopasowanie dolnego podkreślenia. W adresach e-mail jest to prawidłowy znak, ale pamiętaj o tym zawsze gdy chcesz zastosować skróconą formę zapisu „\w”. Znak kropki poprzedziliśmy znakiem ucieczki „\”. W tym wypadku nie jest to konieczne, gdyż wewnątrz nawiasów kwadratowych kropka oznacza bezpośrednio znak „.”, jednakże poza tymi nawiasami odnosi się ona do dowolnego znaku, i wtedy trzeba stosować zapis „\.” aby wyraźnie wskazać, że dopasowujemy dosłownie znak kropki. Jeśli nie masz pewności co do znaczenia poszczególnych znaków specjalnych to dla bezpieczeństwa lepiej zastosować znak ucieczki.

Znak „-\w\.” musi wystąpić co najmniej jeden raz, a następnie musi znajdować się dokładnie jeden znak „@”.

([-\w]+\.)+

Po znaku „@” musi znajdować się nazwa domeny. W tym miejscu musimy wyraźnie oddzielić dwie kwestie. Na wstępie tego artykułu podałem przyjęte przeze mnie (wybrane ze standardu RFC) wymagania dla poprawnego adresu e-mail. Odnosi się to jednak do części adresu znajdującej się na lewo od znaku „@”. Dalej, za tym znakiem znajduje się nazwa domeny, która rządzi się nico innymi prawami.

Zasady tworzenia nazw domen

Nazwa domeny może składać się z liter alfabetu łacińskiego „[A-Za-z]”, cyfr „[0-9]” oraz znaku minusa „-„. Zauważ, że nie występuje tutaj znak podkreślenia, dlatego nie do końca poprawne jest używanie w tym miejscu formy „\w”.

Co więcej, nazwa domeny może składać się z co najmniej trzech i nie więcej niż 63 znaków, przy czym wliczane są w to również kropki oddzielające nazwy subdomen.

Obecnie w nazwach domen można również stosować znaki diaktryczne, czyli w naszym wypadku tzw. „polskie znaki” jak ąęćśźżół. Pytanie jednak czy na pewno chcemy dopuścić ich obecność w adresie e-mail? Osobiście uważam, że w 99,9% przypadków wpisanie przez użytkownika w adresie polskiego znaku diaktrycznego będzie wynikiem błędu edytorskiego co się zdarza szczególnie użytkownikom mniej obytym z komputerem i pocztą elektroniczną. Uważam zatem, że choć adres alicja.kozłowska@kórnik.pl byłby teoretycznie poprawny, to w praktyce jest to błąd we wpisywaniu adresu, który w rzeczywistości nie zawiera polskich znaków.

Powróćmy zatem do naszego przykładu. Nasze wyrażenie regularne próbuje dopasować po znaku „@” nazwę domeny, jednakże poprzez użycie skrótu „\w” omyłkowo dopuszczamy stosowanie dolnych podkreślników. Teoretycznie nikt nie powinien w ten sposób wpisać nazwy domeny gdyż po prostu takiej domeny nie ma. Pytanie jednak, czy na pewno możemy mieć 100% pewność, że użytkownik się nie pomyli i przez nieuwagę nie pomyli podkreślenia „_” ze znakiem myślnika „-„, który to powinien był wpisać?

Na wstępie zaznaczyłem, że walidacja w JavaScript po stronie klienta (nie mówimy tu o stronie serwerowej np. w node.js) jest jedynie dodatkiem i jego zadaniem jest możliwie szybkie wykrycie potencjalnych błędów użytkownika. Z tego powodu tworząc wyrażenia regularne działające po stronie klienta zawsze pamiętaj, aby znaleźć rozsądny kompromis pomiędzy faktycznymi zasadami tworzenia danych (e-mail, nazwa domeny, standardy podawania numerów telefonów, adresów itp.), a możliwymi przypadkowymi błędami użytkownika (jak np. wprowadzenie w adresie polskich znaków diatrycznych).

Na samym końcu adresu musi znajdować się mała lub wielka litera co oznaczono jako

[a-z]+$/i

gdzie flaga „i” oznacza ignorowanie wielkości liter alfabetu (dlatego możemy zapisać [a-z] zamiast [A-Za-z]).

No to czas na testowanie naszego wyrażenia

Tworząc testy dla wyrażenia regularnego należy pamiętać o wielu aspektach jak rodzaje znaków, znaki specjalne, wielkość liter, kwantyfikatory powtórzeń dopasowania, granice dopasowania itp.

Stworzymy sobie tablice zawierające kilka wersji adresów e-mail zarówno poprawnych jak i błędnych, a następnie przeiterujemy po tablicach wywołując dla każdego z adresów metodę RegExp.prototype.test(). W naszym przykładzie użyjmy dwóch zmiennych globalnych, ale w kodzie produkcyjnym staraj się do minimum ograniczać ilość takich zmiennych (najlepiej ograniczyć się do maksymalnie 1-2 zmiennych globalnych).

var valid = ['tomek@drogimex.pl',
'tomek.sochacki@drogimex.pl',
'tomek@urzad.gov.pl',
'ToMeK@drogimex.pl',
'tomek123@domena111.pl',
'tomek_123@domena.pl',
'11tomek@domena.pl'],

invalid = ['-tomek@domena.net', //myślnik na początku
'_tomek@domena.pl', //podkreślenie na początku
'tomek.drogimex.pl', //brak "@"
'@domena.pl', //brak części przed "@"
'tomek@domena', //błędna domena
'tomek@domena.', //błędna domena
'tomek@-domena.pl', //myślnik na początku domeny
'tomek@_domena.pl', //podkreślenie na pozątku domeny
'łukasz@kórnik.gov'] //polskie znaki diaktryczne

Nie jest to pełna lista testów dla adresów e-mail, ale na nasze potrzeby powinna wystarczyć. Sprawdźmy więc nasze testy na pierwszej wersji wyrażenia regularnego dla adresu e-mail. Ponieważ jest to blog przeznaczony dla osób zaczynających dopiero swoją przygodę z JavaScript i RegExp to ograniczę się na razie do prostej metody console.log nie wchodząc w metody assert().

var reg = /^[-\w\.]+@([-\w]+\.)+[a-z]+$/i;

console.log('Testy pozytywne:');
valid.forEach(v => {console.log(reg.test(v),v)});

console.log('Testy negatywne:');
invalid.forEach(v => {console.log(reg.test(v),v)});

Oto wyniki dla powyższych testów:

Jak widać testy adresów poprawnych przeszły prawidłowo, akceptując każdy z podanych adresów. Pewien problem pojawia się jednak w testach badających błędne adresy e-mail.

Wyrażenie regularne nie przestrzega dwóch zasad tworzenia adresów e-mail i nazw domen:

  • nazwa użytkownika w adresie (część przed „@”) musi zaczynać się od litery [A-Za-z] lub cyfry [0-9], więc zapis „\w” jest błędny, gdyż zezwala na stosowanie na początku adresu również znaku podkreślenia. Ponad to dopuszczamy jednocześnie użycie na początku znaku myślnika, co również jest niepoprawne.
  • w nazwie domeny zezwalamy, aby zaczynała się ona od myślnika i dolnego podkreślenia co również jest błędem. Domena może natomiast zaczynać się cyfrą.

No dobrze, spróbujmy więc nieco inną wersję wyrażenia sprawdzającego e-mail

Osobiście stosuję nieco inne wyrażenie do sprawdzania poprawności adresów e-mail w formularzach kontaktowych, którego składnia jest następująca:

var reg = /^[a-z\d]+[\w\d.-]*@(?:[a-z\d]+[a-z\d-]+\.){1,5}[a-z]{2,6}$/i;

Nie będziemy już omawiać po kolei każdego ze składników gdyż zakładam, że czytelnik zna poszczególne oznaczenia. Skupmy się więc na kwestiach ogólnych.

Po pierwsze wprowadziłem ograniczenia w zakresie pierwszych znaków w nazwie użytkownika oraz w nazwie domeny. I tak nazwa użytkownika musi zaczynać się od co najmniej jednej litery lub cyfry [A-Za-z0-9] (w przykładzie jest [a-z\d] ale zwróć uwagę na końcową flagę „i”). Nazwa domeny z kolei musi zaczynać się od litery bądź cyfry [A-Za-z0-9] i nie może zaczynać się myślnikiem.

Niektóre firmy zarządzające sprzedażą domen wprowadzają również dodatkowe warunki, np. ograniczenie ilości powtórzeń myślników (czyli nie dopuszczanie zapisu „—-„, czy ustalanie konkretnych pozycji w domenie (indeksu znaku) gdzie nie może znajdować się myślnik. W naszym przykładzie nie będziemy jednak aż tak dokładnie tego analizować, gdyż wymagałoby to analizy wielu specyfikacji różnych sprzedawców domen, a ponad to zawsze pamiętaj, że z formularza może korzystać osoba posiadająca konto w serwisie zagranicznym, gdzie mogą być nieco inne dodatkowe obostrzenia.

Kolejna zmiana to ograniczenie ilości subdomen oddzielanych kropką. Ograniczenie to nie wynika ze specyfikacji RFC lecz z moich własnych założeń. Wyszedłem w tym momencie z założenia, że sześcioczłonowa nazwa domeny (od 1 do 5 + końcówka, np. „.pl”) to maksymalna ilość, jaka może wystąpić w ogólnie przyjętych poprawnych adresach. Zakładam więc, że większa ilość subdomen może oznaczać potencjalny spam i adres generowany automatycznie, dlatego odrzucam go jako nieprawidłowy. Jest to kwestia moich własnych założeń, które jeśli chcesz możesz zmienić lub w ogóle zrezygnować z ograniczania ilości subdomen.

Zakładam również, że końcówka domeny musi być złożona z co najmniej dwóch i maksymalnie sześciu znaków. Kwestia nazw domen jest ciągle zmieniana i wprowadzane są nowe, uznawane za poprawne nazwy.

Istnieje jeszcze jeden warunek, mówiący o tym, że nazwa całej domeny (po symbolu „@” w adresie e-mail) nie może przekroczyć 63 znaków, a minimalnie powinna mieć trzy znaki. Ja w swoim wyrażeniu nie ograniczam górnej ilości do 63 znaków gdyż wiązałoby się to z dość znacznym skomplikowaniem wyrażenia regularnego, a w praktyce uważam, że domena dłuższa niż 63 znaki mogłaby nigdy się w formularzu nie pojawić – po co więc na siłę komplikować RegExp i wydłużać czas jego testowania.

Zastosowałem także tzw. grupowanie nieprzechwytujące dopasowując nazwę domeny  – symbol „?:”. Nie jest to konieczne, ale uważam to za dobrą praktykę jeśli nie potrzebujemy operować na znalezionych dopasowaniach, np. w metodzie String.prototype.replace().

Ok, przetestujmy więc drugą wersję wyrażenia regularnego na tych samych adresach e-mail:

Tym razem wszystkie nasze testy wychodzą prawidłowo, czyli dla e-maili poprawnych zwracana jest wartość true, a dla błędnych wartość false.

Podsumowanie

Zastanówmy się jednak przez chwilę czy na pewno to koniec testów? Otóż warto jeszcze wspomnieć o pewnym szczególe. Otóż jeśli w polu input (text lub email) wpiszemy ”   tomek@drogimex.pl  ” to pobierając później wartość value elementu DOM będziemy mieli również początkowe i końcowe spacje (tzw. białe znaki).

Generalnie w adresie e-mail nie może być spacji, lecz uważam, że warto przepuścić wartość z input’a przez metodę String.prototype.trim(), która usunie początkowe i końcowe białe znaki i dopiero taką wersję poddać walidacji RegExp. Dlaczego tak? Otóż wychodzę w tym wypadku z założenia, że spacje na początku (na końcu raczej się nie zdarzają) wynikają z omyłki użytkownika i nie traktuje on spacji jako elementu wprowadzanego adresu e-mail. Wyjdźmy zatem naprzeciw takim sytuacjom i usuńmy te nadmiarowe spacje. Jeśli tak przetworzony adres przejdzie testy walidacji to adres (już bez spacji) można przesłać na serwer… w celu drugiej – właściwej walidacji.

Jak widać powstał nam całkiem spory artykuł omawiający teoretycznie tak trywialny problem jak sprawdzenie adresu e-mail. Osobiście uważam, że wyrażenia regularne to bardzo dobry sposób walidacji różnego rodzaju danych, jednakże zawsze należy tworzyć je z rozsądkiem aby znaleźć kompromis między wydajnością RegExp, a faktycznymi możliwymi błędami wprowadzanych przez użytkownika danych.

I na koniec mała uwaga praktyczna… Zawsze dokładnie przeanalizuj swoje testy. Użytkownicy mają tak wiele pomysłów jak źle coś wpisać, że w praktyce nie sposób chyba wyłapać wszystkie możliwości. Proste formularze na stronach www to jeszcze nie problem, ale gdy wchodzimy w bardziej zaawansowane aplikacje, gdzie użytkownik wprowadza kilkanaście danych to na prawdę mogą dziać się cuda 🙂 Powinniśmy w miarę możliwości elegancko obsłużyć błędy i poinformować użytkownika o popełnionych błędach.

Czasami warto również spojrzeć na niektóre sprawy z boku, jak chociażby na kwestię dopuszczenia polskich liter w adresach e-mail. Byłoby to poprawne technicznie i zgodne ze specyfikacjami, ale pytanie, ile takich adresów będą stanowiły adresy faktycznie zawierające znaki diaktryczne, a ile z nich to będą po prostu błędnie wpisane e-maile?