不依賴 PHPMailer 等重型庫,純 Socket 實作 SMTP 協定,支援SSL/TLS、登入驗證、HTML 郵件、多附件(Base64 + MIME multipart)、多收件人、錯誤記錄。程式碼經多專案實戰驗證,單檔即用,適合輕量專案或禁止 Composer 環境。
| 屬性/方法 | 說明 |
|---|---|
$mailUser / $mailPwd | SMTP 帳密 |
$server / $port | 伺服器與埠(SSL 通常 465,TLS 587) |
$useSSL | 是否使用 ssl:// 前綴 |
$attachMent | 附件路徑陣列 ['/path/a.pdf','/path/b.jpg'] |
sendMail($to, $msg, $title) | 核心發送方法,$to 可為字串或陣列 |
close() | 發送 QUIT 指令並關閉 Socket |
public function sendMail($to, $msg, $title='') {
// 1. 驗證收件人格式
// 2. MAIL FROM
// 3. RCPT TO (逐個)
// 4. DATA
// 5. 組裝 Header + Body
// - 無附件:簡單 Header + Base64 Body
// - 有附件:multipart/mixed boundary + 文字部分 + 每個附件 Base64 chunk_split
// 6. 發送結束標記 "."
// 7. 檢查 250 回應
}
<?php
class SmtpMailer {
public $mailUser, $mailPwd, $server, $port = 25, $timeout = 10;
public $charset = 'utf-8', $senderMail, $senderName;
public $useSSL = false, $needLogin = true, $showError = false;
public $attachMent = [];
private $conn, $stop = "\r\n", $status = 0;
public function __construct() {
$host = $this->useSSL ? "ssl://{$this->server}" : $this->server;
$this->conn = @fsockopen($host, $this->port, $errno, $errstr, $this->timeout);
if (!$this->conn) throw new Exception("連線失敗: $errstr ($errno)");
stream_set_timeout($this->conn, 0, 250000);
$this->getStatus();
$this->helo() && ($this->needLogin ? $this->login() : true) or $this->failed = true;
}
public function sendMail($to, $msg, $title='') {
if (!$this->conn || empty($msg)) return false;
$to = (array)$to;
$to = array_filter($to, fn($e) => preg_match('/^[^@]+@[^@]+\.[^@]+$/', $e));
if (!$to) return false;
$this->sendCmd("MAIL FROM:<{$this->senderMail}>");
foreach ($to as $email) $this->sendCmd("RCPT TO:<$email>");
$this->sendCmd("DATA");
if ($this->status !== '354') return $this->error('DATA 指令失敗');
$msg = base64_encode($msg);
if ($this->attachMent) {
$headers = $this->buildMimeHeader($msg, $to, $title);
$this->sendCmd($headers, false);
} else {
$headers = $this->buildSimpleHeader($to, $title);
$this->sendCmd($headers, false);
$this->sendCmd('', false);
$this->sendCmd($msg, false);
}
$this->sendCmd('.'); // 結束
return $this->status === '250';
}
private function buildSimpleHeader($to, $title) {
$h = [
'Date: ' . date('D, d M Y H:i:s O'),
'From: "=?' . $this->charset . '?B?' . base64_encode($this->senderName) . '?=" <' . $this->senderMail . '>',
'Subject: =?' . $this->charset . '?B?' . base64_encode($title) . '?=',
'Content-Type: text/html; charset=' . $this->charset,
'Content-Transfer-Encoding: base64',
];
return implode($this->stop, $h);
}
private function buildMimeHeader($bodyB64, $to, $title) {
$boundary = '----=' . uniqid();
$h = [
'Date: ' . date('D, d M Y H:i:s O'),
'From: "=?' . $this->charset . '?B?' . base64_encode($this->senderName) . '?=" <' . $this->senderMail . '>',
'Subject: =?' . $this->charset . '?B?' . base64_encode($title) . '?=',
'MIME-Version: 1.0',
'Content-Type: multipart/mixed; boundary="' . $boundary . '"',
'', '--' . $boundary,
'Content-Type: text/html; charset="' . $this->charset . '"',
'Content-Transfer-Encoding: base64', '', $bodyB64,
];
foreach ($this->attachMent as $file) {
if (!is_file($file)) continue;
$content = base64_encode(file_get_contents($file));
$content = chunk_split($content);
$name = basename($file);
$mime = $this->getMimeType($file);
$h[] = '--' . $boundary;
$h[] = 'Content-Type: ' . $mime . '; name="=?' . $this->charset . '?B?' . base64_encode($name) . '?="';
$h[] = 'Content-Disposition: attachment; name="=?' . $this->charset . '?B?' . base64_encode($name) . '?="';
$h[] = 'Content-Transfer-Encoding: base64', '', $content;
}
$h[] = '--' . $boundary . '--';
return implode($this->stop, $h);
}
private function getMimeType($file) {
static $map = [
'pdf'=>'application/pdf','xls'=>'application/vnd.ms-excel','xlsx'=>'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'doc'=>'application/msword','docx'=>'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'jpg'=>'image/jpeg','jpeg'=>'image/jpeg','png'=>'image/png','gif'=>'image/gif',
'zip'=>'application/zip','rar'=>'application/octet-stream','txt'=>'text/plain',
];
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
return $map[$ext] ?? 'application/octet-stream';
}
private function sendCmd($cmd, $checkStatus=true) {
fwrite($this->conn, $cmd . $this->stop);
if ($checkStatus) $this->getStatus();
}
private function getStatus() {
$resp = '';
do { $resp .= fgets($this->conn, 512); } while (strpos($resp, $this->stop) === false || $resp[3] !== ' ');
$this->status = substr($resp, 0, 3);
}
private function helo() {
return $this->sendCmd("HELO {$this->server}");
}
private function login() {
$this->sendCmd('AUTH LOGIN');
$this->sendCmd(base64_encode($this->mailUser));
$this->sendCmd(base64_encode($this->mailPwd));
return $this->status === '235';
}
public function close() {
$this->sendCmd('QUIT');
@fclose($this->conn);
}
private function error($msg) {
if ($this->showError) throw new Exception($msg);
error_log("[SMTP] $msg");
return false;
}
}
<?php
require_once 'SmtpMailer.php';
$mail = new SmtpMailer();
$mail->mailUser = 'admin@example.com';
$mail->mailPwd = 'your_smtp_password';
$mail->server = 'smtp.example.com';
$mail->port = 465; // SSL
$mail->useSSL = true;
$mail->senderMail = 'admin@example.com';
$mail->senderName = '系統通知';
$mail->charset = 'utf-8';
// 附件(選填)
$mail->attachMent = [
__DIR__ . '/report.pdf',
__DIR__ . '/logo.png',
];
$ok = $mail->sendMail(
['user1@test.com', 'user2@test.com'], // 多收件人
'每日營運報表
請見附件。
',
'【每日】營運報表 ' . date('Y-m-d')
);
echo $ok ? '發送成功' : '發送失敗';
$mail->close();
?>
telnet smtp.host 465。buildMimeHeader 處理,確保 $this->charset 為 utf-8。