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

PHP 高性能郵件發送類:SMTP 協議原生實現、支援 SSL/TLS、附件、HTML 郵件

     編輯:PHP 綜合知識

為什麼不直接用 mail() 或 PHPMailer?

  • mail():依賴本地 MTA(sendmail/postfix),配置複雜、無 SMTP 驗證、進垃圾箱率高、不支援附件/HTML
  • PHPMailer:功能強大但體積大(>200KB)、依賴多、舊版本有安全漏洞
  • 原生 SMTP 類:零依賴、完全可控、適合嵌入框架、學習協議細節

核心類架構

class SmtpMailer {
    // 連接配置
    private string $host;
    private int $port = 25;
    private int $timeout = 30;
    private bool $auth = true;
    private string $username;
    private string $password;
    private bool $ssl = false;      // SSL/TLS 模式
    private bool $tls = false;      // STARTTLS 升級
    
    // 運行時資源
    private $socket;
    private bool $connected = false;
    
    // 日誌與調試
    private bool $debug = false;
    private string $logFile = '';
    private string $lastError = '';

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

    // 核心發送入口
    public function send(
        string|array $to,
        string $from,
        string $subject,
        string $body,
        bool $isHtml = true,
        array $cc = [],
        array $bcc = [],
        array $attachments = []
    ): bool {
        // 1. 建立連接
        if (!$this->connect()) return false;
        
        // 2. SMTP 會話流程
        try {
            $this->ehlo();
            if ($this->tls) $this->startTls();
            if ($this->auth) $this->authLogin();
            
            $this->mailFrom($from);
            $allRcpt = $this->rcptTo(array_merge((array)$to, $cc, $bcc));
            
            $this->data();
            $this->sendMessage($from, $to, $subject, $body, $isHtml, $cc, $bcc, $attachments);
            $this->dataEnd();
            
            $this->quit();
            return true;
        } catch (Exception $e) {
            $this->lastError = $e->getMessage();
            $this->log($e->getMessage());
            return false;
        } finally {
            $this->close();
        }
    }

    // ========== 底層協議實現 ==========
    
    private function connect(): bool {
        $prefix = $this->ssl ? 'ssl://' : '';
        $this->socket = @fsockopen($prefix . $this->host, $this->port, $errno, $errstr, $this->timeout);
        if (!$this->socket) {
            throw new Exception("連接失敗: $errstr ($errno)");
        }
        stream_set_timeout($this->socket, $this->timeout);
        return $this->readResponse(220);  // 等待服務器就緒
    }

    private function sendCommand(string $cmd, string $args = ''): bool {
        $line = $cmd . ($args ? ' ' . $args : '') . "\r\n";
        $this->log(">> $line");
        fwrite($this->socket, $line);
        return $this->readResponse(250);  // 通用成功碼
    }

    private function readResponse(int $expectCode = 0): bool {
        $response = '';
        while ($line = fgets($this->socket, 512)) {
            $this->log("<< $line");
            $response .= $line;
            if (preg_match('/^\d{3} /', $line)) break;  // 最後一行以代碼開頭
        }
        $code = (int)substr($response, 0, 3);
        if ($expectCode && $code !== $expectCode) {
            throw new Exception("SMTP 錯誤 [$code]: $response");
        }
        return $code >= 200 && $code < 400;
    }

    private function ehlo(): bool {
        return $this->sendCommand('EHLO', gethostname());
    }

    private function startTls(): bool {
        if (!$this->sendCommand('STARTTLS')) return false;
        // 升級為加密流
        $crypto = stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
        if (!$crypto) throw new Exception('TLS 升級失敗');
        return $this->ehlo();  // 重新 EHLO
    }

    private function authLogin(): bool {
        if (!$this->sendCommand('AUTH LOGIN')) return false;
        if (!$this->sendCommand(base64_encode($this->username))) return false;
        return $this->sendCommand(base64_encode($this->password));
    }

    private function mailFrom(string $from): bool {
        return $this->sendCommand('MAIL FROM', "<$from>");
    }

    private function rcptTo(array $recipients): bool {
        foreach ($recipients as $rcpt) {
            if (!$this->sendCommand('RCPT TO', "<$rcpt>")) return false;
        }
        return true;
    }

    private function data(): bool {
        return $this->sendCommand('DATA', '', 354);  // 354 進入數據模式
    }

