您發送的每一封郵件都會經過一個精心編排的伺服器網路,而郵件交換(Mail Exchange,MX)記錄就是引導這一旅程的路標。對於任何構建郵箱驗證系統、聯絡表單或收集郵箱地址的應用程式的開發者來說,了解如何驗證 MX 記錄是一項基本技能。本綜合指南探討了從基本概念到進階實作策略的 MX 記錄驗證,為您提供在應用程式中構建強大郵箱驗證功能的知識。
理解 MX 記錄
郵件交換記錄是 DNS 記錄,用於指定哪些郵件伺服器負責代表網域接收郵件。當您向 user@example.com 發送郵件時,您的郵件伺服器需要知道將郵件發送到哪裡。MX 記錄通過指向網域的郵件伺服器來提供此資訊。
MX 記錄的工作原理
發送郵件時,發送郵件伺服器執行 DNS 查詢以查找收件人網域的 MX 記錄。此查詢返回一個或多個郵件伺服器主機名稱以及表示優先順序的優先順序值。
對 gmail.com 的典型 MX 記錄查詢可能返回:
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 記錄包含兩個基本資訊:
優先順序(Preference)
表示應嘗試郵件伺服器順序的數值。數字越小表示優先順序越高。具有相同優先順序的伺服器按隨機順序嘗試,提供負載平衡。
郵件伺服器主機名稱
處理該網域郵件的郵件伺服器的完全限定網域名稱(FQDN)。此主機名稱必須透過 A 或 AAAA 記錄解析為 IP 位址。
為什麼 MX 記錄對郵箱驗證很重要
MX 記錄驗證是郵箱驗證過程中的關鍵檢查點:
網域存在確認
如果網域沒有 MX 記錄,通常無法接收郵件。一些網域可能有 A 記錄回退,但缺少 MX 記錄通常是該網域未設定郵件功能的強烈訊號。
基礎設施驗證
解析為工作郵件伺服器的有效 MX 記錄表明該網域已部署郵件基礎設施。這並不保證特定地址存在,但確認該網域可以接收郵件。
垃圾郵件和詐欺檢測
合法企業維護適當的 MX 記錄。用於垃圾郵件或詐欺的可疑網域通常設定錯誤或缺少 MX 記錄。
效能最佳化
在嘗試 SMTP 驗證之前檢查 MX 記錄可避免浪費時間連接到無法接收郵件的網域。
實作 MX 記錄查詢
讓我們探討如何在不同的程式設計環境中實作 MX 記錄驗證。
Node.js 實作
Node.js 透過 dns 模組提供內建的 DNS 解析:
const dns = require('dns').promises;
async function getMxRecords(domain) {
try {
const records = await dns.resolveMx(domain);
// Sort by priority (lowest first)
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': 'No MX records found for this domain',
'ENOTFOUND': 'Domain does not exist',
'ETIMEOUT': 'DNS lookup timed out',
'ESERVFAIL': 'DNS server failed to respond'
};
return messages[code] || 'Unknown DNS error';
}
// Usage
const result = await getMxRecords('gmail.com');
console.log(result);
Python 實作
Python 的 dnspython 函式庫中的 dns.resolver 模組提供全面的 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
})
# Sort by priority
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': 'Domain does not exist'
}
except dns.resolver.NoAnswer:
return {
'success': False,
'domain': domain,
'error': 'NoAnswer',
'message': 'No MX records found for this domain'
}
except dns.exception.Timeout:
return {
'success': False,
'domain': domain,
'error': 'Timeout',
'message': 'DNS lookup timed out'
}
# Usage
result = get_mx_records('gmail.com')
print(result)
Go 實作
Go 的 net 套件提供直接的 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 by priority
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) {
// Get MX records
const mxResult = await getMxRecords(domain);
if (!mxResult.success) {
return mxResult;
}
// Validate each mail server
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 {
// Resolve hostname to IP
const addresses = await dns.resolve4(hostname);
if (addresses.length === 0) {
return { reachable: false, error: 'No A record' };
}
// Test connection to port 25
const connected = await testConnection(addresses[0], 25);
return {
reachable: connected,
ip: addresses[0],
error: connected ? null : 'Connection refused'
};
} 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) {
// Try MX records first
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;
}
}
// Fallback to A record
try {
const aRecords = await dns.resolve4(domain);
if (aRecords.length > 0) {
return {
type: 'A_FALLBACK',
servers: [{ exchange: domain, priority: 0 }],
warning: 'Using A record fallback - no MX records found'
};
}
} catch (error) {
if (error.code !== 'ENODATA') {
throw error;
}
}
return {
type: 'NONE',
servers: [],
error: 'No mail servers found for domain'
};
}
檢測空 MX 記錄
RFC 7505 定義了「空 MX」記錄,明確表示網域不接受郵件。這些記錄具有優先順序為 0 且主機名稱為空(".")的單個 MX 條目:
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: 'Domain explicitly does not accept email'
};
}
return mxResult;
}
快取 MX 查詢
DNS 查詢會給每次驗證增加延遲。實作快取以提高效能:
class MxCache {
constructor(ttlMs = 3600000) { // 1 hour default TTL
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
});
}
// Respect DNS TTL values when available
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')
);
// Check if domain is not a known Google consumer domain
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. Syntax validation (fastest, no network)
const syntaxResult = validateEmailSyntax(email);
if (!syntaxResult.valid) {
return { valid: false, reason: 'invalid_syntax', details: syntaxResult };
}
const domain = email.split('@')[1];
// 2. MX record validation (fast DNS lookup)
const mxResult = await validateMxRecords(domain);
if (!mxResult.valid) {
return { valid: false, reason: 'no_mx_records', details: mxResult };
}
// 3. Additional checks (disposable, role-based, etc.)
const domainCheck = await checkDomainReputation(domain);
if (domainCheck.isDisposable) {
return { valid: true, risky: true, reason: 'disposable_domain' };
}
// 4. SMTP verification (slowest, most thorough)
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) {
// Group emails by domain
const emailsByDomain = {};
for (const email of emails) {
const domain = email.split('@')[1];
if (!emailsByDomain[domain]) {
emailsByDomain[domain] = [];
}
emailsByDomain[domain].push(email);
}
// Lookup MX records for all domains in parallel
const domains = Object.keys(emailsByDomain);
const mxResults = await Promise.all(
domains.map(domain => getMxRecordsCached(domain))
);
// Map results back to domains
const mxByDomain = {};
domains.forEach((domain, index) => {
mxByDomain[domain] = mxResults[index];
});
// Process emails with MX data
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;
}
// Continue with SMTP verification using cached MX data
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 lookup timed out',
retryable: true
};
}
throw error;
}
}
無效網域處理
在嘗試 DNS 查詢之前處理語法無效的網域:
function isValidDomain(domain) {
if (!domain || typeof domain !== 'string') {
return false;
}
// Check length
if (domain.length > 253) {
return false;
}
// Check for valid characters and structure
const domainRegex = /^(?!-)[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
if (!domainRegex.test(domain)) {
return false;
}
// Check each label length
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: 'Domain format is invalid'
};
}
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);
// Don't retry for definitive failures
if (result.success ||
result.error === 'NXDOMAIN' ||
result.error === 'ENOTFOUND') {
return result;
}
// Retry for temporary failures
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 over 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;
}
// Wait for token availability
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 的優勢
全面檢查
BillionVerify 的郵箱驗證 API 在單個 API 呼叫中將 MX 驗證與語法檢查、SMTP 驗證、拋棄式郵箱檢測等相結合。這消除了維護多個驗證系統的需要。
最佳化的基礎設施
專業服務維護全球分散式的 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 information is included in the response
console.log('MX Valid:', result.mx_found);
console.log('Domain Valid:', result.domain_valid);
console.log('Is Deliverable:', result.is_deliverable);
return result;
}
結論
MX 記錄驗證是郵箱驗證的基本組成部分,在嘗試更耗費資源的檢查之前確認網域可以接收郵件。透過實作適當的 MX 驗證,您可以快速過濾無效網域,最佳化驗證效能,並構建更可靠的郵件處理應用程式。
MX 記錄驗證的關鍵要點:
- 始終檢查 MX 記錄,在嘗試 SMTP 驗證之前節省時間和資源
- 處理 A 記錄回退,根據 RFC 標準處理沒有 MX 記錄的網域
- 實作快取,減少重複驗證的 DNS 查詢開銷
- 識別常見模式,識別郵件提供商和潛在風險
- 優雅地處理錯誤,包括逾時、重試和適當的錯誤訊息
無論您是構建自訂郵箱驗證系統還是整合 BillionVerify 等服務,了解 MX 記錄都有助於您在應用程式中構建更好的郵件處理功能。立即開始實作 MX 驗證,邁出全面郵箱驗證的第一步。