GoEmailVerifier — практикум
См. также: Первая программа · TCP и UDP · Тестирование · CLI на cobra · GoHTMLParser.
О практикуме
GoEmailVerifier — консольная утилита, которая отвечает на вопрос "можно ли доставить письмо на этот адрес?" без отправки самого письма. Проверка идёт в три слоя:
- Синтаксис — строка похожа на email (
user@domain.tld). - DNS — у домена есть MX-записи, указывающие на почтовые серверы.
- SMTP — один из MX-серверов принимает или отклоняет адрес через команду
RCPT TO.
Проект целиком на стандартной библиотеке Go — flag, net, net/smtp, regexp, testing. Сторонних зависимостей нет — весь код собирается по шагам на этапах 0–6 ниже.
Соседний GoHTMLParser — другой сетевой сценарий (HTTP GET и парсинг HTML), но та же идея "маленькая CLI-утилита на stdlib".
Нужны Go 1.22+, пройденная первая программа и базовое понимание обработки ошибок. Сеть — TCP и UDP; модули — go mod; тесты — статья про testing.
Ключевые термины
| Термин | Простыми словами |
|---|---|
Адрес вида локальная_часть@домен; локальная часть — имя ящика, домен — зона, где ищут почтовые серверы | |
| DNS | Система имён: по домену gmail.com возвращает записи разных типов (A, MX, TXT…) |
| MX-запись | Mail eXchange — "куда слать почту для этого домена"; у записи есть приоритет (меньше число — важнее сервер) |
| SMTP | Протокол передачи почты между серверами; работает поверх TCP, обычно порт 25 |
| RCPT TO | SMTP-команда "кому письмо"; сервер отвечает OK или кодом ошибки (например 550) |
| Null-sender | Пустой отправитель в MAIL FROM <> — часто используют при проверке без реальной отправки |
| CLI | Command Line Interface — программа, которой передают аргументы и флаги в терминале |
Теория протоколов на уровне интеграций — HTTP как основа веб-интеграций; здесь мы опускаемся на уровень TCP и прикладного SMTP.
Как устроена доставка почты
Когда вы отправляете письмо с you@yourmail.com на friend@gmail.com, происходит примерно следующее:
- DNS не знает, существует ли конкретный ящик — только где искать почтовый сервер домена.
- SMTP может сказать "ящик есть" или "нет", но многие серверы намеренно не раскрывают это незнакомым IP (anti-harvesting).
- Утилита учебная: она показывает работу
net.LookupMX,net/smtpи обработку ошибок в Go, а не продуктовую верификацию базы контактов.
Многие почтовые серверы не отвечают на RCPT TO с чужих IP или всегда возвращают "OK". Порт 25 часто закрыт у домашних провайдеров. Результат "вероятно существует" — не юридическое доказательство доставляемости.
Чему научитесь
После практикума вы сможете:
- инициализировать Go-модуль и собрать один бинарник через
go build; - разложить код по файлам одного пакета
mainбез преждевременного усложнения; - вызывать DNS из Go и интерпретировать MX-записи;
- установить TCP-соединение с таймаутом и пройти минимальный SMTP-диалог;
- описать результат проверки структурой с полем-указателем (
*boolдля трёх состояний); - принять аргументы через пакет
flagи вернуть ненулевой exit code; - написать table-driven тест для чистых функций.
Архитектура проекта
| Файл | Роль |
|---|---|
main.go | CLI — флаги -timeout, -mx, вывод результата, код выхода |
verifier.go | Структура Result, тип Verifier, метод Verify, ParseDomain, PrintMX |
dns.go | lookupMX — DNS-запрос и сортировка MX по приоритету |
smtp.go | verifySMTP — TCP, EHLO, MAIL FROM, RCPT TO |
verifier_test.go | Table-driven тесты regex и ParseDomain |
Почему не один файл? Каждый слой (CLI, оркестрация, DNS, SMTP) меняется по своим причинам — так проще читать и тестировать. Пока всё в package main; выделение в internal/ имеет смысл, когда появятся переиспользуемые пакеты — см. GoHTMLParser.
Для одной команды и двух флагов достаточно flag из stdlib. Дерево подкоманд, конфиг из YAML и shell completion — CLI на cobra и viper.
Как проходить практикум
- Создайте каталог и выполните
go mod init(этап 0). - Идите по этапам 0–6 ниже — после каждого шага запускайте
go run .илиgo test. - На этапе 2 проверьте
-mx google.com— в терминале должен появиться список MX с приоритетами. - На этапе 5 соберите бинарник
go build -o email-verifier .и вызовитеuser@gmail.com. - Отметьте пункты "Самопроверка" в конце каждого этапа.
- Если SMTP падает по timeout — это нормально для домашней сети; DNS-часть (
-mx) всё равно должна работать.
Карта этапов
| Этап | Фокус | Результат |
|---|---|---|
| 0 | Модуль Go | Каталог с go.mod, первый запуск |
| 1 | Модель данных | Result, regex, NewVerifier |
| 2 | DNS | lookupMX в dns.go, команда PrintMX |
| 3 | SMTP | verifySMTP в smtp.go |
| 4 | Verifier | Полный Verify, ParseDomain, вывод |
| 5 | CLI | main.go с flag и exit code |
| 6 | Тесты | verifier_test.go, go test |
Этап 0 — модуль и каркас
Цель — рабочий Go-модуль с одним файлом и командой go run.
Теория
Go-модуль — каталог с файлом go.mod, в котором указаны имя модуля и версия Go. С Go 1.16+ модули — стандартный способ организации проекта; старый $GOPATH для новых учебных проектов не нужен. Подробнее — Модули и go mod, Первая программа.
Пакет main — единственный, из которого собирается исполняемый файл. Функция func main() — точка входа, аналог public static void main в Java, но без класса-обёртки — см. package main и func main().
Команды
mkdir email-verifier && cd email-verifier
go mod init github.com/you/email-verifier
mkdirсоздаёт каталог проекта.go mod initзаписывает вgo.modпуть модуля (здесь вымышленныйgithub.com/you/...— для локальной учебной работы подойдёт любой уникальный путь).
Создайте main.go:
package main
import "fmt"
func main() {
fmt.Println("GoEmailVerifier — заготовка")
}
Запуск:
go run .
Точка . означает "текущий модуль" — Go найдёт все .go файлы пакета main и скомпилирует их.
Разбор main.go
| Строка | Смысл |
|---|---|
package main | Исполняемый пакет; имя main — требование toolchain |
import "fmt" | Подключение пакета форматированного вывода из stdlib |
func main() | Начало выполнения программы |
Самопроверка
-
go versionпоказывает Go 1.22+. - В каталоге есть
go.modиmain.go. -
go run .печатает заготовку без ошибок.
Этап 1 — Result и проверка формата
Цель — описать результат проверки структурой и отсечь заведомо неверный синтаксис email.
Теория
Валидация email в продакшене обычно двухуровневая. Общая таблица проверок — Проверка и валидация.
- Синтаксис — строка соответствует шаблону (быстро, офлайн).
- Доставляемость — домен принимает почту, ящик может существовать (медленно, нужна сеть).
На этом этапе делаем только синтаксис. Полный RFC 5322 разбирать не будем — для учебного проекта достаточно regexp, который отсекает явный мусор (@, пробелы, отсутствие домена). Работа со строками — Строки, руны и Unicode.
Поле Exists *bool — указатель на bool, а не сам bool. Так в Go кодируют три состояния:
Значение Exists | Интерпретация |
|---|---|
nil | SMTP ещё не вызывали или не удалось получить ответ |
&true | Сервер принял RCPT TO |
&false | Сервер явно отклонил адрес |
Обычный bool не различает "false" и "не проверяли" — оба дают false.
Код
Создайте verifier.go:
Код ITЗагрузка примера кода…
Временно измените main.go:
package main
import (
"fmt"
"time"
)
func main() {
v := NewVerifier(15 * time.Second)
fmt.Printf("%+v\n", v.Verify("User@Example.COM"))
fmt.Printf("%+v\n", v.Verify("not-an-email"))
}
Разбор Verify на этапе 1
| Фрагмент | Зачем |
|---|---|
regexp.MustCompile | Компиляция шаблона один раз при старте; при ошибке в паттерне — panic (допустимо для константы) |
TrimSpace + ToLower | Нормализация ввода: User@Mail.COM → user@mail.com |
SplitN(..., "@", 2) | Ровно одно разбиение — локальная часть может содержать редкие символы, домен — parts[1] |
return result без Error | Некорректный формат — не exception, а часть доменной модели (ошибки в Go) |
Самопроверка
-
User@Example.COM→Valid: true, доменexample.com, регистр нормализован. -
not-an-email→Valid: false,Detailпро формат. -
Existsпока всегдаnil.
Этап 2 — DNS MX lookup
Цель — по домену получить список почтовых серверов и вывести их с приоритетами.
Теория
DNS (Domain Name System) переводит имена в данные. Для почты нужен тип записи MX (Mail eXchange). У каждой MX-записи два поля:
- Host — hostname сервера (часто с точкой в конце, например
gmail-smtp-in.l.google.com.). - Pref (preference) — приоритет; сервер с меньшим числом пробуют первым.
Если MX нет — домен, скорее всего, не принимает почту по SMTP (или использует A-запись как fallback, что мы здесь не разбираем).
В Go DNS вызывают через пакет net: net.LookupMX(domain) — блокирующий вызов с резолвером ОС. Это тот же уровень стека, что и TCP-сокеты — TCP и UDP в Go.
Код
Создайте dns.go:
Код ITЗагрузка примера кода…
Добавьте в verifier.go функцию PrintMX и импорт "fmt":
// PrintMX выводит MX-записи домена (для демонстрации работы DNS).
func PrintMX(domain string) error {
records, err := lookupMX(domain)
if err != nil {
return err
}
fmt.Printf("MX-записи для %s:\n", domain)
for _, mx := range records {
fmt.Printf(" приоритет %d → %s\n", mx.Pref, strings.TrimSuffix(mx.Host, "."))
}
return nil
}
Проверка из main.go:
func main() {
if err := PrintMX("google.com"); err != nil {
fmt.Println("ошибка:", err)
}
}
Разбор lookupMX
| Строка | Смысл |
|---|---|
net.LookupMX | Запрос к DNS; при NXDOMAIN или сетевой ошибке — err != nil |
fmt.Errorf("...: %w", err) | Обёртка ошибки — цепочка причин, идиома Go (Обработка ошибок в Go) |
len(mxRecords) == 0 | Технически редкий случай, но явная проверка лучше пустого цикла |
sort.Slice + Pref | Сортировка in-place; первый элемент — самый приоритетный MX |
TrimSuffix(mx.Host, ".") | DNS часто возвращает FQDN с trailing dot — для вывода и SMTP убираем |
Пример вывода
Для google.com вы увидите несколько строк с разными приоритетами — это нормально: у крупных доменов несколько MX для отказоустойчивости.
Самопроверка
-
PrintMX("google.com")— список MX с приоритетами. -
PrintMX("домен-которого-нет.invalid")— понятная ошибка DNS. - Функция возвращает
error, а не паникует.
Этап 3 — SMTP-проверка
Цель — установить TCP-соединение с MX-сервером и узнать, принимает ли он адрес получателя.
Теория
SMTP (Simple Mail Transfer Protocol) — текстовый протокол поверх TCP. Минимальный диалог для проверки ящика (без отправки тела письма):
- TCP connect на порт 25 MX-хоста.
- EHLO / HELO — представиться клиентом.
- MAIL FROM — указать отправителя (мы используем пустой
<>— null-sender). - RCPT TO — указать получателя; здесь сервер говорит, есть ящик или нет.
- QUIT — завершить сессию. Команду DATA не вызываем — письмо не отправляем.
Типичные коды ответа:
| Код | Частый смысл |
|---|---|
| 250 | Команда принята |
| 550 | Mailbox unavailable — ящик не найден или недоступен |
| 551 | User not local |
| 553 | Mailbox name not allowed |
Пакет net/smtp в stdlib инкапсулирует отправку команд; низкоуровневый сокет — net.DialTimeout из статьи про TCP.
Код
Создайте smtp.go:
Код ITЗагрузка примера кода…
Разбор verifySMTP
| Шаг | Код | Зачем |
|---|---|---|
| Адрес | JoinHostPort(host, "25") | Корректная склейка host:port с учётом IPv6 |
| Таймаут connect | DialTimeout | Не зависнуть, если MX не отвечает |
| Дедлайн сессии | SetDeadline | Ограничить весь обмен, не только connect |
| Освобождение | defer conn.Close() | Закрыть сокет при любом выходе из функции |
| Приветствие | Hello("email-verifier.local") | Имя клиента для логов сервера |
| Ошибка RCPT | return false, parse..., nil | Ответ "ящик не найден" — не transport error, а бизнес-результат |
| Успех | return true, "...", nil | Сервер принял получателя |
Если на этапе 4 появляется connection timed out — проверьте, не блокирует ли провайдер исходящий порт 25. Флаг -mx (этап 5) работает без SMTP. Для экспериментов подойдёт VPS или корпоративная сеть с открытым 25.
Самопроверка
-
go build .— компиляция без ошибок. - Понимаете разницу между
err != nil(сеть, протокол) иexists == false(SMTP-код 550).
Этап 4 — оркестрация Verify
Цель — связать синтаксис, DNS и SMTP в один метод и красиво вывести результат.
Теория
Оркестрация — функция верхнего уровня задаёт порядок шагов и правила fallback:
- синтаксис неверен → дальше не идём;
- DNS не ответил → фиксируем
Error, SMTP не вызываем; - первый MX недоступен → пробуем следующий по приоритету;
- один MX ответил на RCPT → возвращаем результат и не опрашиваем остальных.
ParseDomain понадобится CLI-флагу -mx: пользователь может передать и google.com, и user@gmail.com — утилита извлечёт домен.
Код Verify
Замените метод Verify в verifier.go:
Код ITЗагрузка примера кода…
Код ParseDomain
Код ITЗагрузка примера кода…
Добавьте "net" в import verifier.go.
Вывод результата
В main.go (временно, до этапа 5):
Код ITЗагрузка примера кода…
Разбор цикла по MX
| Ситуация | Поведение |
|---|---|
verifySMTP вернул err != nil | Запоминаем в lastErr, continue — пробуем следующий MX |
verifySMTP вернул exists == false | Это ответ сервера, не сбой сети — сразу return result |
| Все MX недоступны | Exists остаётся nil, в Detail текст про недоступность |
switch в printResult | Три ветки для *bool — без dereference при nil |
Слово "вероятно" в выводе — намеренное: SMTP-проверка эвристическая, не доказательство.
Самопроверка
- Валидный email — DNS находит MX; SMTP отвечает или timeout (зависит от сети).
-
@example.comбез локальной части — отсекается regex на этапе 1. -
printResultне паникует приExists == nil.
Этап 5 — CLI с flag
Цель — принять аргументы из командной строки, добавить флаги и код выхода для скриптов.
Теория
CLI-утилита в Unix-смысле — программа с:
- positional arguments — то, что остаётся после флагов (
email-verifier user@gmail.com); - flags — опции (
-mx,-timeout 30s); - exit code — 0 при успехе, ненулевой при ошибке (важно для shell и CI).
Пакет flag из stdlib достаточен для одной команды — так же устроены многие примеры в Простых приложениях. Когда появятся подкоманды (tool check, tool mx) — cobra и viper.
flag.Duration понимает суффиксы Go — 30s, 1m30s, 500ms.
Код
Замените main.go финальной версией:
Код ITЗагрузка примера кода…
Сборка и запуск
go build -o email-verifier .
./email-verifier -mx google.com
./email-verifier user@gmail.com
echo $? # код выхода: 0 или 1
go build создаёт статически слинкованный бинарник — на машине без установленного Go его можно запускать напрямую (Первая программа).
Разбор CLI
| Элемент | Назначение |
|---|---|
flag.Parse() | Разобрать os.Args; после него — только positional args |
flag.Args() | Слайс аргументов без флагов |
fmt.Fprintf(os.Stderr, ...) | Сообщения об ошибках в stderr — stdout свободен для пайпов |
os.Exit(1) | Сигнал ошибки вызывающему скрипту |
Условие перед Exit | Невалидный формат, transport error или Exists == false — всё это "неуспех" |
Самопроверка
- Без аргументов — usage в stderr и exit 1.
-
-mx google.com— список MX, exit 0. -
-timeout 5s— более короткое ожидание SMTP. -
go build— один файлemail-verifier(или.exeна Windows).
Этап 6 — table-driven тесты
Цель — автоматически проверить чистые функции без сети.
Теория
Unit-тест в Go — файл *_test.go, функции TestXxx(t *testing.T), запуск go test. Table-driven стиль — таблица кейсов в слайсе структур и цикл с t.Run — идиома языка (Тестирование в Go).
Что тестируем:
- regex — детерминирован, не ходит в сеть;
ParseDomain— чистая функция строк → строка/error.
Что не тестируем в unit-тестах без mock:
lookupMX— реальный DNS;verifySMTP— живой MX на порту 25.
Для интеграционных тестов нужен стаб SMTP или build tag integration — за рамками базового практикума.
Код
Создайте verifier_test.go:
Код ITЗагрузка примера кода…
Запуск
go test -v .
go test -cover .
Разбор тестов
| Приём | Где |
|---|---|
package main | Тесты видят неэкспортируемые emailRegex, ParseDomain |
| Два слайса valid/invalid | Простая таблица без лишней структуры для regex |
tests := []struct{...} | Классический table-driven для ParseDomain |
(err != nil) != tt.wantErr | Проверка "ожидали ли ошибку" одним сравнением |
t.Errorf + %q | %q печатает строки в кавычках — видны пробелы |
Самопроверка
-
go test— все тесты зелёные. - В каталоге пять файлов —
main.go,verifier.go,dns.go,smtp.go,verifier_test.go. -
-coverпоказывает покрытие (SMTP-файлы будут с низким % — это ожидаемо).
Что дальше
| Направление | Материал |
|---|---|
| Подкоманды и YAML-конфиг | CLI на cobra и viper |
| HTTP API вместо CLI | Gin, Веб на stdlib |
| Параллельная проверка списка адресов | Горутины и каналы |
| Покрытие, mock SMTP | Тестирование |
| Парсинг HTML по URL | GoHTMLParser |
Идеи для самостоятельного расширения:
- читать адреса из файла (по одному на строку);
- флаг
-jsonдля машиночитаемого вывода; - worker pool из N горутин для пакетной проверки;
- логирование через
slogвместоfmt.Printf.
Частые ошибки
| Симптом | Вероятная причина | Решение |
|---|---|---|
connection timed out на SMTP | Порт 25 закрыт провайдером | -mx для DNS; другая сеть или VPS |
MX-записи не найдены | Опечатка в домене или домен без почты | Проверьте домен через nslookup -type=mx |
undefined: lookupMX | dns.go не в том пакете | Все файлы — package main, один каталог |
| Тесты не видят функции | Другой package в *_test.go | package main, не main_test |
flag provided but not defined | Аргументы до flag.Parse | Сначала объявить флаги, потом Parse() |
Паника при *r.Exists | Dereference при nil | switch с веткой r.Exists == nil |