常規正則驗證只能檢查郵箱格式,無法確認郵箱是否真實存在。本文介紹基於 SMTP 協議的「真實發送模擬」驗證技術:查詢域名 MX 記錄、連接郵件服務器、模擬 SMTP 握手與 RCPT TO 命令,判斷郵箱是否可投遞。提供完整類實現、異常處理、性能優化與法律合規提示。
核心原理
- MX 記錄查詢: `dns_get_record('example.com', DNS_MX)` 獲取郵件交換器列表,按優先級排序
- SMTP 連接: `fsockopen()` 連接 MX 主機 25 端口(或 587/465)
- 握手流程: `HELO` → `MAIL FROM:<>` → `RCPT TO:` → `QUIT`
- 判斷邏輯: 服務器返回 250 碼表示郵箱存在/可接收;550 等錯誤碼表示不存在;其他碼視為不確定
完整驗證類實現
<?php
class SmtpEmailValidator {
private $timeout = 10; // 連接超時(秒)
private $heloHost = 'localhost'; // HELO 主機名
private $fromEmail = 'validator@example.com'; // MAIL FROM 地址
private $debug = false;
public function __construct(array $options = []) {
foreach ($options as $k => $v) if (property_exists($this, $k)) $this->$k = $v;
}
/**
* 驗證單個郵箱
* @return array ['valid'=>bool, 'exists'=>bool|null, 'code'=>int, 'message'=>string, 'mx'=>string]
*/
public function verify($email) {
// 1. 基礎格式檢查
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return ['valid' => false, 'exists' => false, 'code' => 0, 'message' => 'Invalid email format'];
}
// 2. 解析域名
$domain = substr(strrchr($email, '@'), 1);
// 3. 獲取 MX 記錄
$mxHost = $this->getMxHost($domain);
if (!$mxHost) {
return ['valid' => true, 'exists' => null, 'code' => 0, 'message' => 'No MX record found', 'mx' => ''];
}
// 4. SMTP 會話驗證
return $this->smtpCheck($mxHost, $email);
}
/**
* 批量驗證(生成器,節省內存)
*/
public function verifyBatch(array $emails) {
foreach ($emails as $email) {
yield $email => $this->verify($email);
}
}
// 獲取優先級最高的 MX 主機
private function getMxHost($domain) {
$records = @dns_get_record($domain, DNS_MX);
if (empty($records)) return false;
usort($records, fn($a, $b) => $a['pri'] - $b['pri']);
return $records[0]['target'];
}
// 核心 SMTP 交互
private function smtpCheck($mxHost, $email) {
$errno = $errstr = 0;
$socket = @fsockopen($mxHost, 25, $errno, $errstr, $this->timeout);
if (!$socket) {
return ['valid' => true, 'exists' => null, 'code' => $errno, 'message' => "Connection failed: $errstr", 'mx' => $mxHost];
}
stream_set_timeout($socket, $this->timeout);
try {
// 讀取歡迎消息 (220)
$reply = $this->readReply($socket);
if (!$this->checkCode($reply, 220)) throw new \Exception("No 220 greeting: $reply");
// HELO
$this->sendCommand($socket, "HELO {$this->heloHost}");
$reply = $this->readReply($socket);
if (!$this->checkCode($reply, 250)) throw new \Exception("HELO failed: $reply");
// MAIL FROM (空發件人,僅驗證)
$this->sendCommand($socket, "MAIL FROM:<{$this->fromEmail}>");
$reply = $this->readReply($socket);
if (!$this->checkCode($reply, 250)) throw new \Exception("MAIL FROM failed: $reply");
// RCPT TO (關鍵步驟)
$this->sendCommand($socket, "RCPT TO:<$email>");
$reply = $this->readReply($socket);
$code = (int)substr($reply, 0, 3);
// QUIT
$this->sendCommand($socket, "QUIT");
$this->readReply($socket); // 忽略響應
$exists = ($code === 250 || $code === 251); // 250 OK, 251 User not local but will forward
return [
'valid' => true,
'exists' => $exists,
'code' => $code,
'message' => $reply,
'mx' => $mxHost
];
} catch (\Exception $e) {
return ['valid' => true, 'exists' => null, 'code' => 0, 'message' => $e->getMessage(), 'mx' => $mxHost];
} finally {
@fclose($socket);
}
}
private function sendCommand($socket, $cmd) {
if ($this->debug) error_log("SMTP > $cmd");
fwrite($socket, $cmd . "\r\n");
}
private function readReply($socket) {
$reply = '';
while ($line = fgets($socket, 512)) {
$reply .= $line;
if ($this->debug) error_log("SMTP < $line");
// 多行響應以 "250 " 開頭,最後一行以 "250 " 開頭且第 4 字符為空格
if (preg_match('/^\d{3} /', $line)) break;
}
return trim($reply);
}
private function checkCode($reply, $expected) {
return (int)substr($reply, 0, 3) === $expected;
}
}
// 使用示例
$validator = new SmtpEmailValidator(['debug' => true, 'timeout' => 15]);
$result = $validator->verify('test@gmail.com');
print_r($result);
/*
輸出示例:
Array
(
[valid] => 1
[exists] => 1
[code] => 250
[message] => 250 2.1.5 OK
[mx] => gmail-smtp-in.l.google.com
)
*/
關鍵技術點與坑點解析
1. 端口選擇與加密
- 25 端口: 標準 SMTP,但許多雲服務器(AWS、阿里雲、騰訊雲)默認封禁出站 25 端口防垃圾郵件
- 587 端口 (Submission): 需 STARTTLS 加密,驗證流程複雜(需 TLS 握手後再發 SMTP 命令)
- 465 端口 (SMTPS): 隱式 SSL,同樣需 TLS 層
- 解決方案: 使用支持 25 端口的服務器(自建機房、部分 VPS),或接入第三方驗證 API(如 ZeroBounce、Hunter、AbstractAPI)
2. 灰名單與反垃圾策略導致的假陰性
- 許多郵件服務器(如 Outlook、企業郵箱)啟用 灰名單:首次 RCPT TO 返回 450/451 暫時性錯誤,要求稍後重試
- 真實郵件發送會重試,但驗證腳本通常不重試 → 誤判為不存在
- 緩解: 對 4xx 碼視為「不確定」而非「不存在」;實現指數退避重試(增加耗時)
3. Catch-all 域名(全收域名)
- 部分域名配置 `* @domain` 所有郵箱均返回 250 OK
- 無法區分真實存在與 Catch-all
- 檢測: 隨機生成極不可能存在的郵箱(如 `xk7q9z2m@domain`)驗證,若也返回 250 則為 Catch-all
4. 法律合規與道德邊界
- GDPR/個信法: 郵箱屬個人信息,驗證行為需用戶同意或合法依據
- 反垃圾郵件法: 大規模驗證可能被視為掃描/騷擾,IP 易被列入 RBL 黑名單
- 最佳實踐: 僅在用戶註冊、表單提交等主動場景驗證;限制頻率;記錄審計日誌;提供退出機制
性能優化方案
| 方案 | 原理 | 適用場景 |
| DNS 緩存 | MX 記錄 TTL 通常較長(小時級),本地緩存減少 DNS 查詢 | 高頻驗證同域名 |
| 連接池復用 | 對同一 MX 主機保持長連接,批量驗證複用 | 批量導入、清洗數據 |
| 異步並發 | Swoole/Workerman 協程並發連接多 MX 主機 | 百萬級清洗任務 |
| 第三方 API | 轉嫁基礎設施成本,享受專業灰名單處理、Catch-all 檢測 | 無自建條件、合規優先 |
現代替代方案:郵箱驗證 API 對比
| 服務商 | 免費額度 | 價格(萬次) | 特色 |
| AbstractAPI | 100/月 | ~$50 | API 簡單、支持批量、Catch-all 檢測 |
| ZeroBounce | 100/月 | ~$39 | 準確率高、GDPR 合規、實時驗證 |
| Hunter.io | 25/月 | ~$49 | 結合郵箱搜索、驗證 |
| Kickbox | 100/月 | ~$40 | 實時/批量、可嵌入表單 |
總結: SMTP 級驗證能顯著提升郵箱真實性判斷準確度,但面臨「25 端口封禁」「灰名單誤判」「Catch-all 無法區分」「法律合規」等現實挑戰。工程選型建議:核心業務(註冊/登錄)接入專業 API 服務,享受合規、高可用、專業反灰名單能力;內部數據清洗、低頻場景可自建驗證服務,部署於支持 25 端口的服務器,實現異步批量處理。切勿將自建驗證用於大規模外部掃描,以免 IP 信譽受損、觸犯法規。
- 上一篇文章: PHP 取得字串首字母(支援中文拼音首字母)實作
- 下一篇文章: macOS 安裝 PHP 圖片智能裁剪擴展 Tclip:OpenCV 編譯避坑全記錄
Copyright ©
程式碼之家 All Rights Reserved