PHP 內建 mail() 函數功能有限:不支援 SMTP 認證、SSL/TLS 加密、附件、HTML 郵件等企業級需求。本文提供一個功能完整的 SMTP 郵件發送類,基於 Socket 直連郵件服務器,支援 SSL 加密、多收件人、抄送、密送、附件、HTML/文本混合格式,經測試兼容 Gmail、QQ 郵箱、163 等主流服務商。
<?php
/**
* SMTP 郵件發送類
* 依賴擴展:sockets、openssl、fileinfo
* 編碼:UTF-8,傳輸編碼:Base64
*/
class MySendMail {
// 屬性定義...
protected $_userName;
protected $_password;
protected $_sendServer;
protected $_port = 25;
protected $_from;
protected $_to = array();
protected $_cc = array();
protected $_bcc = array();
protected $_subject;
protected $_body;
protected $_attachment = array();
protected $_socket;
protected $_isSecurity = false;
protected $_errorMessage;
/**
* 設置 SMTP 服務器
* @param string $server 服務器地址
* @param string $username 登錄用戶名
* @param string $password 登錄密碼/授權碼
* @param int $port 端口 (25/465/587)
* @param bool $isSecurity 是否啟用 SSL
* @return bool
*/
public function setServer($server, $username="", $password="", $port=25, $isSecurity=false) {
$this->_sendServer = $server;
$this->_port = $port;
$this->_isSecurity = $isSecurity;
// SMTP AUTH LOGIN 需要 Base64 編碼的用戶名密碼
$this->_userName = empty($username) ? "" : base64_encode($username);
$this->_password = empty($password) ? "" : base64_encode($password);
return true;
}
// 設置發件人、收件人、抄送、密送、附件、主題正文等 setter 方法...
// (為節省篇幅省略,完整代碼見下方)
/**
* 發送郵件主入口
* @return bool
*/
public function sendMail() {
$command = $this->getCommand(); // 構建完整 SMTP 命令序列
// 建立連接
$this->_isSecurity ? $this->socketSecurity() : $this->socket();
// 逐條發送命令並驗證回應碼
foreach ($command as $value) {
$result = $this->_isSecurity
? $this->sendCommandSecurity($value[0], $value[1])
: $this->sendCommand($value[0], $value[1]);
if (!$result) return false;
}
$this->_isSecurity ? $this->closeSecutity() : $this->close();
return true;
}
/**
* 構建 SMTP 命令序列(核心邏輯)
* @return array [[命令, 期望回應碼], ...]
*/
protected function getCommand() {
$separator = "----=_Part_" . md5($this->_from . time()) . uniqid();
$command = array(array("HELO sendmail\r\n", 250));
// 認證流程
if (!empty($this->_userName)) {
$command[] = array("AUTH LOGIN\r\n", 334);
$command[] = array($this->_userName . "\r\n", 334);
$command[] = array($this->_password . "\r\n", 235);
}
// MAIL FROM
$command[] = array("MAIL FROM: <" . $this->_from . ">\r\n", 250);
$header = "FROM: <" . $this->_from . ">\r\n";
// RCPT TO (收件人/抄送/密送)
// ... 循環構建 TO/CC/BCC 命令與 Header
// 郵件頭構建
$header .= "Subject: =?UTF-8?B?" . $this->_subject . "?=\r\n";
// 根據是否有附件決定 Content-Type
if (!empty($this->_attachment)) {
$header .= "Content-Type: multipart/mixed;\r\n";
} else {
$header .= "Content-Type: multipart/alternative;\r\n";
}
$header .= "\tboundary=\"" . $separator . "\"\r\n";
$header .= "MIME-Version: 1.0\r\n";
// 郵件體:HTML 部分
$header .= "\r\n--" . $separator . "\r\n";
$header .= "Content-Type: text/html; charset=utf-8\r\n";
$header .= "Content-Transfer-Encoding: base64\r\n\r\n";
$header .= $this->_body . "\r\n"; // 已 Base64 編碼
$header .= "--" . $separator . "\r\n";
// 附件部分
if (!empty($this->_attachment)) {
foreach ($this->_attachment as $file) {
$header .= "\r\n--" . $separator . "\r\n";
$header .= "Content-Type: " . $this->getMIMEType($file) . '; name="=?UTF-8?B?' . base64_encode(basename($file)) . '?="' . "\r\n";
$header .= "Content-Transfer-Encoding: base64\r\n";
$header .= 'Content-Disposition: attachment; filename="=?UTF-8?B?' . base64_encode(basename($file)) . '?="' . "\r\n";
$header .= "\r\n";
$header .= $this->readFile($file); // 文件內容 Base64
$header .= "\r\n--" . $separator . "\r\n";
}
}
$header .= "\r\n.\r\n"; // 數據結束標誌
$command[] = array("DATA\r\n", 354);
$command[] = array($header, 250);
$command[] = array("QUIT\r\n", 221);
return $command;
}
// Socket 連接、SSL 連接、命令發送、文件讀取、MIME 類型獲取等 protected 方法...
// (完整代碼較長,關鍵點見下方說明)
}
使用 PHP Stream 套接字而非傳統 Socket,支援現代 TLS 協議:
protected function socketSecurity() {
$remoteAddr = "tcp://" . $this->_sendServer . ":" . $this->_port;
$this->_socket = stream_socket_client($remoteAddr, $errno, $errstr, 30);
if (!$this->_socket) { $this->_errorMessage = $errstr; return false; }
// 關鍵:啟用 TLS/SSL 加密,STREAM_CRYPTO_METHOD_TLS_CLIENT 更安全
stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
stream_set_blocking($this->_socket, 1);
$str = fread($this->_socket, 1024);
if (!preg_match("/220+/", $str)) { $this->_errorMessage = $str; return false; }
return true;
}
採用 RFC 2047 標準:=?UTF-8?B?Base64編碼內容?=,確保跨客戶端顯示正常。
每條命令發送後讀取服務器回應,通過正則匹配期望代碼(如 250、334、354),失敗即拋出錯誤並終止流程。
<?php
require_once 'MySendMail.class.php';
$mail = new MySendMail();
// 配置:QQ 郵箱 SMTP (SSL 465 端口)
$mail->setServer("smtp.qq.com", "your_qq@qq.com", "授權碼", 465, true);
// 或 Gmail
// $mail->setServer("smtp.gmail.com", "your@gmail.com", "應用專用密碼", 465, true);
$mail->setFrom("your_qq@qq.com"); // 發件人
$mail->setReceiver("recipient1@example.com"); // 收件人
$mail->setCc("cc@example.com"); // 抄送
$mail->setBcc("bcc@example.com"); // 密送
$mail->addAttachment("/path/to/report.pdf"); // 附件
$mail->addAttachment("/path/to/image.png");
$mail->setMail("項目週報", "<h3>本週進度</h3><p>詳細內容...</p>"); // 主題、HTML 正文
if ($mail->sendMail()) {
echo "郵件發送成功!";
} else {
echo "發送失敗: " . $mail->error();
}
| 問題 | 原因 | 解決方案 |
|---|---|---|
| 認證失敗 (535) | 密碼錯誤/未開啟 SMTP/使用登錄密碼而非授權碼 | 郵箱後台開啟 SMTP,獲取專用授權碼/應用密碼 |
| 連接超時 | 端口被封/防火牆/服務器地址錯誤 | telnet 測試端口連通性,確認 465/587/25 端口 |
| 附件亂碼/無法打開 | 文件讀取編碼錯誤/MIME 類型不匹配 | 使用 file_get_contents + base64_encode,fileinfo 擴展獲取 MIME |
| 進入垃圾箱 | 無 SPF/DKIM/DMARC、內容敏感、IP 信譽差 | 配置域名解析記錄、優化內容、使用可信發信服務商 |
| SSL 握手失敗 | PHP OpenSSL 版本過低/協議不匹配 | 升級 PHP,嘗試 STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT |
本類通過底層 Socket 實現了完整的 SMTP 協議交互,無需依賴第三方庫(如 PHPMailer),適合學習協議細節或輕量級項目。生產環境高併發場景建議:使用異步隊列(如 Redis + Worker)剝離發送邏輯、接入專業發信服務(SendGrid、Mailgun、阿里云邮件推送)提升送達率、實現退訂/彈回/打開追蹤等高級功能。