Один пользователь зарегистрировался у вас четыре раза - и во всех случаях формально законно
Дубли в пользовательской базе не всегда появляются из-за ошибок регистрации, сбоев или намеренного обхода ограничений. Иногда причина гораздо прозаичнее: система сравнивает адреса электронной почты как обычные строки.
В результате один человек может создать несколько учётных записей, а база будет считать их принадлежащими разным пользователям. Например:
- `Ivan.Petrov@gmail.com`;
- `ivanpetrov@gmail.com`;
- `ivan.petrov+shop@gmail.com`;
- `IVAN.PETROV@gmail.com`.
Для программного кода это четыре разные последовательности символов. Для Gmail - один почтовый ящик. Все сообщения попадут одному владельцу, а значит, ограничения вроде "акция только для новых клиентов", "один бесплатный период" или "одна заявка на человека" легко обходятся без взлома и сложных манипуляций.
Почему простое сравнение адресов не работает
Электронный адрес состоит из локальной части и домена, разделённых символом `@`. Эти компоненты подчиняются разным правилам, поэтому универсальная операция "перевести всё в нижний регистр и удалить лишние символы" может привести к ошибкам.
Регистр символов
Доменная часть адреса не зависит от регистра: `Example.com`, `example.com` и `EXAMPLE.COM` указывают на один домен.
С локальной частью ситуация сложнее. Формально почтовый стандарт допускает чувствительность к регистру, однако популярные сервисы почти всегда обрабатывают такие адреса без учёта регистра. Поэтому адреса вроде `Ivan.Petrov@gmail.com` и `ivan.petrov@gmail.com` на практике ведут в один ящик.
Если сохранить адрес в исходном виде и использовать посимвольное сравнение, система создаст несколько записей для одного пользователя.
Плюс-адресация
Многие почтовые сервисы поддерживают plus addressing. Символ `+` и текст после него используются как дополнительная метка:
- `ivan.petrov+shop@gmail.com`;
- `ivan.petrov+promo@gmail.com`;
- `ivan.petrov+test@gmail.com`.
Письма всё равно поступят на основной ящик `ivan.petrov@gmail.com`. Такая возможность полезна для сортировки корреспонденции и поиска источника утечки адреса, но одновременно позволяет создавать практически неограниченное количество вариантов для регистрации.
Удалять суффикс после `+` или нет - решение владельца продукта. Если цель состоит в защите промоакций и лимитов, такую часть адреса часто считают незначимой. Но в некоторых сервисах пользователи сознательно применяют разные метки для рабочих и личных процессов, поэтому бездумная очистка способна вызвать недовольство и ошибки.
Точки в адресах Gmail
В Gmail точки в локальной части адреса не влияют на доставку. Поэтому:
- `ivan.petrov@gmail.com`;
- `ivanpetrov@gmail.com`;
- `i.v.a.n.p.e.t.r.o.v@gmail.com`
рассматриваются как один почтовый ящик.
Однако это правило нельзя автоматически переносить на все домены. Для других провайдеров точка может быть значимой частью имени. Если удалять точки у каждого входящего адреса, можно ошибочно объединить учётные записи разных людей.
Отдельная проблема - похожие символы
Не каждый визуально одинаковый адрес является технически одинаковым. В домене могут использоваться символы из разных алфавитов, например кириллическая `е` вместо латинской `e`.
Такой адрес выглядит привычно, но фактически ведёт на другой домен. Это уже не просто проблема дублей, а потенциальный инструмент мошенничества: злоумышленник может зарегистрироваться под видом сотрудника, поставщика или известной компании.
Для проверки международных доменов применяют IDNA-кодирование. После преобразования подозрительный домен часто получает вид `xn--...`. Это не означает автоматически, что адрес вредоносный, но требует дополнительного внимания. Особенно важно проверять подобные значения при регистрации корпоративных аккаунтов, подключении контрагентов и обработке заявок от имени организаций.
Как правильно хранить email
Надёжная модель обычно предполагает два отдельных поля:
- `email` - исходное значение, введённое пользователем;
- `email_normalized` - техническая форма для поиска, сравнений и ограничений.
Первое поле нужно для отображения и отправки писем. Второе - для уникального индекса и проверки существующих аккаунтов.
Показывать человеку нормализованное значение не стоит. Если пользователь ввёл адрес с заглавными буквами или плюсом, а система внезапно отображает изменённую строку, он может решить, что сервис испортил его данные.
На поле `email_normalized` следует установить уникальное ограничение на уровне базы данных. Проверка в приложении сама по себе недостаточна: два параллельных запроса могут одновременно пройти условие "такого адреса ещё нет" и создать дубликаты.
Нормализация должна учитывать домен
Безопасный базовый алгоритм может включать:
1. удаление пробелов по краям;
2. приведение доменной части к нижнему регистру;
3. приведение локальной части к нижнему регистру там, где это соответствует реальному поведению провайдера;
4. отдельную обработку plus addressing по заранее принятой политике;
5. удаление точек только для тех доменов, где такое правило подтверждено.
Список доменов и правила их обработки лучше хранить явно, а не зашивать в большое условие внутри кода. Поведение почтовых сервисов может отличаться, а универсальная нормализация часто приводит к ложному объединению разных адресов.
Важно заранее определить, что именно считается одним пользователем. Электронный адрес не всегда является надёжным идентификатором человека: у одного владельца может быть несколько ящиков, а одним рабочим ящиком иногда пользуется целый отдел. Поэтому ограничения на акции и бонусы желательно дополнять другими механизмами контроля.
Почему не стоит писать сложную регулярку
Полная грамматика email-адреса сложнее большинства регулярных выражений, которые используют в формах регистрации. Слишком строгая проверка может отклонить действительный адрес, а слишком мягкая - пропустить значение, на которое невозможно доставить письмо.
Практичная проверка обычно ограничивается базовыми условиями:
- в строке есть ровно один символ `@`;
- обе части не пустые;
- отсутствуют пробелы и управляющие символы;
- домен имеет допустимую структуру;
- адрес подтверждён письмом.
Письмо с кодом или ссылкой подтверждения надёжнее любой регулярки: оно показывает, что пользователь действительно контролирует этот ящик.
Как найти уже существующие дубли
После добавления нормализации необходимо проверить старые записи. Полезно сгруппировать аккаунты по домену и нормализованному адресу, а затем найти группы, в которых исходных записей больше одной.
Упрощённая логика запроса выглядит так:
```sql
SELECT email_normalized, COUNT(*) AS accounts
FROM users
GROUP BY email_normalized
HAVING COUNT(*) > 1;
```
Если нормализованное поле ещё не заполнено, его сначала потребуется рассчитать по единым правилам. Важно сохранить исходные адреса: они помогут разобраться, почему записи были объединены и не произошло ли ошибочное совпадение.
Для анализа маркетинговых злоупотреблений полезно дополнительно сравнить:
- количество аккаунтов и подтверждённых адресов;
- даты регистраций;
- IP-адреса и устройства;
- историю получения бонусов;
- номера телефонов и платёжные реквизиты;
- домены, на которых чаще всего встречаются варианты с `+` и точками.
При этом одинаковый адрес сам по себе не доказывает мошенничество. Он лишь показывает, что несколько аккаунтов контролируются одним почтовым ящиком.
Как не сломать существующих пользователей
Изменение правил нормализации нельзя внедрять без миграции и обратной совместимости. Сначала следует провести аудит в режиме отчёта, оценить количество конфликтов и вручную проверить спорные случаи.
Если найдены совпадающие аккаунты, можно:
- объединить их после подтверждения личности;
- оставить одну активную запись, а остальные перевести в архив;
- перенести историю заказов и бонусов;
- запросить повторное подтверждение адреса;
- уведомить пользователя о найденном конфликте.
Автоматически удалять дубли опасно: за похожими адресами могут стоять разные люди, особенно если речь идёт не о Gmail, а о корпоративном или региональном почтовом домене.
Что проверить в новой реализации
Перед запуском обновлённой логики стоит протестировать несколько сценариев:
- разные варианты регистра;
- адреса с пробелами в начале и конце;
- plus addressing;
- точки в Gmail;
- точки в доменах других провайдеров;
- IDN-домены и похожие символы;
- Unicode-символы;
- повторную регистрацию после подтверждения;
- одновременные запросы к форме регистрации.
Также важно покрыть тестами границу между локальной частью и доменом. Ошибка в разборе строки до символа `@` может привести к тому, что приложение начнёт удалять плюсы или точки там, где этого делать нельзя.
Правильная работа с email - это не простое приведение строки к нижнему регистру. Необходимо учитывать особенности конкретных почтовых сервисов, разделять отображаемое и техническое значение, использовать уникальные ограничения базы и осторожно проводить миграцию. Тогда один и тот же почтовый ящик не сможет незаметно превратиться в четыре, десять или сто "новых пользователей".
