Привет, Хабр. Меня зовут Денис.

У меня уже был опыт с 152-ФЗ на другом проекте — умный поиск вакансий, там перед эмбеддингами стоял PII Guard на natasha + регулярках. Казалось, я уже примерно понимаю, куда класть минимизацию. Потом я взялся за почту.

Продукт называется MailBro. Несколько ящиков, сводка «что сделать сегодня», сроки, черновик ответа. Человек подключает Яндекс / Mail.ru / Gmail / Exchange паролем приложения, мы тащим письма, гоняем через модель, отдаём приоритет и черновик. Отправка только после ручного подтверждения — про это ниже чуть-чуть, но не в этом главная боль.

Главная боль в том, что я почти повесил на главную фразу «мы соблюдаем 152-ФЗ», потому что перед моделью вырезаю паспорт и карту. Юрист потом смотрел на меня так, будто я предложил закрыть налоговую красивым CSS.

Ниже — что именно сломалось в голове, какой код получился и где я до сих пор не буду делать вид, что всё сшито.

С чего началась самоуверенность

Первая версия Privacy Gateway выглядела «достаточно». Берём subject + body_text, режем тело до бюджета (у меня сейчас AI_CLASSIFY_CHAR_BUDGET=3000 символов — иначе одно письмо с простынёй съедает разбор целиком), гоняем детектор, подменяем находки плейсхолдерами вида [PERSON_001], [PHONE_002], [EMAIL_001]. Маппинг оригинал↔плейсхолдер кладём в отдельную таблицу, шифруем через Laravel Crypt, TTL маппинга короче жизни тела: тело письма у нас живёт 48 часов (MAIL_BODY_TTL_HOURS), маппинг по умолчанию 24 (PRIVACY_MAPPING_TTL_HOURS).

Дальше модель видит уже дырявый текст. Ответ модели мы восстанавливаем обратно через restore(), чтобы в карточке у человека снова были нормальные имена, а не [PERSON_001]. В промпте жёстко: плейсхолдеры непрозрачные, угадывать ФИО запрещено.

На этом месте у меня в голове щёлкнуло: «ну всё, ПДн не уезжают». Хотелось бейдж на лендинг.

Потом я открыл приказ №140

После 01.09.2025 обезличивание — это не «мы замазали ФИО». Критерий: без дополнительной информации нельзя понять, чьи это данные.

Свободный текст письма этому почти никогда не удовлетворяет. Вычеркнули Ивана Ивановича — осталось «договор с ООО „Вектор“ на 12,4 млн, оплатить до пятницы, ждём на объекте на Варшавке». Человека всё равно вычислят. Плейсхолдер [CARD_001] не делает письмо анонимным набором слов.

Юридически то, что я делаю, ближе к минимизации / маскированию / псевдонимизации. Не к обезличиванию. В подвале сайта я специально оставил скучную формулировку про метки. Маркетолог внутри меня каждый раз хочет её «улучшить». Не даю.

И да: в статье про Smart Search я когда-то сам писал «обезличенный текст» про тот же приём. Сейчас бы так не написал. Там резюме, тут чужая переписка — цена ошибки другая.

Что детектим и что режем навсегда

Детектор у меня пока на регулярках и ключевых словах. Без иллюзий: это не NLP-лаборатория. Зато поведение предсказуемое и тестируемое.

Классы примерно такие:

Класс

Что ловим

Что делаем

PERSON

ФИО кириллицей

→ [PERSON_00N]

PHONE

+7 / 8…

→ [PHONE_00N]

EMAIL

обычный email

→ [EMAIL_00N]

TAX_ID

ИНН 10/12

→ плейсхолдер

SNILS

СНИЛС

→ плейсхолдер

ADDRESS

ул./г./пр-т…

→ плейсхолдер

PASSPORT

серии/номера

письмо в модель не уходит

CARD

13–19 цифр + Луна

не уходит

ACCOUNT

20 цифр р/с

не уходит

AUTHENTICATION

пароль, otp, cvv, 2fa, «код подтверждения»…

не уходит

SPECIAL

куски про здоровье/судимость/веру и т.п. по ключевым словам

не уходит

CORPORATE_SECRET

«коммерческая тайна», BEGIN PRIVATE KEY, api_key…

не уходит

Вот это важное. Не всё маскируется. Часть классов — fail-closed: PrivacyDecision.allowed = false, состояние письма уходит в AiSkipped, в аудит пишется privacy_blocked и счётчики сущностей. Денег не списываем. Модель вообще не вызывается.

Почему так жёстко? Потому что «замаскировать паспорт как [PASSPORT_001]» всё равно оставляет рядом контекст, из которого часто понятно, чей это паспорт. Я лучше потеряю один разбор, чем отправлю такое наружу.