    private function sendMessage(
        string $from,
        array|string $to,
        string $subject,
        string $body,
        bool $isHtml,
        array $cc,
        array $bcc,
        array $attachments
    ): void {
        $headers = [];
        $headers[] = "From: $from";
        $headers[] = "To: " . implode(', ', (array)$to);
        if ($cc) $headers[] = "Cc: " . implode(', ', $cc);
        $headers[] = "Subject: $subject";
        $headers[] = "Date: " . date('r');
        $headers[] = "MIME-Version: 1.0";
        $headers[] = "Message-ID: <" . microtime(true) . "@" . gethostname() . ">";
        $headers[] = "X-Mailer: PHP SmtpMailer";

        if ($attachments) {
            $this->buildMultipartMessage($headers, $body, $isHtml, $attachments);
        } else {
            $headers[] = "Content-Type: " . ($isHtml ? 'text/html' : 'text/plain') . "; charset=UTF-8";
            $headers[] = "Content-Transfer-Encoding: base64";
            fwrite($this->socket, implode("\r\n", $headers) . "\r\n\r\n");
            fwrite($this->socket, chunk_split(base64_encode($body)) . "\r\n");
        }
    }

    private function buildMultipartMessage(
        array &$headers,
        string $body,
        bool $isHtml,
        array $attachments
    ): void {
        $boundary = '----PHP_SMTP_' . md5(microtime(true));
        $headers[] = "Content-Type: multipart/mixed; boundary=\"$boundary\"";
        fwrite($this->socket, implode("\r\n", $headers) . "\r\n\r\n");

        // 文本部分
        fwrite($this->socket, "--$boundary\r\n");
        fwrite($this->socket, "Content-Type: " . ($isHtml ? 'text/html' : 'text/plain') . "; charset=UTF-8\r\n");
        fwrite($this->socket, "Content-Transfer-Encoding: base64\r\n\r\n");
        fwrite($this->socket, chunk_split(base64_encode($body)) . "\r\n");

        // 附件部分
        foreach ($attachments as $att) {
            [$path, $name, $mime] = $att + ['', basename($att[0]), 'application/octet-stream'];
            $content = file_get_contents($path);
            fwrite($this->socket, "--$boundary\r\n");
            fwrite($this->socket, "Content-Type: $mime; name=\"$name\"\r\n");
            fwrite($this->socket, "Content-Transfer-Encoding: base64\r\n");
            fwrite($this->socket, "Content-Disposition: attachment; filename=\"$name\"\r\n\r\n");
            fwrite($this->socket, chunk_split(base64_encode($content)) . "\r\n");
        }
        fwrite($this->socket, "--$boundary--\r\n");
    }

    private function dataEnd(): bool {
        return $this->sendCommand('.', '', 250);  // 單獨一行 . 結束
    }

    private function quit(): void {
        @$this->sendCommand('QUIT');
    }

    private function close(): void {
        if ($this->socket) {
            fclose($this->socket);
            $this->socket = null;
            $this->connected = false;
        }
    }

    // ========== 工具方法 ==========
    private function log(string $msg): void {
        if ($this->debug) echo $msg . "\n";
        if ($this->logFile) {
            error_log(date('Y-m-d H:i:s ') . $msg . "\n", 3, $this->logFile);
        }
    }

    public function getLastError(): string {
        return $this->lastError;
    }
}

使用示例

$mailer = new SmtpMailer([
    'host' => 'smtp.qq.com',
    'port' => 465,
    'ssl' => true,           // 隱式 SSL
    'auth' => true,
    'username' => 'your@qq.com',
    'password' => '授權碼',    // 非登錄密碼
    'timeout' => 30,
    'debug' => true,
    'logFile' => __DIR__ . '/logs/smtp.log',
]);

$success = $mailer->send(
    to: 'target@example.com',
    from: 'your@qq.com',
    subject: '測試郵件 ' . date('H:i:s'),
    body: '

HTML 郵件測試

當前時間:' . date('Y-m-d H:i:s') . '

', isHtml: true, cc: ['cc1@example.com'], attachments: [ [__DIR__ . '/report.pdf', 'monthly_report.pdf', 'application/pdf'], [__DIR__ . '/logo.png', 'logo.png', 'image/png'], ] ); if ($success) { echo "發送成功"; } else { echo "失敗: " . $mailer->getLastError(); }

常見問題排查

現象原因解決
連接超時端口被防火牆攔截、SSL/TLS 模式錯誤telnet 測試端口、嘗試 25/465/587、確認 ssl/tls 參數
認證失敗密碼錯誤、未開啟 SMTP 服務、需授權碼登錄郵箱網頁版開啟 SMTP、生成授權碼
進垃圾箱缺少 SPF/DKIM/DMARC、IP 信譽低、內容觸發規則配置域名解析、使用可信發信 IP、優化內容、預熱 IP
中文亂碼編碼聲明缺失確保 Subject/Base64 編碼、Content-Type charset=UTF-8

生產環境建議

  1. 異步發送:將郵件推入隊列,Worker 進程消費,避免阻塞 HTTP 響應
  2. 連接池複用:長連接發送批量郵件,減少握手開銷
  3. 發信池輪換:多 IP、多帳號輪換,降低單 IP 信譽風險
  4. 退信處理:解析 Bounce 郵件,自動清理無效地址
  5. 監控告警:發送成功率、延遲、隊列堆積指標接入 Prometheus/Grafana
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved