Zapraszam na kolejny wpis gościnny na moim blogu! Jego temat - walidacja e-mail za pomocą Regex’ów - to na pewno coś co warto znać. Autorem posta jest Tomek Sochacki, z wykształcenia inżynier budownictwa, a obecnie programista, specjalizujący się w projektowaniu relacyjnych systemów bazo-danowych, pasjonat JavaScript oraz RegExp. Bloga Tomka możesz znaleźć tutaj.

W artykule omówimy zastosowanie wyrażeń regularnych w celu walidacji adresu e-mail, np. w formularzu kontaktowym, w czasie rejestracji użytkownika, w aplikacjach CRM itp.

Na wstępie należy jednak pamiętać o jednej, ważnej 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 aby móc w pełni świadomie kontrolować cały proces analizy adresu e-mail.

Proponowane we wpisie wyrażenie regularne nie jest zaprzeczeniem gotowych bibliotek typu jQuery Validate. Zapewnia ono jednak pełną kontrolę nad procesem walidacji zgodnie z przyjętymi przez programistę zasadami (a nie ogólnymi standardami). Jego niewątpliwą zaletą jest to, że nie wymaga podciągania kolejnej biblioteki do sprawdzenia jednego czy dwóch pól formularza.

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.

W moich przykładach uznaję zatem, że poprawny adres to taki, który zawiera tylko ww. elementy (litery, cyfry, kropki, @, myślnik, podkreślenie). Ponadto 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-Za-z] lub cyfry - na początku nie może znajdować się żaden inny znak.

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 można w tym miejscu używać skróconej formy regexp: \w.

Obecnie w nazwach domen można również stosować znaki diaktryczne, czyli w naszym wypadku tzw. “polskie znaki”. 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. 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.

Pamiętajmy więc, aby znaleźć rozsądny kompromis pomiędzy faktycznymi zasadami tworzenia danych (standard RFC dla e-mail), a możliwymi przypadkowymi błędami użytkownika.

Na początek opracujmy zestaw testów

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.

Na potrzeby dalszych rozważań 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().

const 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'];

const 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 początku domeny
                 'łukasz@kórnik.gov'];  //polskie znaki diaktryczne

Walidacja e-mail - pierwsze wyrażenie regularne

Zaczniemy od wzorca regexp, który dość często przewija się w wielu poradnikach i książkach poświęconych językowi JavaScript. Jest ono krótkie i stosunkowo proste w zrozumieniu lecz niestety posiada również pewne wady:

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

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

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

Oto wyniki dla powyższych testów:

/// Testy pozytywne: (oczekiwane TRUE)
true "tomek@drogimex.pl"
true "tomek.sochacki@drogimex.pl"
true "tomek@urzad.gov.pl"
true "ToMeK@drogimex.pl"
true "tomek123@domena111.pl"
true "tomek_123@domena.pl"
true "11tomek@domena.pl"

// Testy negatywne: (oczekiwane FALSE)
true "-tomek@domena.net"
true "_tomek@domena.pl"
false "tomek.drogimex.pl"
false "@domena.pl"
false "tomek@domena"
false "tomek@domena."
true "tomek@-domena.pl"
true "tomek@_domena.pl"
false "łukasz@kórnik.gov"

Analiza testów dla pierwszego wzorca RegExp

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ą.

Zmodyfikowane wyrażenie sprawdzające e-mail

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

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

Omówmy dokładnie zmiany jakie wprowadziliśmy we wcześniejszej wersji wzorca regexp.

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.

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ć błąd użytkownika wprowadzającego adres, 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.

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().

Testujemy drugą wersję wzorca RegExp

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

// Testy pozytywne: (oczekiwane TRUE)
true "tomek@drogimex.pl"
true "tomek.sochacki@drogimex.pl"
true "tomek@urzad.gov.pl"
true "ToMeK@drogimex.pl"
true "tomek123@domena111.pl"
true "tomek_123@domena.pl"
true "11tomek@domena.pl"

// Testy negatywne: (oczekiwane FALSE)
false "-tomek@domena.net"
false "_tomek@domena.pl"
false "tomek.drogimex.pl"
false "@domena.pl"
false "tomek@domena"
false "tomek@domena."
false "tomek@-domena.pl"
false "tomek@_domena.pl"
false "łukasz@kórnik.gov"

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

Walidacja e-mail - podsumowanie

Zastanówmy się jednak czy to na pewno koniec testów? Otóż warto jeszcze wspomnieć o pewnym szczególe. Jeśli w polu input (typu “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).

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.

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 użytkownika.

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?