Пять шлюзов — почему?

Одна проверка регулярным выражением ничего не говорит. 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 автоматически сохраняет собранные контакты — с той же дедупликацией и верификацией через журнал чеков. Контакты попадают в базу данных, даже если модель отвлеклась, контекст сжат или сессия упала.