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

PHP SMTP 郵件發送類完整實作:支援附件、HTML、抄送、SSL/TLS 與錯誤日誌

     編輯:PHP 綜合知識

前言

PHP 內建 mail() 功能簡陋、無法支援 SMTP 認證、附件、SSL/TLS。本文提供一個功能完整的 SMTP 類別,支援:SMTP AUTH、SSL/TLS、HTML/純文字、多附件、抄送/密件副本、自定義 Headers、發送日誌、錯誤處理,可直接整合至專案使用。

SMTP 類別完整代碼

 $val) {
            if (property_exists($this, $key)) $this->$key = $val;
        }
    }

    // 主發送方法
    public function send($to, $subject, $body, $options = []) {
        $defaults = [
            'cc' => '',
            'bcc' => '',
            'attachments' => [], // ['/path/file.pdf', ...] 或 [['path'=>'...', 'name'=>'...', 'mime'=>'...'], ...]
            'isHtml' => true,
            'charset' => 'UTF-8',
            'headers' => [], // 額外 headers ['Reply-To' => '...', ...]
            'priority' => 3, // 1=高, 3=正常, 5=低
        ];
        $opts = array_merge($defaults, $options);

        // 建立 MIME 訊息
        $mime = $this->buildMimeMessage($to, $opts['cc'], $opts['bcc'], $subject, $body, $opts);

        // 連線 SMTP
        if (!$this->connect()) return false;

        // SMTP 交談
        try {
            $this->sendCommand('HELO', $this->heloHost);
            if ($this->auth) $this->authenticate();
            $this->sendCommand('MAIL FROM:', "<{$this->fromEmail}>");
            
            $recipients = array_merge(
                (array)$to,
                $opts['cc'] ? (array)$opts['cc'] : [],
                $opts['bcc'] ? (array)$opts['bcc'] : []
            );
            foreach ($recipients as $rcpt) {
                $this->sendCommand('RCPT TO:', "<{$rcpt}>");
            }
            
            $this->sendCommand('DATA');
            $this->sendData($mime);
            $this->sendCommand('QUIT');
            
            $this->log("郵件發送成功: " . implode(', ', (array)$to));
            return true;
        } catch (Exception $e) {
            $this->log("發送失敗: " . $e->getMessage());
            return false;
        } finally {
            $this->disconnect();
        }
    }

    // 建立 MIME 訊息 (支援附件、HTML/TEXT 替代)
    private function buildMimeMessage($to, $cc, $bcc, $subject, $body, $opts) {
        $boundary = '==MULTIPART_' . md5(uniqid(time())) . '==';
        $altBoundary = '==ALTERNATIVE_' . md5(uniqid(time())) . '==';
        
        $headers = [];
        $headers[] = "MIME-Version: 1.0";
        $headers[] = "Date: " . date('r');
        $headers[] = "From: {$this->encodeHeader($this->fromName)} <{$this->fromEmail}>";
        $headers[] = "To: " . $this->formatAddresses($to);
        if ($cc) $headers[] = "Cc: " . $this->formatAddresses($cc);
        if ($bcc) $headers[] = "Bcc: " . $this->formatAddresses($bcc);
        $headers[] = "Subject: " . $this->encodeHeader($subject);
        $headers[] = "Message-ID: <" . time() . "." . mt_rand() . "@{$this->heloHost}>";
        $headers[] = "X-Mailer: PHP SMTP Class";
        $headers[] = "X-Priority: {$opts['priority']}";
        
        // 自定義 headers
        foreach ($opts['headers'] as $k => $v) {
            $headers[] = "$k: $v";
        }

        $headerStr = implode("\r\n", $headers) . "\r\n\r\n";

        // 訊息主體
        $message = '';
        if (!empty($opts['attachments'])) {
            // 混合類型:含附件
            $message .= "--{$boundary}\r\n";
            $message .= "Content-Type: multipart/alternative; boundary=\"{$altBoundary}\"\r\n\r\n";
            
            // 純文字版本
            $message .= "--{$altBoundary}\r\n";
            $message .= "Content-Type: text/plain; charset=\"{$opts['charset']}\"\r\n";
            $message .= "Content-Transfer-Encoding: base64\r\n\r\n";
            $message .= base64_encode($opts['isHtml'] ? strip_tags($body) : $body) . "\r\n\r\n";
            
            // HTML 版本
            $message .= "--{$altBoundary}\r\n";
            $message .= "Content-Type: text/html; charset=\"{$opts['charset']}\"\r\n";
            $message .= "Content-Transfer-Encoding: base64\r\n\r\n";
            $message .= base64_encode($body) . "\r\n\r\n";
            $message .= "--{$altBoundary}--\r\n\r\n";
            
            // 附件
            foreach ($opts['attachments'] as $att) {
                $path = is_array($att) ? $att['path'] : $att;
                $name = is_array($att) ? ($att['name'] ?? basename($path)) : basename($path);
                $mime = is_array($att) ? ($att['mime'] ?? 'application/octet-stream') : 'application/octet-stream';
                
                if (!file_exists($path)) continue;
                $content = file_get_contents($path);
                
                $message .= "--{$boundary}\r\n";
                $message .= "Content-Type: {$mime}; name=\"{$name}\"\r\n";
                $message .= "Content-Transfer-Encoding: base64\r\n";
                $message .= "Content-Disposition: attachment; filename=\"{$name}\"\r\n\r\n";
                $message .= chunk_split(base64_encode($content)) . "\r\n\r\n";
            }
            $message .= "--{$boundary}--\r\n";
        } else {
            // 無附件:簡單 alternative
            $message .= "--{$altBoundary}\r\n";
            $message .= "Content-Type: text/plain; charset=\"{$opts['charset']}\"\r\n";
            $message .= "Content-Transfer-Encoding: base64\r\n\r\n";
            $message .= base64_encode($opts['isHtml'] ? strip_tags($body) : $body) . "\r\n\r\n";
            
            if ($opts['isHtml']) {
                $message .= "--{$altBoundary}\r\n";
                $message .= "Content-Type: text/html; charset=\"{$opts['charset']}\"\r\n";
                $message .= "Content-Transfer-Encoding: base64\r\n\r\n";
                $message .= base64_encode($body) . "\r\n\r\n";
            }
            $message .= "--{$altBoundary}--\r\n";
        }

        return $headerStr . $message;
    }

    // SMTP 連線
    private function connect() {
        $host = $this->secure === 'ssl' ? "ssl://{$this->host}" : $this->host;
        $this->socket = @fsockopen($host, $this->port, $errno, $errstr, $this->timeout);
        if (!$this->socket) {
            throw new Exception("無法連線 SMTP: $errstr ($errno)");
        }
        stream_set_timeout($this->socket, $this->timeout);
        $this->readResponse(); // 220
        return true;
    }

    // SMTP 認證 (AUTH LOGIN)
    private function authenticate() {
        $this->sendCommand('AUTH LOGIN');
        $this->sendCommand(base64_encode($this->username));
        $this->sendCommand(base64_encode($this->password));
    }

    // 發送命令並檢查回應
    private function sendCommand($cmd, $arg = '') {
        $line = $arg ? "$cmd $arg" : $cmd;
        fwrite($this->socket, $line . "\r\n");
        $this->debug("> $line");
        $response = $this->readResponse();
        if ($response[0] >= '4') { // 4xx/5xx 錯誤
            throw new Exception("SMTP 錯誤 [$cmd]: $response");
        }
        return $response;
    }

    // 發送 DATA 內容
    private function sendData($data) {
        fwrite($this->socket, $data . "\r\n.\r\n");
        $this->debug("> [DATA] " . strlen($data) . " bytes");
        $this->readResponse();
    }

    // 讀取 SMTP 回應
    private function readResponse() {
        $response = '';
        while ($line = fgets($this->socket, 512)) {
            $response .= $line;
            $this->debug("< $line");
            if (preg_match('/^\d{3} /', $line)) break;
        }
        return trim($response);
    }

    // 斷開連線
    private function disconnect() {
        if ($this->socket) {
            @fclose($this->socket);
            $this->socket = null;
        }
    }

    // 格式化地址列表
    private function formatAddresses($addresses) {
        $arr = (array)$addresses;
        return implode(', ', $arr);
    }

    // 編碼 Header (RFC 2047)
    private function encodeHeader($str) {
        if (preg_match('/[\x80-\xFF]/', $str)) {
            return '=?UTF-8?B?' . base64_encode($str) . '?=';
        }
        return $str;
    }

    // 日誌記錄
    private function log($msg) {
        $line = date('Y-m-d H:i:s') . " $msg\n";
        if ($this->debug) echo $line;
        if ($this->logFile) {
            @file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
        }
    }

    // 除錯輸出
    private function debug($msg) {
        if ($this->debug) error_log("[SMTP] $msg");
    }
}
?>

