連線 → 220 歡迎 HELO/EHLO → 250 OK AUTH LOGIN → 334 用戶名 → 334 密碼 → 235 認證成功 MAIL FROM: → 250 OK RCPT TO: → 250 OK (此處可驗證郵箱是否存在) DATA → 354 開始輸入 郵件內容(Header + Body) . → 250 郵件已排隊 QUIT → 221 關閉
<?php
class SmtpMailer {
// 連線設定
public $host, $port = 25, $user, $password;
// 郵件內容
public $from, $fromName, $to, $toName, $subject, $body, $isHtml = true;
// 內部狀態
private $socket;
private $response = '';
private $log = [];
public function __construct(array $config = []) {
foreach ($config as $k => $v) if (property_exists($this, $k)) $this->$k = $v;
}
/** 發送郵件主流程 */
public function send(): bool {
try {
$this->connect();
$this->helo();
$this->auth();
$this->mailFrom();
$recipients = is_array($this->to) ? $this->to : [$this->to];
foreach ($recipients as $rcpt) {
$this->rcptTo($rcpt);
}
$this->data();
$this->sendContent();
$this->quit();
return true;
} catch (Exception $e) {
$this->log[] = 'ERROR: ' . $e->getMessage();
return false;
}
}
/** 建立 Socket 連線 */
private function connect(): void {
$this->socket = @fsockopen($this->host, $this->port, $errno, $errstr, 15);
if (!$this->socket) throw new Exception("連線失敗: $errstr ($errno)");
$this->readResponse(220);
}
/** 發送指令並驗證回應碼 */
private function command(string $cmd, int $expectCode): void {
fwrite($this->socket, $cmd . "\r\n");
$this->readResponse($expectCode);
}
/** 讀取伺服器回應 */
private function readResponse(int $expectCode = null): string {
$line = fgets($this->socket, 512);
$this->log[] = 'S: ' . trim($line);
$code = (int)substr($line, 0, 3);
if ($expectCode !== null && $code !== $expectCode) {
throw new Exception("指令失敗,預期 $expectCode 實際 $code: " . trim($line));
}
$this->response = $line;
return $line;
}
private function helo(): void {
$this->command("EHLO {$this->host}", 250);
}
private function auth(): void {
$this->command('AUTH LOGIN', 334);
$this->command(base64_encode($this->user), 334);
$this->command(base64_encode($this->password), 235);
}
private function mailFrom(): void {
$this->command("MAIL FROM:<{$this->from}>", 250);
}
private function rcptTo(string $email): void {
$this->command("RCPT TO:<$email>", 250);
}
private function data(): void {
$this->command('DATA', 354);
}
/** 發送郵件完整內容 */
private function sendContent(): void {
$headers = $this->buildHeaders();
$body = $this->body;
// 透明度處理:行首為 . 時加 .
$headers = str_replace("\r\n.", "\r\n..", $headers);
$body = str_replace("\r\n.", "\r\n..", $body);
if ($body[0] === '.') $body = '.' . $body;
fwrite($this->socket, $headers . "\r\n\r\n" . $body . "\r\n.\r\n");
$this->readResponse(250);
}
private function buildHeaders(): string {
$date = gmdate('D, d M Y H:i:s') . ' +0000';
$toAddr = is_array($this->to) ? implode(',', $this->to) : $this->to;
$contentType = $this->isHtml ? 'text/html' : 'text/plain';
$headers = [
"Date: $date",
"To: " . $this->encodeAddr($this->toName, $toAddr),
"From: " . $this->encodeAddr($this->fromName, $this->from),
"Subject: " . $this->encodeSubject($this->subject),
"Content-Type: $contentType; charset=UTF-8",
"Content-Transfer-Encoding: base64",
"MIME-Version: 1.0",
];
return implode("\r\n", $headers);
}
private function encodeAddr(string $name, string $addr): string {
return '=?' . 'UTF-8?B?' . base64_encode($name) . '?=' . " <$addr>";
}
private function encodeSubject(string $subject): string {
return '=?' . 'UTF-8?B?' . base64_encode($subject) . '?=';
}
private function quit(): void {
if ($this->socket) {
@fwrite($this->socket, "QUIT\r\n");
@fgets($this->socket);
fclose($this->socket);
}
}
public function getLog(): array { return $this->log; }
}
?>
<?php
$mailer = new SmtpMailer([
'host' => 'smtp.163.com',
'port' => 25, // 或 465(SSL), 587(TLS)
'user' => 'your_email@163.com',
'password' => 'your_auth_code', // 授權碼,非登入密碼
'from' => 'your_email@163.com',
'fromName' => '系統通知',
'to' => ['user1@example.com', 'user2@example.com'],
'toName' => '尊敬的用戶',
'subject' => '測試郵件 ' . date('Y-m-d H:i:s'),
'body' => base64_encode('你好!
這是一封測試郵件。
'),
'isHtml' => true,
]);
if ($mailer->send()) {
echo "發送成功!\n";
} else {
echo "發送失敗\n";
}
// 除錯:查看 SMTP 交互日誌
echo "";
print_r($mailer->getLog());
echo "
";
?>
利用 RCPT TO 指令回應碼判斷郵箱是否存在:
<?php
function verify163Email(string $email): bool {
$socket = fsockopen('smtp.163.com', 25, $errno, $errstr, 10);
if (!$socket) return false;
auto_detect_line_endings(true);
$read = function($expect) use ($socket) {
$line = fgets($socket);
return (int)substr($line, 0, 3) === $expect;
};
$write = function($cmd) use ($socket) {
fwrite($socket, $cmd . "\r\n");
};
try {
$read(220) || throw new Exception('連線失敗');
$write('HELO verify.example.com'); $read(250);
$write('MAIL FROM:'); $read(250);
$write("RCPT TO:<$email>");
$result = $read(250); // 250 存在,550 不存在
$write('QUIT');
return $result;
} catch (Exception $e) {
return false;
} finally {
fclose($socket);
}
}
// 批次驗證(需加延遲避免被封 IP)
$emails = ['test1@163.com', 'test2@163.com'];
foreach ($emails as $email) {
echo "$email: " . (verify163Email($email) ? '存在' : '不存在/不可達') . "\n";
sleep(2); // 禮貌延遲
}
?>
<?php
// SSL 直連 (port 465)
$socket = fsockopen('ssl://smtp.163.com', 465, $errno, $errstr, 15);
// STARTTLS 升級 (port 587)
$socket = fsockopen('smtp.163.com', 587, $errno, $errstr, 15);
// ... EHLO ...
$write('STARTTLS'); $read(220);
// 升級為 SSL
$socket = stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
// ... 重新 EHLO 後繼續 AUTH ...
?>
| 方案 | 優點 | 缺點 | 建議場景 |
|---|---|---|---|
| Socket 手寫 | 零依賴、完全控制、教學價值 | 易出錯、需處理邊界情況、維護成本高 | 學習、特殊需求、嵌入式環境 |
| PHPMailer | 成熟穩定、功能完整、社群支援 | 需 Composer、體積較大 | 大多數生產專案 |
| Symfony Mailer | 現代架構、Transport 抽象、異步支援 | 需 Symfony 生態 | Symfony/Laravel 專案 |
| 原生 mail() | 無依賴、簡單 | 功能極少、無認證、易進垃圾箱 | 極簡測試、本地開發 |