Каждое отправляемое вами письмо проходит через тщательно организованную сеть серверов, а записи Mail Exchange (MX) являются указателями, направляющими это путешествие. Понимание того, как валидировать MX-записи, является фундаментальным навыком для любого разработчика, создающего системы проверки электронной почты, контактные формы или приложения, которые собирают адреса электронной почты. Это всестороннее руководство исследует валидацию MX-записей от базовых концепций до продвинутых стратегий реализации, предоставляя вам знания для создания надежной проверки электронной почты в ваших приложениях.
Понимание MX-записей
Записи Mail Exchange — это DNS-записи, которые определяют, какие почтовые серверы отвечают за прием электронной почты от имени домена. Когда вы отправляете письмо на адрес user@example.com, ваш почтовый сервер должен знать, куда его доставить. MX-записи предоставляют эту информацию, указывая на почтовые серверы домена.
Как работают MX-записи
Когда отправляется письмо, отправляющий почтовый сервер выполняет DNS-запрос для поиска MX-записей домена получателя. Этот запрос возвращает одно или несколько имен почтовых серверов вместе со значениями приоритета, которые указывают порядок предпочтения.
Типичный запрос MX-записей для gmail.com может вернуть:
gmail.com. MX 5 gmail-smtp-in.l.google.com. gmail.com. MX 10 alt1.gmail-smtp-in.l.google.com. gmail.com. MX 20 alt2.gmail-smtp-in.l.google.com. gmail.com. MX 30 alt3.gmail-smtp-in.l.google.com. gmail.com. MX 40 alt4.gmail-smtp-in.l.google.com.
Отправляющий сервер сначала пытается доставить письмо на сервер с наименьшим приоритетом (в данном случае приоритет 5). Если этот сервер недоступен, он пробует следующий уровень приоритета, и так далее. Такая избыточность обеспечивает доставку электронной почты даже когда отдельные серверы недоступны.
Компоненты MX-записи
Каждая MX-запись содержит две важные части информации:
Приоритет (Предпочтение)
Числовое значение, указывающее порядок, в котором следует пробовать почтовые серверы. Меньшие числа означают более высокий приоритет. Серверы с одинаковым приоритетом опрашиваются в случайном порядке, обеспечивая балансировку нагрузки.
Имя хоста почтового сервера
Полное доменное имя (FQDN) почтового сервера, который обрабатывает электронную почту для домена. Это имя хоста должно разрешаться в IP-адрес через A или AAAA запись.
Почему MX-записи важны для проверки электронной почты
Валидация MX-записей служит критической контрольной точкой в процессе проверки электронной почты:
Подтверждение существования домена
Если у домена нет MX-записей, он обычно не может получать электронную почту. Некоторые домены могут иметь резервную A-запись, но отсутствие MX-записей часто является сильным индикатором того, что домен не настроен для электронной почты.
Проверка инфраструктуры
Действительные MX-записи, которые разрешаются в работающие почтовые серверы, указывают на то, что домен имеет почтовую инфраструктуру. Это не гарантирует существование конкретного адреса, но подтверждает, что домен может получать электронную почту.
Обнаружение спама и мошенничества
Легитимные компании поддерживают правильные MX-записи. Подозрительные домены, используемые для спама или мошенничества, часто имеют неправильно настроенные или отсутствующие MX-записи.
Оптимизация производительности
Проверка MX-записей перед попыткой SMTP-верификации избегает потери времени на подключение к доменам, которые не могут получать электронную почту.
Реализация MX-запросов
Давайте рассмотрим, как реализовать валидацию MX-записей в различных программных средах.
Реализация в Node.js
Node.js предоставляет встроенное разрешение DNS через модуль dns:
const dns = require('dns').promises;
async function getMxRecords(domain) {
try {
const records = await dns.resolveMx(domain);
// Сортировка по приоритету (сначала самый низкий)
records.sort((a, b) => a.priority - b.priority);
return {
success: true,
domain,
records: records.map(r => ({
exchange: r.exchange,
priority: r.priority
}))
};
} catch (error) {
return {
success: false,
domain,
error: error.code,
message: getMxErrorMessage(error.code)
};
}
}
function getMxErrorMessage(code) {
const messages = {
'ENODATA': 'MX-записи для этого домена не найдены',
'ENOTFOUND': 'Домен не существует',
'ETIMEOUT': 'Тайм-аут DNS-запроса',
'ESERVFAIL': 'DNS-сервер не смог ответить'
};
return messages[code] || 'Неизвестная ошибка DNS';
}
// Использование
const result = await getMxRecords('gmail.com');
console.log(result);
Реализация в Python
Модуль dns.resolver Python из библиотеки dnspython предоставляет всесторонние возможности DNS-запросов:
import dns.resolver
import dns.exception
def get_mx_records(domain):
try:
answers = dns.resolver.resolve(domain, 'MX')
records = []
for rdata in answers:
records.append({
'exchange': str(rdata.exchange).rstrip('.'),
'priority': rdata.preference
})
# Сортировка по приоритету
records.sort(key=lambda x: x['priority'])
return {
'success': True,
'domain': domain,
'records': records
}
except dns.resolver.NXDOMAIN:
return {
'success': False,
'domain': domain,
'error': 'NXDOMAIN',
'message': 'Домен не существует'
}
except dns.resolver.NoAnswer:
return {
'success': False,
'domain': domain,
'error': 'NoAnswer',
'message': 'MX-записи для этого домена не найдены'
}
except dns.exception.Timeout:
return {
'success': False,
'domain': domain,
'error': 'Timeout',
'message': 'Тайм-аут DNS-запроса'
}
# Использование
result = get_mx_records('gmail.com')
print(result)
Реализация в Go
Пакет net в Go предоставляет простые функции DNS-запросов:
package main
import (
"fmt"
"net"
"sort"
)
type MxResult struct {
Success bool
Domain string
Records []MxRecord
Error string
}
type MxRecord struct {
Exchange string
Priority uint16
}
func getMxRecords(domain string) MxResult {
records, err := net.LookupMX(domain)
if err != nil {
return MxResult{
Success: false,
Domain: domain,
Error: err.Error(),
}
}
if len(records) == 0 {
return MxResult{
Success: false,
Domain: domain,
Error: "No MX records found",
}
}
// Сортировка по приоритету
sort.Slice(records, func(i, j int) bool {
return records[i].Pref < records[j].Pref
})
result := MxResult{
Success: true,
Domain: domain,
Records: make([]MxRecord, len(records)),
}
for i, r := range records {
result.Records[i] = MxRecord{
Exchange: r.Host,
Priority: r.Pref,
}
}
return result
}
func main() {
result := getMxRecords("gmail.com")
fmt.Printf("%+v\n", result)
}
Продвинутые техники MX-валидации
Базовые MX-запросы подтверждают существование записей, но всесторонняя валидация электронной почты требует более глубокого анализа.
Валидация подключения к почтовым серверам
MX-записи указывают на имена хостов, которые должны разрешаться в IP-адреса. Проверьте, что почтовые серверы действительно доступны:
const dns = require('dns').promises;
const net = require('net');
async function validateMxConnectivity(domain) {
// Получение MX-записей
const mxResult = await getMxRecords(domain);
if (!mxResult.success) {
return mxResult;
}
// Валидация каждого почтового сервера
const validatedRecords = [];
for (const record of mxResult.records) {
const validation = await validateMailServer(record.exchange);
validatedRecords.push({
...record,
...validation
});
}
return {
success: true,
domain,
records: validatedRecords,
hasReachableServer: validatedRecords.some(r => r.reachable)
};
}
async function validateMailServer(hostname) {
try {
// Разрешение имени хоста в IP
const addresses = await dns.resolve4(hostname);
if (addresses.length === 0) {
return { reachable: false, error: 'Нет A-записи' };
}
// Тест подключения к порту 25
const connected = await testConnection(addresses[0], 25);
return {
reachable: connected,
ip: addresses[0],
error: connected ? null : 'Соединение отклонено'
};
} catch (error) {
return {
reachable: false,
error: error.message
};
}
}
function testConnection(host, port, timeout = 5000) {
return new Promise((resolve) => {
const socket = new net.Socket();
socket.setTimeout(timeout);
socket.on('connect', () => {
socket.destroy();
resolve(true);
});
socket.on('timeout', () => {
socket.destroy();
resolve(false);
});
socket.on('error', () => {
resolve(false);
});
socket.connect(port, host);
});
}
Обработка резервной A-записи
Когда MX-записи отсутствуют, стандарты электронной почты (RFC 5321) указывают, что A-запись домена должна использоваться в качестве резервной. Реализуйте эту резервную логику в вашей валидации:
async function getMailServers(domain) {
// Сначала пробуем MX-записи
try {
const mxRecords = await dns.resolveMx(domain);
if (mxRecords.length > 0) {
return {
type: 'MX',
servers: mxRecords.sort((a, b) => a.priority - b.priority)
};
}
} catch (error) {
if (error.code !== 'ENODATA') {
throw error;
}
}
// Резервная A-запись
try {
const aRecords = await dns.resolve4(domain);
if (aRecords.length > 0) {
return {
type: 'A_FALLBACK',
servers: [{ exchange: domain, priority: 0 }],
warning: 'Используется резервная A-запись - MX-записи не найдены'
};
}
} catch (error) {
if (error.code !== 'ENODATA') {
throw error;
}
}
return {
type: 'NONE',
servers: [],
error: 'Почтовые серверы для домена не найдены'
};
}
Обнаружение нулевых MX-записей
RFC 7505 определяет "нулевые MX" записи, которые явно указывают, что домен не принимает электронную почту. Эти записи имеют одну MX-запись с приоритетом 0 и пустым именем хоста ("."):
function hasNullMx(mxRecords) {
if (mxRecords.length === 1) {
const record = mxRecords[0];
if (record.priority === 0 &&
(record.exchange === '.' || record.exchange === '')) {
return true;
}
}
return false;
}
async function validateDomainMx(domain) {
const mxResult = await getMxRecords(domain);
if (!mxResult.success) {
return mxResult;
}
if (hasNullMx(mxResult.records)) {
return {
success: false,
domain,
error: 'NULL_MX',
message: 'Домен явно не принимает электронную почту'
};
}
return mxResult;
}
Кэширование MX-запросов
DNS-запросы добавляют задержку к каждой проверке. Реализуйте кэширование для улучшения производительности:
class MxCache {
constructor(ttlMs = 3600000) { // TTL по умолчанию 1 час
this.cache = new Map();
this.ttl = ttlMs;
}
get(domain) {
const entry = this.cache.get(domain.toLowerCase());
if (!entry) return null;
if (Date.now() > entry.expiry) {
this.cache.delete(domain.toLowerCase());
return null;
}
return entry.data;
}
set(domain, data) {
this.cache.set(domain.toLowerCase(), {
data,
expiry: Date.now() + this.ttl
});
}
// Учитываем значения DNS TTL, когда доступны
setWithTtl(domain, data, ttlSeconds) {
const ttlMs = Math.min(ttlSeconds * 1000, this.ttl);
this.cache.set(domain.toLowerCase(), {
data,
expiry: Date.now() + ttlMs
});
}
}
const mxCache = new MxCache();
async function getMxRecordsCached(domain) {
const cached = mxCache.get(domain);
if (cached) {
return { ...cached, fromCache: true };
}
const result = await getMxRecords(domain);
if (result.success) {
mxCache.set(domain, result);
}
return { ...result, fromCache: false };
}
Распространенные паттерны MX-записей
Понимание распространенных конфигураций MX помогает вам интерпретировать результаты валидации и выявлять потенциальные проблемы.
Основные почтовые провайдеры
Распознавание MX-паттернов основных провайдеров может помочь идентифицировать бесплатные адреса электронной почты:
const knownProviders = {
'google': [
'gmail-smtp-in.l.google.com',
'googlemail-smtp-in.l.google.com',
'aspmx.l.google.com'
],
'microsoft': [
'outlook-com.olc.protection.outlook.com',
'mail.protection.outlook.com'
],
'yahoo': [
'mta5.am0.yahoodns.net',
'mta6.am0.yahoodns.net',
'mta7.am0.yahoodns.net'
],
'protonmail': [
'mail.protonmail.ch',
'mailsec.protonmail.ch'
]
};
function identifyEmailProvider(mxRecords) {
const exchanges = mxRecords.map(r => r.exchange.toLowerCase());
for (const [provider, patterns] of Object.entries(knownProviders)) {
for (const pattern of patterns) {
if (exchanges.some(ex => ex.includes(pattern.toLowerCase()))) {
return provider;
}
}
}
return 'unknown';
}
Обнаружение Google Workspace
Домены Google Workspace (ранее G Suite) используют почтовые серверы Google, но не являются бесплатными учетными записями:
function isGoogleWorkspace(domain, mxRecords) {
const isGoogleMx = mxRecords.some(r =>
r.exchange.toLowerCase().includes('google') ||
r.exchange.toLowerCase().includes('googlemail')
);
// Проверка, что домен не является известным потребительским доменом Google
const googleConsumerDomains = ['gmail.com', 'googlemail.com'];
const isConsumerDomain = googleConsumerDomains.includes(domain.toLowerCase());
return isGoogleMx && !isConsumerDomain;
}
Обнаружение самостоятельно размещенной почты
Домены, которые размещают собственную электронную почту, часто имеют MX-записи, указывающие на поддомены:
function isSelfHosted(domain, mxRecords) {
const domainParts = domain.toLowerCase().split('.');
const baseDomain = domainParts.slice(-2).join('.');
return mxRecords.some(r => {
const exchange = r.exchange.toLowerCase();
return exchange.includes(baseDomain) &&
!isKnownProvider(exchange);
});
}
function isKnownProvider(exchange) {
const providers = ['google', 'microsoft', 'yahoo', 'outlook', 'protonmail'];
return providers.some(p => exchange.includes(p));
}
MX-валидация в пайплайнах проверки электронной почты
MX-валидация является одним из шагов в комплексном процессе проверки электронной почты. Понимание ее роли помогает создавать эффективные пайплайны верификации.
Порядок верификации
MX-валидация обычно происходит на ранней стадии пайплайна верификации:
async function verifyEmail(email) {
// 1. Валидация синтаксиса (самая быстрая, без сети)
const syntaxResult = validateEmailSyntax(email);
if (!syntaxResult.valid) {
return { valid: false, reason: 'invalid_syntax', details: syntaxResult };
}
const domain = email.split('@')[1];
// 2. Валидация MX-записей (быстрый DNS-запрос)
const mxResult = await validateMxRecords(domain);
if (!mxResult.valid) {
return { valid: false, reason: 'no_mx_records', details: mxResult };
}
// 3. Дополнительные проверки (одноразовые, ролевые и т.д.)
const domainCheck = await checkDomainReputation(domain);
if (domainCheck.isDisposable) {
return { valid: true, risky: true, reason: 'disposable_domain' };
}
// 4. SMTP-верификация (самая медленная, самая тщательная)
const smtpResult = await verifySmtp(email, mxResult.records);
return {
valid: smtpResult.exists,
deliverable: smtpResult.deliverable,
mxRecords: mxResult.records,
provider: mxResult.provider
};
}
Параллельные MX-запросы
При проверке нескольких писем, распараллеливайте MX-запросы для разных доменов:
async function verifyEmailsBatch(emails) {
// Группировка писем по доменам
const emailsByDomain = {};
for (const email of emails) {
const domain = email.split('@')[1];
if (!emailsByDomain[domain]) {
emailsByDomain[domain] = [];
}
emailsByDomain[domain].push(email);
}
// Запрос MX-записей для всех доменов параллельно
const domains = Object.keys(emailsByDomain);
const mxResults = await Promise.all(
domains.map(domain => getMxRecordsCached(domain))
);
// Сопоставление результатов с доменами
const mxByDomain = {};
domains.forEach((domain, index) => {
mxByDomain[domain] = mxResults[index];
});
// Обработка писем с MX-данными
const results = [];
for (const email of emails) {
const domain = email.split('@')[1];
const mx = mxByDomain[domain];
if (!mx.success) {
results.push({ email, valid: false, reason: 'invalid_domain' });
continue;
}
// Продолжение с SMTP-верификацией, используя кэшированные MX-данные
const smtpResult = await verifySmtp(email, mx.records);
results.push({ email, ...smtpResult });
}
return results;
}
Обработка ошибок и граничных случаев
Надежная MX-валидация корректно обрабатывает различные условия ошибок.
Обработка тайм-аутов DNS
Сетевые проблемы могут привести к зависанию DNS-запросов. Реализуйте обработку тайм-аутов:
async function getMxRecordsWithTimeout(domain, timeoutMs = 10000) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('DNS_TIMEOUT')), timeoutMs);
});
try {
const result = await Promise.race([
getMxRecords(domain),
timeoutPromise
]);
return result;
} catch (error) {
if (error.message === 'DNS_TIMEOUT') {
return {
success: false,
domain,
error: 'TIMEOUT',
message: 'Тайм-аут DNS-запроса',
retryable: true
};
}
throw error;
}
}
Обработка недействительных доменов
Обрабатывайте синтаксически недействительные домены до попытки DNS-запросов:
function isValidDomain(domain) {
if (!domain || typeof domain !== 'string') {
return false;
}
// Проверка длины
if (domain.length > 253) {
return false;
}
// Проверка на допустимые символы и структуру
const domainRegex = /^(?!-)[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
if (!domainRegex.test(domain)) {
return false;
}
// Проверка длины каждого лейбла
const labels = domain.split('.');
for (const label of labels) {
if (label.length > 63) {
return false;
}
}
return true;
}
async function validateDomainMxSafe(domain) {
if (!isValidDomain(domain)) {
return {
success: false,
domain,
error: 'INVALID_DOMAIN',
message: 'Формат домена недействителен'
};
}
return await getMxRecordsWithTimeout(domain);
}
Обработка временных сбоев DNS
Сбои DNS могут быть временными. Реализуйте логику повторных попыток с экспоненциальной задержкой:
async function getMxRecordsWithRetry(domain, maxRetries = 3) {
const delays = [1000, 2000, 4000];
for (let attempt = 0; attempt < maxRetries; attempt++) {
const result = await getMxRecordsWithTimeout(domain);
// Не повторять для окончательных сбоев
if (result.success ||
result.error === 'NXDOMAIN' ||
result.error === 'ENOTFOUND') {
return result;
}
// Повторить для временных сбоев
if (result.retryable && attempt < maxRetries - 1) {
await sleep(delays[attempt]);
continue;
}
return result;
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Соображения безопасности
MX-валидация вводит соображения безопасности, которые разработчики должны учитывать.
Предотвращение подмены DNS
Стандартные DNS-запросы не зашифрованы и уязвимы для подмены. Рассмотрите использование DNS через HTTPS (DoH) для критичных приложений:
const https = require('https');
async function getMxRecordsDoH(domain) {
const url = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=MX`;
return new Promise((resolve, reject) => {
https.get(url, {
headers: { 'Accept': 'application/dns-json' }
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const response = JSON.parse(data);
if (response.Status !== 0) {
resolve({
success: false,
domain,
error: 'DNS_ERROR',
status: response.Status
});
return;
}
const records = (response.Answer || [])
.filter(a => a.type === 15)
.map(a => {
const [priority, exchange] = a.data.split(' ');
return {
priority: parseInt(priority),
exchange: exchange.replace(/\.$/, '')
};
})
.sort((a, b) => a.priority - b.priority);
resolve({
success: records.length > 0,
domain,
records
});
} catch (error) {
reject(error);
}
});
}).on('error', reject);
});
}
Ограничение скорости DNS-запросов
Предотвращайте злоупотребления, ограничивая скорость DNS-запросов:
class DnsRateLimiter {
constructor(maxQueriesPerSecond = 100) {
this.tokens = maxQueriesPerSecond;
this.maxTokens = maxQueriesPerSecond;
this.lastRefill = Date.now();
}
async acquire() {
this.refillTokens();
if (this.tokens > 0) {
this.tokens--;
return true;
}
// Ожидание доступности токена
await sleep(1000 / this.maxTokens);
return this.acquire();
}
refillTokens() {
const now = Date.now();
const elapsed = now - this.lastRefill;
const tokensToAdd = (elapsed / 1000) * this.maxTokens;
this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
const dnsLimiter = new DnsRateLimiter(50);
async function getMxRecordsRateLimited(domain) {
await dnsLimiter.acquire();
return getMxRecords(domain);
}
Использование BillionVerify для MX-валидации
Хотя самостоятельная реализация MX-валидации имеет образовательную ценность, профессиональные сервисы проверки электронной почты, такие как BillionVerify, обрабатывают MX-валидацию как часть комплексной проверки электронной почты.
Преимущества использования API проверки электронной почты
Комплексные проверки
API проверки электронной почты BillionVerify сочетает MX-валидацию с проверкой синтаксиса, SMTP-верификацией, обнаружением одноразовых адресов и многое другое в одном вызове API. Это исключает необходимость поддержки нескольких систем валидации.
Оптимизированная инфраструктура
Профессиональные сервисы поддерживают глобально распределенные DNS-резолверы, обрабатывают кэширование в масштабе и оптимизируют производительность для миллионов проверок.
Непрерывные обновления
Конфигурации почтовых серверов постоянно меняются. Сервисы проверки электронной почты непрерывно обновляют свои базы данных известных провайдеров, одноразовых доменов и паттернов почтовых серверов.
Пример интеграции API
async function verifyEmailWithBillionVerify(email) {
const response = await fetch('https://api.billionverify.com/v1/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.BILLIONVERIFY_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email })
});
const result = await response.json();
// MX-информация включена в ответ
console.log('MX действительна:', result.mx_found);
console.log('Домен действителен:', result.domain_valid);
console.log('Доставляема:', result.is_deliverable);
return result;
}
Заключение
Валидация MX-записей является фундаментальным компонентом проверки электронной почты, который подтверждает, что домен может получать электронную почту, прежде чем пытаться выполнить более ресурсоемкие проверки. Реализуя правильную MX-валидацию, вы можете быстро отфильтровать недействительные домены, оптимизировать производительность верификации и создавать более надежные приложения для обработки электронной почты.
Ключевые выводы для валидации MX-записей:
- Всегда проверяйте MX-записи перед попыткой SMTP-верификации, чтобы сэкономить время и ресурсы
- Обрабатывайте резервную A-запись согласно стандартам RFC для доменов без MX-записей
- Реализуйте кэширование для уменьшения накладных расходов DNS-запросов при повторных валидациях
- Распознавайте распространенные паттерны для идентификации почтовых провайдеров и потенциальных рисков
- Обрабатывайте ошибки корректно с тайм-аутами, повторными попытками и правильными сообщениями об ошибках
Независимо от того, создаете ли вы пользовательскую систему проверки электронной почты или интегрируетесь с сервисом вроде BillionVerify, понимание MX-записей помогает вам создавать лучшую обработку электронной почты в ваших приложениях. Начните реализацию MX-валидации сегодня и сделайте первый шаг к комплексной проверке электронной почты.