使用範例

基本發送 (HTML + 附件)

require 'SMTP.php';

$smtp = new SMTP([
    'host' => 'smtp.gmail.com',
    'port' => 465,
    'secure' => 'ssl',
    'auth' => true,
    'username' => 'your@gmail.com',
    'password' => 'app_password', // Gmail 應用程式密碼
    'fromEmail' => 'your@gmail.com',
    'fromName' => '我的系統',
    'logFile' => __DIR__ . '/logs/mail.log',
    'debug' => true,
]);

$success = $smtp->send(
    'recipient@example.com',
    '測試郵件主旨',
    '

HTML 內容

支援 中文 與圖片。

', [ 'cc' => 'cc@example.com', 'attachments' => [ ['path' => '/path/to/report.pdf', 'name' => '月報.pdf'], '/path/to/image.png', // 簡寫:只給路徑 ], 'isHtml' => true, 'headers' => ['Reply-To' => 'reply@example.com'], ] ); echo $success ? '發送成功' : '發送失敗';

批量發送 (效能優化:重用連線)

// 修改類別加入 batchSend() 或手動控制連線
// 此處示範單發循環 (簡單但每封重新連線)
$recipients = ['a@x.com', 'b@x.com', 'c@x.com'];
foreach ($recipients as $to) {
    $smtp->send($to, '批量通知', '內容...');
    usleep(100000); // 避免觸發頻率限制
}

Gmail / Outlook / 企業郵箱設定對照

服務商SMTP HostPortSecure備註
Gmailsmtp.gmail.com465ssl需開啟「應用程式密碼」
Gmailsmtp.gmail.com587tlsSTARTTLS
Outlook/Hotmailsmtp-mail.outlook.com587tls
Office 365smtp.office365.com587tls
163smtp.163.com465/25ssl/無需授權碼
QQsmtp.qq.com465/587ssl/tls需獨立密碼/授權碼

常見問題排查

現象原因解決
連線逾時Port/SSL 錯誤、防火牆阻擋確認 Port 465/587、telnet 測試連通性
認證失敗密碼錯誤、未開啟 SMTP/應用程式密碼重設授權碼、檢查帳號安全設定
進垃圾信箱SPF/DKIM/DMARC 未設定、內容觸發規則設定域名 DNS 記錄、優化內容、使用專用 IP
附件損壞/無法開啟檔案路徑錯誤、base64 編碼問題確認 file_exists、chunk_split(base64_encode())
中文主旨/附件名亂碼Header 未 RFC 2047 編碼類別內建 encodeHeader() 自動處理

現代替代方案建議

  • PHPMailer / Symfony Mailer:成熟、支援 OAuth2、DKIM 簽名、非同步隊列
  • API 發信 (SendGrid, Mailgun, Postmark, Amazon SES):高送達率、統計分析、模板管理、免維運 SMTP Server

小結

本 SMTP 類別實作完整 SMTP 協定流程,支援現代發信所需的所有核心功能。可直接複製使用,或作為理解 SMTP 運作原理的教學參考。生產環境高流量發信建議改用專業郵件 API 服務。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved