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

PHP Socket 郵件發送類別:SMTP 協定底層實作與 163 郵箱驗證應用

     編輯:PHP 相關知識

PHP Socket 底層實作 SMTP 郵件發送:完整協定流程與類別封裝

為什麼用 Socket 而非 mail() 或 PHPMailer?

  • 完全控制:理解 SMTP 交互細節、自訂指令流程
  • 無依賴:不需安裝擴充、Composer 套件
  • 進階應用:RCPT 指令驗證郵箱存在性、自定義 HELO/EHLO、多郵件複用連線
  • 學習價值:網路協定教學、除錯郵件投遞問題

SMTP 協定核心流程

連線 → 220 歡迎
HELO/EHLO → 250 OK
AUTH LOGIN → 334 用戶名 → 334 密碼 → 235 認證成功
MAIL FROM: → 250 OK
RCPT TO: → 250 OK (此處可驗證郵箱是否存在)
DATA → 354 開始輸入
郵件內容(Header + Body)
. → 250 郵件已排隊
QUIT → 221 關閉

完整 Mail 類別實作

<?php
class SmtpMailer {
    // 連線設定
    public $host, $port = 25, $user, $password;
    // 郵件內容
    public $from, $fromName, $to, $toName, $subject, $body, $isHtml = true;
    // 內部狀態
    private $socket;
    private $response = '';
    private $log = [];

    public function __construct(array $config = []) {
        foreach ($config as $k => $v) if (property_exists($this, $k)) $this->$k = $v;
    }

    /** 發送郵件主流程 */
    public function send(): bool {
        try {
            $this->connect();
            $this->helo();
            $this->auth();
            $this->mailFrom();
            
            $recipients = is_array($this->to) ? $this->to : [$this->to];
            foreach ($recipients as $rcpt) {
                $this->rcptTo($rcpt);
            }
            
            $this->data();
            $this->sendContent();
            $this->quit();
            return true;
        } catch (Exception $e) {
            $this->log[] = 'ERROR: ' . $e->getMessage();
            return false;
        }
    }

    /** 建立 Socket 連線 */
    private function connect(): void {
        $this->socket = @fsockopen($this->host, $this->port, $errno, $errstr, 15);
        if (!$this->socket) throw new Exception("連線失敗: $errstr ($errno)");
        $this->readResponse(220);
    }

    /** 發送指令並驗證回應碼 */
    private function command(string $cmd, int $expectCode): void {
        fwrite($this->socket, $cmd . "\r\n");
        $this->readResponse($expectCode);
    }

    /** 讀取伺服器回應 */
    private function readResponse(int $expectCode = null): string {
        $line = fgets($this->socket, 512);
        $this->log[] = 'S: ' . trim($line);
        $code = (int)substr($line, 0, 3);
        
        if ($expectCode !== null && $code !== $expectCode) {
            throw new Exception("指令失敗,預期 $expectCode 實際 $code: " . trim($line));
        }
        $this->response = $line;
        return $line;
    }

    private function helo(): void {
        $this->command("EHLO {$this->host}", 250);
    }

    private function auth(): void {
        $this->command('AUTH LOGIN', 334);
        $this->command(base64_encode($this->user), 334);
        $this->command(base64_encode($this->password), 235);
    }

    private function mailFrom(): void {
        $this->command("MAIL FROM:<{$this->from}>", 250);
    }

    private function rcptTo(string $email): void {
        $this->command("RCPT TO:<$email>", 250);
    }

    private function data(): void {
        $this->command('DATA', 354);
    }

    /** 發送郵件完整內容 */
    private function sendContent(): void {
        $headers = $this->buildHeaders();
        $body = $this->body;
        
        // 透明度處理:行首為 . 時加 .
        $headers = str_replace("\r\n.", "\r\n..", $headers);
        $body = str_replace("\r\n.", "\r\n..", $body);
        if ($body[0] === '.') $body = '.' . $body;
        
        fwrite($this->socket, $headers . "\r\n\r\n" . $body . "\r\n.\r\n");
        $this->readResponse(250);
    }

    private function buildHeaders(): string {
        $date = gmdate('D, d M Y H:i:s') . ' +0000';
        $toAddr = is_array($this->to) ? implode(',', $this->to) : $this->to;
        $contentType = $this->isHtml ? 'text/html' : 'text/plain';
        
        $headers = [
            "Date: $date",
            "To: " . $this->encodeAddr($this->toName, $toAddr),
            "From: " . $this->encodeAddr($this->fromName, $this->from),
            "Subject: " . $this->encodeSubject($this->subject),
            "Content-Type: $contentType; charset=UTF-8",
            "Content-Transfer-Encoding: base64",
            "MIME-Version: 1.0",
        ];
        return implode("\r\n", $headers);
    }

