Пять шлюзов — почему?
Одна проверка регулярным выражением ничего не говорит. Email может пройти валидацию синтаксиса, разрешить MX-записи и всё равно вернуться — потому что почтовый ящик не существует, домен catch-all или адрес является алиасом отдела, который никогда не читает холодные письма. Каждый шлюз устраняет свой класс ложных срабатываний.
SMTP-рукопожатие
Опциональный пятый шлюз подключается к лучшему MX-хосту на порт 25 с 10-секундным таймаутом. Диалог: читаем баннер (220), говорим HELO, отправляем MAIL FROM, затем RCPT TO — реальный сигнал доставимости. Сервер отвечает 250 (принято) или 5xx (отклонено). Мы никогда не отправляем содержимое сообщения — только конверт. Ответ 4xx означает greylisting, который мы помечаем как inconclusive, а не false-negative.
Журнал чеков
Каждая проверка записывает типизированный чек в append-only JSONL-файл. Ключ — (тип, значение) — поэтому PASS по email_smtp никогда не замаскирует FAIL по email_domain_mx. Когда save_contacts запускается, он обращается к журналу: SMTP принял? → Verified. Синтаксис+MX ок? → Partial. Иначе → Unverified. Так мы гарантируем честные метки статуса.
Автосейв: страховочная сетка
LLM не всегда помнит вызвать save_contacts. Поэтому после каждого вызова extract_contacts или find_leads runtime автоматически сохраняет собранные контакты — с той же дедупликацией и верификацией через журнал чеков. Контакты попадают в базу данных, даже если модель отвлеклась, контекст сжат или сессия упала.