Карта дня, май, перетяжка
Карта дня, май, перетяжка
Карта дня, май, перетяжка

Как проверять email адрес на валидность правильно

Аватар Пётр Соковых
Отредактировано

44К открытий46К показов
Как проверять email адрес на валидность правильно

Предположим, у вас есть простая задача — создать форму, которая даст пользователю возможность подписаться на e-mail оповещения. Разумеется, вам необходимо предотвратить ввод в эту форму всякого мусора, при этом не должно получаться так, чтобы валидный адрес вдруг был забракован системой.

Как же выглядит e-mail адрес? Интуитивно можно предположить, что так:

			// Пример адреса:
johndoe@example.com
 
// В виде составных частей:
${MAILBOX}@${SUBDOMAIN}.${TLD}
 
// Регулярка, которую можно было бы использовать
[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+
		

Выглядит хорошо, но это совершенно не тот случай, когда стоит доверять интуиции. Доверять следует спецификации. А спецификация говорит нам следующее:

			// Пропущен домен? Всё нормально, письмо уйдёт на локаль!
john-doe
 
// Да, это тоже валидно, но такого ящика не существует с вероятностью 99%
john-doe@com
 
// И это валидно, но доменов из одной буквы не существует
john-doe@example.c
 
// Оу, мы кажется забыли про субдомены. Мы же не можем просто забыть про .co.uk?
john@doe.example.com
 
 
// Да, IP адрес в качестве сервера это тоже нормально
john@[192.168.1.1]
 
// По спецификации подходит, но такой домен не может быть зарегистрирован
john@-doe.com
 
// А ещё можно делать так:
john.doe@example.com
 
// Но нельзя так:
john..doe@eaxmple.com
.john@example.com
john.@example.com
		

Конечно, и тут можно обойтись с помощью регулярного выражения:

			(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])
		

Но с этим есть несколько проблем:

  • Как вы будете проверять правильность этого монстра? Такие регулярные выражения переходят «из уст в уста» на форумах, и каждый добавляет в них функциональность до тех пор, пока работа с выражением становится невозможной. И это именно тот случай.
  • Я бы не сказал, что регулярные выражения такой длины эффективнее, чем другие методы. Чем длиннее выражение, тем дольше оно будет компилироваться (сравнение всегда происходит за O(n)).
  • С помощью регулярного выражения можно сделать только проверку на соответствие. Выполнить проверку на то, находится ли домен в чёрном списке, у вас уже, увы, не получится.

Давайте пойдём другим путём

Вот основа нашей проверки:

			char[] input; // Строка, которую нужно проверить
int index = 0; // Итератор для input
char ch; // Текущий символ (input[index])
int state = 0; // Состояние проверки (-1 для ошибки)
 
while (index <= input.length && state != -1) {
 
	if (index == input.length) {
		ch = '\0'; // Символ, по которому проверка прекращается
	}
	else {
		ch = input[index];
	}
 
	switch (state) {
		// case 0: {...
	}
 
	index++;
}
		

А вот диаграмма, описывающая алгоритм, по которому наша программа будет работать:

Как проверять email адрес на валидность правильно 1

Если вы заметили, что тут не все правила соблюдаются — не волнуйтесь, мы вернёмся к этому позже. Если вы не заметили, и вообще не понимаете, что тут происходит, то сейчас объясню.

Вершина графа — состояние проверки. Ребро графа — прочитанный символ. Если в результате считывания символа невозможно перейти ни по одному ребру, значит, адрес не валиден. Вот, например, как будет реализована первая часть этого алгоритма:

Как проверять email адрес на валидность правильно 2
			case 0: {
	if (ch >= 'a' && ch <= 'z') {
		count++; 
		break; // State stays the same
	}
	if (ch == '\0') {
		state = 1; // EOL
		break;
	}
	state = -1; // Error
	break;
}
		

Теперь о проверках, которые мы сделаем, после того, как код пройдёт по этой диаграмме:

			public class Validator {
 
	public static boolean isValid(final char[] input) {
		int state = 0;
		char ch;
		int index = 0;
		String local = null;
		ArrayList<String> domain = new ArrayList<String>();
 
		// Код проверки по диаграмме
		// [..]
 
		// Не прошло валидацию
		if (state != 6)
			return false;
 
		// Домен должен быть по меньшей мере второго уровня
		if (domain.size() < 2)
			return false;
 
		// RFC 5321 ограничивает длину имени ящика до 64 символов
		if (local.length() > 64)
			return false;
 
		// RFC 5321 ограничивает длину адреса до 254 символов
		if (input.length > 254)
			return false;
 
		// Доменная зона должна состоять только из букв и быть по меньше мере два символо длинной
		index = input.length - 1;
		while (index > 0) {
			ch = input[index];
			if (ch == '.' && input.length - index > 2) {
				return true;
			}
			if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))) {
				return false;
			}
			index--;
		}
 
		return true;
	}
}
		