    private function encodeAddr(string $name, string $addr): string {
        return '=?' . 'UTF-8?B?' . base64_encode($name) . '?=' . " <$addr>";
    }

    private function encodeSubject(string $subject): string {
        return '=?' . 'UTF-8?B?' . base64_encode($subject) . '?=';
    }

    private function quit(): void {
        if ($this->socket) {
            @fwrite($this->socket, "QUIT\r\n");
            @fgets($this->socket);
            fclose($this->socket);
        }
    }

    public function getLog(): array { return $this->log; }
}
?>

使用範例

<?php
$mailer = new SmtpMailer([
    'host' => 'smtp.163.com',
    'port' => 25, // 或 465(SSL), 587(TLS)
    'user' => 'your_email@163.com',
    'password' => 'your_auth_code', // 授權碼,非登入密碼
    'from' => 'your_email@163.com',
    'fromName' => '系統通知',
    'to' => ['user1@example.com', 'user2@example.com'],
    'toName' => '尊敬的用戶',
    'subject' => '測試郵件 ' . date('Y-m-d H:i:s'),
    'body' => base64_encode('

你好!

這是一封測試郵件。

'), 'isHtml' => true, ]); if ($mailer->send()) { echo "發送成功!\n"; } else { echo "發送失敗\n"; } // 除錯:查看 SMTP 交互日誌 echo "
";
print_r($mailer->getLog());
echo "
"; ?>

163 郵箱存在性驗證應用

利用 RCPT TO 指令回應碼判斷郵箱是否存在:

<?php
function verify163Email(string $email): bool {
    $socket = fsockopen('smtp.163.com', 25, $errno, $errstr, 10);
    if (!$socket) return false;
    
    auto_detect_line_endings(true);
    $read = function($expect) use ($socket) {
        $line = fgets($socket);
        return (int)substr($line, 0, 3) === $expect;
    };
    
    $write = function($cmd) use ($socket) {
        fwrite($socket, $cmd . "\r\n");
    };
    
    try {
        $read(220) || throw new Exception('連線失敗');
        $write('HELO verify.example.com'); $read(250);
        $write('MAIL FROM:'); $read(250);
        $write("RCPT TO:<$email>"); 
        $result = $read(250); // 250 存在,550 不存在
        $write('QUIT');
        return $result;
    } catch (Exception $e) {
        return false;
    } finally {
        fclose($socket);
    }
}

// 批次驗證(需加延遲避免被封 IP)
$emails = ['test1@163.com', 'test2@163.com'];
foreach ($emails as $email) {
    echo "$email: " . (verify163Email($email) ? '存在' : '不存在/不可達') . "\n";
    sleep(2); // 禮貌延遲
}
?>

SSL/TLS 加密連線支援

<?php
// SSL 直連 (port 465)
$socket = fsockopen('ssl://smtp.163.com', 465, $errno, $errstr, 15);

// STARTTLS 升級 (port 587)
$socket = fsockopen('smtp.163.com', 587, $errno, $errstr, 15);
// ... EHLO ...
$write('STARTTLS'); $read(220);
// 升級為 SSL
$socket = stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
// ... 重新 EHLO 後繼續 AUTH ...
?>

生產環境建議對比

關鍵最佳實踐

  • ✅ 使用授權碼而非登入密碼
  • ✅ 設定 SPF/DKIM/DMARC 域名記錄
  • ✅ 郵件內容 Base64 編碼 + Quoted-Printable 長行折行
  • ✅ 批次發送加延遲、分散 IP、監控退信率
  • ✅ 記錄完整 SMTP 日誌供事後分析
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
方案優點缺點建議場景
Socket 手寫零依賴、完全控制、教學價值易出錯、需處理邊界情況、維護成本高學習、特殊需求、嵌入式環境
PHPMailer成熟穩定、功能完整、社群支援需 Composer、體積較大大多數生產專案
Symfony Mailer現代架構、Transport 抽象、異步支援需 Symfony 生態Symfony/Laravel 專案
原生 mail()無依賴、簡單功能極少、無認證、易進垃圾箱極簡測試、本地開發