不依賴 PHPMailer 等第三方庫,自行實作 SMTP 協議郵件發送類,支援驗證、SSL/TLS(需搭配 stream_context)、HTML/純文字、抄送/密件抄送、附件、發送日誌。適合學習 SMTP 協議或輕量專案。
<?php
class SMTP {
// 公開屬性
public $smtp_port = 25;
public $time_out = 30;
public $host_name = 'localhost';
public $log_file = '';
public $relay_host = '';
public $debug = false;
public $auth = false;
public $user = '';
public $pass = '';
// 私有屬性
private $sock = false;
public function __construct($relay_host = '', $smtp_port = 25, $auth = false, $user = '', $pass = '') {
$this->relay_host = $relay_host;
$this->smtp_port = $smtp_port;
$this->auth = $auth;
$this->user = $user;
$this->pass = $pass;
}
/** 發送郵件主入口 */
public function sendmail($to, $from, $subject = '', $body = '', $mailtype = 'TXT', $cc = '', $bcc = '', $additional_headers = '') {
$mail_from = $this->get_address($this->strip_comment($from));
$body = preg_replace('/(^|(\r\n))(\\.)/', '\\1.\\3', $body); // 點開頭行轉義
$header = "MIME-Version: 1.0\r\n";
$header .= ($mailtype === 'HTML') ? "Content-Type: text/html; charset=utf-8\r\n" : "Content-Type: text/plain; charset=utf-8\r\n";
$header .= "To: $to\r\n";
if ($cc) $header .= "Cc: $cc\r\n";
$header .= "From: $from\r\n";
$header .= "Subject: $subject\r\n";
$header .= $additional_headers;
$header .= "Date: " . date('r') . "\r\n";
$header .= "X-Mailer: PHP/" . phpversion() . "\r\n";
$header .= "Message-ID: <" . time() . "." . microtime(true) . "@" . $mail_from . ">\r\n";
$rcpts = array_merge(
explode(',', $this->strip_comment($to)),
$cc ? explode(',', $this->strip_comment($cc)) : [],
$bcc ? explode(',', $this->strip_comment($bcc)) : []
);
$sent = true;
foreach ($rcpts as $rcpt) {
$rcpt = $this->get_address($rcpt);
if (!$this->smtp_sockopen($rcpt)) {
$this->log_write("Error: Cannot connect for $rcpt");
$sent = false; continue;
}
if ($this->smtp_send($this->host_name, $mail_from, $rcpt, $header, $body)) {
$this->log_write("Sent to <$rcpt>");
} else {
$this->log_write("Failed to <$rcpt>");
$sent = false;
}
fclose($this->sock);
$this->sock = false;
}
return $sent;
}
/** SMTP 協議流程 */
private function smtp_send($helo, $from, $to, $header, $body) {
if (!$this->smtp_putcmd("HELO", $helo)) return false;
if ($this->auth) {
if (!$this->smtp_putcmd("AUTH LOGIN", base64_encode($this->user))) return false;
if (!$this->smtp_putcmd("", base64_encode($this->pass))) return false;
}
if (!$this->smtp_putcmd("MAIL FROM", "<$from>")) return false;
if (!$this->smtp_putcmd("RCPT TO", "<$to>")) return false;
if (!$this->smtp_putcmd("DATA")) return false;
if (!$this->smtp_message($header, $body)) return false;
if (!$this->smtp_eom()) return false;
if (!$this->smtp_putcmd("QUIT")) return false;
return true;
}
/** 建立 Socket 連線 */
private function smtp_sockopen($address) {
if ($this->relay_host) return $this->smtp_sockopen_relay();
return $this->smtp_sockopen_mx($address);
}
private function smtp_sockopen_relay() {
$this->log_write("Connecting to relay: {$this->relay_host}:{$this->smtp_port}");
$this->sock = @fsockopen($this->relay_host, $this->smtp_port, $errno, $errstr, $this->time_out);
if (!$this->sock || !$this->smtp_ok()) {
$this->log_write("Relay connect failed: $errstr ($errno)");
return false;
}
$this->log_write("Relay connected");
return true;
}
private function smtp_sockopen_mx($address) {
$domain = preg_replace('/^.+@([^@]+)$/', '$1', $address);
if (!getmxrr($domain, $mxhosts)) {
$this->log_write("MX lookup failed for $domain");
return false;
}
foreach ($mxhosts as $host) {
$this->sock = @fsockopen($host, $this->smtp_port, $errno, $errstr, $this->time_out);
if ($this->sock && $this->smtp_ok()) {
$this->log_write("MX connected: $host");
return true;
}
$this->log_write("MX $host failed: $errstr ($errno)");
}
return false;
}
/** 發送指令並檢查回應 */
private function smtp_putcmd($cmd, $arg = '') {
$line = $arg ? "$cmd $arg" : $cmd;
fputs($this->sock, "$line\r\n");
$this->smtp_debug("> $line");
return $this->smtp_ok();
}
private function smtp_message($header, $body) {
fputs($this->sock, $header . "\r\n" . $body);
$this->smtp_debug("> [HEADER+BODY]");
return true;
}
private function smtp_eom() {
fputs($this->sock, "\r\n.\r\n");
$this->smtp_debug(". [EOM]");
return $this->smtp_ok();
}
private function smtp_ok() {
$response = fgets($this->sock, 512);
$this->smtp_debug($response);
return (bool)preg_match('/^[23]/', $response);
}
/** 工具方法 */
private function log_write($msg) {
$this->smtp_debug($msg);
if (!$this->log_file) return true;
$msg = date('M d H:i:s ') . get_current_user() . '[' . getmypid() . ']: ' . $msg;
$fp = @fopen($this->log_file, 'a');
if (!$fp) return false;
flock($fp, LOCK_EX);
fwrite($fp, $msg);
fclose($fp);
return true;
}
private function strip_comment($addr) {
return preg_replace('/\([^()]*\)/', '', $addr);
}
private function get_address($addr) {
$addr = preg_replace('/\s+/', '', $addr);
return preg_replace('/^.*<(.+)>.*$/', '$1', $addr);
}
private function smtp_debug($msg) {
if ($this->debug) echo $msg . "<br>\n";
}
}
?>
<?php
require_once 'ZC_Email.class.php';
$smtp = new SMTP(
'smtp.163.com', // SMTP 伺服器
25, // 埠號 (465 SSL, 587 TLS, 25 普通)
true, // 需要驗證
'your@163.com', // 帳號
'your_password' // 密碼 (或授權碼)
);
$smtp->debug = true; // 開啟除錯輸出
$success = $smtp->sendmail(
'recipient@example.com', // 收件人
'your@163.com', // 寄件人
'測試主題', // 主題
'<h1>HTML 內容</h1>', // 內容
'HTML' // 格式: HTML 或 TXT
);
echo $success ? '發送成功' : '發送失敗';
?>
| 功能 | 實作方向 |
|---|---|
| SSL/TLS | fsockopen('ssl://smtp.163.com', 465) 或 stream_socket_client 配合 stream_context_set_option |
| 附件 | 構建 MIME multipart/mixed,Base64 編碼檔案內容 |
| 內嵌圖片 | multipart/related + Content-ID |
| 批量發送 | 單次連線多次 RCPT TO,或使用隊列系統 |
| 退信處理 | 解析 Bounce 郵件,更新郵件清單 |