PHP 內建 mail() 功能簡陋、無 SMTP 驗證、難以除錯;PHPMailer 雖強大但體積較大。若需在無 Composer 環境、嵌入式裝置或極簡專案中發送郵件,直接使用 PHP Socket 擴展 實作 SMTP 協定 (RFC 5321) 是輕量且可控的選擇。本文提供完整的 MySendMail 類別,支援純文字/HTML、SSL/TLS (STARTTLS)、附件擴充基礎。
Client Server
| |
|---- TCP Connect (port 25/465/587) --->|
|<--- 220 smtp.example.com Ready ------|
|---- HELO/EHLO hostname ------------->|
|<--- 250 OK --------------------------|
|---- AUTH LOGIN --------------------->|
|<--- 334 VXNlcm5hbWU6 (base64) -------|
|---- base64(username) --------------->|
|<--- 334 UGFzc3dvcmQ6 ----------------|
|---- base64(password) --------------->|
|<--- 235 Authentication successful ---|
|---- MAIL FROM:<sender@domain> ------->|
|<--- 250 OK --------------------------|
|---- RCPT TO:<rcpt@domain> ----------->|
|<--- 250 OK --------------------------|
|---- DATA ---------------------------->|
|<--- 354 Start mail input ------------|
|---- MIME Message (headers+body) ----->|
|---- . (單獨一行) -------------------->|
|<--- 250 Queued ----------------------|
|---- QUIT ---------------------------->|
|<--- 221 Bye -------------------------|
|---- Close Socket --------------------|
<?php
/**
* 輕量級 SMTP 發送類
* 需求:PHP 開啟 sockets 擴展 (php.ini: extension=sockets)
* 支援:純文字/HTML、STARTTLS (port 587)、SSL (port 465)
*/
class MySendMail
{
private string $host = '';
private int $port = 587;
private string $username = '';
private string $password = '';
private string $from = '';
private string $to = '';
private string $subject = '';
private string $body = '';
private bool $isHtml = false;
private bool $useTls = true; // STARTTLS
private bool $useSsl = false; // SMTPS (port 465)
private ?string $error = null;
private $socket = null;
// --- 設定器 ---
public function setServer(string $host, int $port = 587, bool $tls = true, bool $ssl = false): self
{
$this->host = $host;
$this->port = $port;
$this->useTls = $tls;
$this->useSsl = $ssl;
return $this;
}
public function setAuth(string $user, string $pass): self { $this->username = $user; $this->password = $pass; return $this; }
public function setFrom(string $email, string $name = ''): self { $this->from = $name ? "\"$name\" <$email>" : "<$email>"; return $this; }
public function setTo(string $email, string $name = ''): self { $this->to = $name ? "\"$name\" <$email>" : "<$email>"; return $this; }
public function setSubject(string $s): self { $this->subject = $s; return $this; }
public function setBody(string $body, bool $isHtml = false): self { $this->body = $body; $this->isHtml = $isHtml; return $this; }
public function getError(): ?string { return $this->error; }
// --- 核心發送 ---
public function send(): bool
{
try {
$this->connect();
$this->handshake();
$this->auth();
$this->mailTransaction();
$this->close();
return true;
} catch (Throwable $e) {
$this->error = $e->getMessage();
$this->close();
return false;
}
}
private function connect(): void
{
$flags = $this->useSsl ? STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT : STREAM_CLIENT_CONNECT;
$remote = ($this->useSsl ? 'ssl://' : 'tcp://') . $this->host . ':' . $this->port;
$this->socket = @stream_socket_client($remote, $errno, $errstr, 30, $flags);
if (!$this->socket) throw new RuntimeException("連線失敗:$errstr ($errno)");
stream_set_timeout($this->socket, 10);
$this->expect(220); // 歡迎訊息
}
private function handshake(): void
{
$this->command("EHLO " . gethostname());
$this->expect(250);
if ($this->useTls && !$this->useSsl) {
$this->command("STARTTLS");
$this->expect(220);
// 升級為 TLS
$crypto = stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
if (!$crypto) throw new RuntimeException('STARTTLS 升級失敗');
$this->command("EHLO " . gethostname()); // 重新 EHLO
$this->expect(250);
}
}
private function auth(): void
{
$this->command("AUTH LOGIN");
$this->expect(334);
$this->command(base64_encode($this->username));
$this->expect(334);
$this->command(base64_encode($this->password));
$this->expect(235);
}
private function mailTransaction(): void
{
$this->command("MAIL FROM:{$this->from}");
$this->expect(250);
$this->command("RCPT TO:{$this->to}");
$this->expect(250);
$this->command("DATA");
$this->expect(354);
$headers = "From: {$this->from}\r\n";
$headers .= "To: {$this->to}\r\n";
$headers .= "Subject: {$this->subject}\r\n";
$headers .= "MIME-Version: 1.0\r\n";
if ($this->isHtml) {
$headers .= "Content-Type: text/html; charset=UTF-8\r\n";
} else {
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
}
$headers .= "\r\n";
$message = $headers . $this->body . "\r\n.\r\n";
fwrite($this->socket, $message);
$this->expect(250);
$this->command("QUIT");
$this->expect(221);
}
private function command(string $cmd): void
{
fwrite($this->socket, $cmd . "\r\n");
// 可選:echo "C: $cmd\n"; $resp = fgets($this->socket); echo "S: $resp";
}
private function expect(int $code): void
{
$line = fgets($this->socket, 512);
if ($line === false) throw new RuntimeException('伺服器無回應 (timeout)');
$replyCode = (int) substr($line, 0, 3);
if ($replyCode !== $code) {
throw new RuntimeException("SMTP 錯誤 [$replyCode]:$line");
}
}
private function close(): void
{
if (is_resource($this->socket)) {
@fclose($this->socket);
$this->socket = null;
}
}
}
<?php
require 'MySendMail.php';
$mail = new MySendMail();
$mail->setServer('smtp.gmail.com', 587, true, false) // STARTTLS
->setAuth('your@gmail.com', 'app_password') // Gmail 需應用程式密碼
->setFrom('your@gmail.com', '系統通知')
->setTo('target@example.com', '收件者')
->setSubject('測試主旨 ' . date('Y-m-d H:i:s'))
->setBody('<h1>HTML 郵件測試</h1><p>支援中文內容</p>', true);
if ($mail->send()) {
echo "發送成功!";
} else {
echo "發送失敗:" . $mail->getError();
}
在 mailTransaction() 中將 Content-Type 改為 multipart/mixed; boundary="BOUNDARY",依序寫入:
--BOUNDARY
Content-Type: text/html; charset=UTF-8
HTML 內容
--BOUNDARY
Content-Type: application/pdf; name="report.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="report.pdf"
BASE64 編碼內容
--BOUNDARY--
建議改用 PHPMailer 或 Symfony Mime 處理複雜 MIME 結構,避免手工拼接錯誤。
php.ini 取消註解 extension=sockets,重啟 PHP-FPM/Apache。From 必須為已驗證網域。stream_context_set_option($ctx, 'ssl', 'verify_peer', false) 僅供測試,生產請更新 ca-certificates。