Собираем всё вместе

			public class EmailValidator {
 
	public static boolean isValid(final char[] input) {
		if (input == null) {
			return false;
		}
 
		int state = 0;
		char ch;
		int index = 0;
		int mark = 0;
		String local = null;
		ArrayList<String> domain = new ArrayList<String>();
 
		while (index <= input.length && state != -1) {
			
			if (index == input.length) {
				ch = '\0'; // Так мы обозначаем конец нашей работы
			}
			else {
				ch = input[index];
				if (ch == '\0') {
					// символ, которым мы кодируем конец работы, не может быть частью ввода
					return false;
				}
			}
 
			switch (state) {
 
				case 0: {
					// Первый символ {atext} -- текстовой части локального имени
					if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
							|| (ch >= '0' && ch <= '9') || ch == '_' || ch == '-'
							|| ch == '+') {
						state = 1;
						break;
					}
					// Если встретили неправильный символ -> отмечаемся в state об ошибке
					state = -1;
					break;
				}
 
				case 1: {
					// Остальные символы {atext} -- текстовой части локального имени
					if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
							|| (ch >= '0' && ch <= '9') || ch == '_' || ch == '-'
							|| ch == '+') {
						break;
					}
					if (ch == '.') {
						state = 2;
						break;
					}
					if (ch == '@') { // Конец локальной части
						local = new String(input, 0, index - mark);
						mark = index + 1;
						state = 3;
						break;
					}
					// Если встретили неправильный символ -> отмечаемся в state об ошибке
					state = -1;
					break;
				}
 
				case 2: {
					// Переход к {atext} (текстовой части) после точки
					if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
							|| (ch >= '0' && ch <= '9') || ch == '_' || ch == '-'
							|| ch == '+') {
						state = 1;
						break;
					}
					// Если встретили неправильный символ -> отмечаемся в state об ошибке
					state = -1;
					break;
				}
 
				case 3: {
					// Переходим {alnum} (домену), проверяем первый символ
					if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
							|| (ch >= 'A' && ch <= 'Z')) {
						state = 4;
						break;
					}
					// Если встретили неправильный символ -> отмечаемся в state об ошибке
					state = -1;
					break;
				}
 
				case 4: {
					// Собираем {alnum} --- домен
					if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
							|| (ch >= 'A' && ch <= 'Z')) {
						break;
					}
					if (ch == '-') {
						state = 5;
						break;
					}
					if (ch == '.') {
						domain.add(new String(input, mark, index - mark));
						mark = index + 1;
						state = 5;
						break;
					}
					// Проверка на конец строки
					if (ch == '\0') {
						domain.add(new String(input, mark, index - mark));
						state = 6;
						break; // Дошли до конца строки -> заканчиваем работу
					}
					// Если встретили неправильный символ -> отмечаемся в state об ошибке
					state = -1;
					break;
				}
 
				case 5: {
					if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
							|| (ch >= 'A' && ch <= 'Z')) {
						state = 4;
						break;
					}
					if (ch == '-') {
						break;
					}
					// Если встретили неправильный символ -> отмечаемся в state об ошибке
					state = -1;
					break;
				}
 
				case 6: {
					// Успех! (На самом деле, мы сюда никогда не попадём)
					break;
				}
			}
			index++;
		}
 
		// Остальные проверки
 
		// Не прошли проверку выше? Возвращаем false!
		if (state != 6)
			return false;
 
		// Нам нужен домен как минимум второго уровня
		if (domain.size() < 2)
			return false;
 
		// Ограничения длины по спецификации RFC 5321
		if (local.length() > 64)
			return false;
 
		// Ограничения длины по спецификации RFC 5321
		if (input.length > 254)
			return false;
 
		// Домен верхнего уровня должен состоять только из букв и быть не короче двух символов
		index = input.length - 1;
		while (index > 0) {
			ch = input[index];
			if (ch == '.' && input.length - index > 2) {
				return true;
			}
			if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))) {
				return false;
			}
			index--;
		}
 
		return true;
	}
}
		
Следите за новыми постами
Следите за новыми постами по любимым темам
44К открытий46К показов