Минусы честно: ложные срабатывания есть. ИНН из десяти цифр иногда путается с чем попало. ФИО-регексп ловит «Новый Год» и подобные конструкции. SPECIAL по словам — грубый. На этом месте хочется воткнуть NER покруче. Пока живу с регулярками и правлю кейсы по мере боли.

Пайплайн одного письма, без слайдов

Грубо:

IMAP fetch
  → Message (state …)
  → PrivacyGateway::prepare()
       allowed=false → AiSkipped, audit privacy_blocked, план без LLM
       allowed=true  → AiQueued
  → RoutingAiProvider (yandex/* или зарубежный шлюз)
  → schema-valid JSON
  → restore плейсхолдеров
  → карточка / черновик / лейблы

Деньги: кошелёк трогаем только если провайдер вернул валидный по схеме результат. Упал шлюз, кривой JSON, privacy block — пользователю ноль. Иначе боты и ретраи жрут бюджет в пустоту; на вакансиях я уже наступал.

Роутинг моделей отдельный слой. Префикс yandex/ идёт в Yandex AI Studio (обработка в РФ, в запросах x-data-logging-enabled: false). Остальное — через зарубежный OpenAI-совместимый шлюз, и только если пользователь сам так выбрал: на ящик, на сводку или по умолчанию.

Самое вкусное, что я себе запретил: fallback «Яндекс лёг → давайте пока GPT, пользователь не заметит». В RoutingAiProvider это просто AiGatewayFailed::notConfigured(...). Точка. Сводка пустая, поддержка орёт, зато в момент инцидента я не объясняю, почему рабочий ящик уехал за рубеж без выбора человека.

GigaChat в этом продукте live не включал. Со стандартной офертой под чужие ПДн там своя история, я её уже разбирал с юристом на прошлом заходе. Отечественный лейбл сам по себе ничего не закрывает.

Ещё четыре грабля, уже не про код

«Шлюз — российское ООО, базы в РФ → трансгранички нет».
Нет. Если модель за рубежом, ст. 12 никуда не делась от того, что посередине стоит российская обёртка.

«Провайдер пишет, что соблюдает принципы 152-ФЗ».
Его заявление ≠ ваше поручение на обработку. Без основания передача шлюзу — передача ПДн третьему лицу.

«Пользователь нажал „подключить ящик“ — согласие на всю переписку».
На корреспондентов согласие пользователя не работает. Иван не согласился за Петра. У меня основание для их данных — законный интерес (п. 7 ч. 1 ст. 6), и его ещё надо уметь объяснить, а не повесить в подвал сайта.

Админка «ну глянь письмо, там баг».
Тела писем в staff-админке нет. Чиним на синтетике и на логах без контента: latency, model id, class ошибки, entity counts. Иначе админка быстро становится ещё одним почтовым клиентом со всеми ящиками сразу.

Мелочь, которую тоже чуть не сломали «для удобства»: календарная подписка (человек сам добавляет URL в Яндекс/Google/Apple) отдаёт дату и строку «MailBro: срок по письму» + ссылку на задачу. Тему и отправителя туда не кладу. Один тикет «хотя бы тему» закрыл. Тема письма в чужом календаре — часто уже ПДн корреспондента.

Что из этого торчит наружу в продукте

  • Российские модели по умолчанию для новых. Зарубежные дешевле и остаются выбором.

  • Тихого переключения маршрута нет.

  • Тела короткоживущие.

  • Отправка письма — только после человека. Черновик можно, автоответчик я себе не разрешил: один «уверенный» автоответ на письмо из суда стоит дороже года разговоров про магию. Для IMAP это ещё и пароль приложения, не «полный OAuth на весь аккаунт» — основной пароль я не прошу и не храню.

  • В пушах по умолчанию нет отправителя и темы. В Telegram подробностей нет принципиально.

Цены и лимиты лучше смотреть на экране тарифа: я их тут высекать не буду, правят без переписывания Хабра.

Сам сервис: https://mailbro.ru. Справка с деталями по ящикам и моделям лежит в /help.

Честный хвост

Уведомление РКН, поручения, трансграничка при выборе зарубежной модели — это не абзац «мы используем современных провайдеров». Часть бумаги у меня ещё в работе. Минимизация снижает объём и вероятность утечки. Она не превращает продукт в «152-ФЗ compliant» бейдж.

Если делаете ИИ поверх чужих документов в РФ: не пишите на лендинге то, чего нет в договорах. И не делайте fallback в другую страну «на время аварии». Авария — не согласие пользователя.

Буду рад в комментариях, как у вас устроен fail-closed на границе с LLM. У меня пока регулярки, шифрованный маппинг и много запретов. Запреты, как ни странно, держат продукт лучше слоганов.