首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 實現 SMTP 級郵箱真實性驗證:深度解析 MX 記錄查詢與郵件交互模擬

     編輯:PHP 相關知識

常規正則驗證只能檢查郵箱格式,無法確認郵箱是否真實存在。本文介紹基於 SMTP 協議的「真實發送模擬」驗證技術:查詢域名 MX 記錄、連接郵件服務器、模擬 SMTP 握手與 RCPT TO 命令,判斷郵箱是否可投遞。提供完整類實現、異常處理、性能優化與法律合規提示。

核心原理

  1. MX 記錄查詢: `dns_get_record('example.com', DNS_MX)` 獲取郵件交換器列表,按優先級排序
  2. SMTP 連接: `fsockopen()` 連接 MX 主機 25 端口(或 587/465)
  3. 握手流程: `HELO` → `MAIL FROM:<>` → `RCPT TO:` → `QUIT`
  4. 判斷邏輯: 服務器返回 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 黑名單
  • 最佳實踐: 僅在用戶註冊、表單提交等主動場景驗證;限制頻率;記錄審計日誌;提供退出機制

性能優化方案

現代替代方案:郵箱驗證 API 對比

方案原理適用場景
DNS 緩存MX 記錄 TTL 通常較長(小時級),本地緩存減少 DNS 查詢高頻驗證同域名
連接池復用對同一 MX 主機保持長連接,批量驗證複用批量導入、清洗數據
異步並發Swoole/Workerman 協程並發連接多 MX 主機百萬級清洗任務
第三方 API轉嫁基礎設施成本,享受專業灰名單處理、Catch-all 檢測無自建條件、合規優先

總結: SMTP 級驗證能顯著提升郵箱真實性判斷準確度,但面臨「25 端口封禁」「灰名單誤判」「Catch-all 無法區分」「法律合規」等現實挑戰。工程選型建議:核心業務(註冊/登錄)接入專業 API 服務,享受合規、高可用、專業反灰名單能力;內部數據清洗、低頻場景可自建驗證服務,部署於支持 25 端口的服務器,實現異步批量處理。切勿將自建驗證用於大規模外部掃描,以免 IP 信譽受損、觸犯法規。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
服務商免費額度價格(萬次)特色
AbstractAPI100/月~$50API 簡單、支持批量、Catch-all 檢測
ZeroBounce100/月~$39準確率高、GDPR 合規、實時驗證
Hunter.io25/月~$49結合郵箱搜索、驗證
Kickbox100/月~$40實時/批量、可嵌入表單