PHP 邮件发送零依赖方案:原生 SMTP 客户端实现要点

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-16 14:27 ·4 浏览 ·0 回复

PHP 原生发邮件不需要任何第三方库:用 stream_socket_client 打开一个 socket,照着 SMTP 协议跟服务器"一问一答"地说话,几百行代码就能完成认证、TLS 加密和 MIME 邮件构造,完全不依赖 Composer、PHPMailer 或本地 sendmail。这也是 Clara BBS 这类无 Composer、无命令行环境的轻量 PHP 系统能自带邮件能力的前提——后台「系统设置→邮件」和「邮件中心」的底层,本质就是这样一个手写的 SMTP 客户端。

SMTP 的本质:一场有严格顺序的对话

结论:SMTP 客户端就是"发一行命令、读一段响应"的循环,难点不在协议本身,而在响应解析和超时处理。

典型的一次发信流程是:服务器连上先回 `220`,客户端发 `EHLO 域名`(服务器回 `250` 并列出支持的扩展,如 AUTH、STARTTLS、SIZE),接着认证、`MAIL FROM:`、`RCPT TO:`、`DATA`(服务器回 `354` 表示可以开始发正文)、正文、以单独一行 `.` 结束、`QUIT`。

响应解析有个细节必须处理:多行响应用 `250-`(连字符)续行、最后一行用 `250 `(空格)结束。只读一行就判断成功,会在多行 EHLO 响应上直接翻车。判断成败只看响应码前三位,比如 `2xx` 成功、`3xx` 需要继续输入、`4xx/5xx` 失败。

端口与加密:25、465、587 是三种不同姿势

结论:25 端口明文、465 端口是"连接即 TLS"、587 端口是"先明文握手再 STARTTLS 升级",三者代码路径不同,云服务器上 25 端口基本被封,优先用 465 或 587。

  • 465(隐式 TLS):地址写成 `ssl://smtp.example.com`,`stream_socket_client` 建连时直接完成 TLS 握手,最简单。
  • 587(STARTTLS):先连 `tcp://`,发 `EHLO`、发 `STARTTLS`,服务器回 `220` 后调用 `stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)` 升级,然后必须重新发一次 `EHLO`(升级后能力列表会变,很多新手漏掉这步导致认证失败)。

无论哪种,都要 `stream_set_timeout($fp, 10)` 设超时,否则对方不接受连接时 PHP 会一直挂着。用 `fsockopen` 时要手动判断 `ssl://` 前缀,否则加密根本没生效。

认证:AUTH LOGIN 最通用,注意 CRLF 注入

结论:国内邮箱绝大多数支持 `AUTH LOGIN`,实现成本最低;用户名密码要做 base64 编码,并且所有用户输入都必须过滤 CRLF。

`AUTH LOGIN` 的流程是固定的三步:客户端发 `AUTH LOGIN`,服务器回 `334` + "Username:" 的 base64,客户端发 base64 用户名,服务器再回 `334` + "Password:",客户端发 base64 密码,成功返回 `235`。部分服务器支持一条命令搞定的 `AUTH PLAIN`(`base64("\0用户名\0密码")`),可以作为降级方案。

安全上必须做的一件事:发件人、收件人、主题里任何来自用户的字符串,都要 `str_replace(["\r", "\n"], '', $value)`。否则用户可以在收件人里塞换行注入额外的 SMTP 命令,这是最典型的邮件头注入漏洞。

正文构造:MIME 头、编码与点转义

结论:邮件正文的坑八成集中在三个地方——换行符、中文编码、以点开头的行。

第一,所有协议行的结尾必须是 `\r\n`。不要用 `PHP_EOL`,在部分环境它不等于 CRLF,服务器会报语法错误。

第二,中文主题和发件人昵称要用 MIME 编码字,格式是 `=?UTF-8?B?` + base64 内容 + `?=`,例如 `=?UTF-8?B?5rWL6K+V6YKu5Lu2?=`。不编码就会变成乱码或被拒收。

第三,正文里如果某一行以 `.` 开头,必须补成 `..`(点填充,dot-stuffing),否则服务器会误认为邮件结束。

如果要发 HTML 邮件,用 `Content-Type: multipart/alternative; boundary=xxx` 同时带上纯文本和 HTML 两个部分;加附件再套一层 `multipart/mixed`,附件内容 base64 编码后每 76 个字符换行。boundary 用 `bin2hex(random_bytes(16))` 生成,保证不跟正文冲突。

健壮性:别只写通,要能排查

结论:一个能上生产的 SMTP 客户端,至少要具备逐条响应码校验、完整通讯日志、失败重试与发送频率限制。

每个命令发完都要校验响应码,不匹配就抛出带完整服务器原文的异常——线上出问题时,那句原文比任何猜测都有用。建议把整个对话过程(密码部分打码)记进日志,Clara BBS 后台就有独立的「邮件中心」,配置完 SMTP 后先用一封测试邮件跑通再投production。

另外两点容易被忽略:一是发送频率,短时间大量发信会被服务商限流甚至封号,注册验证码这类邮件要加队列或节流;二是送达率,真正决定邮件进不进收件箱的是发信域名的 SPF 和 DKIM 记录,而不是代码——代码写得再漂亮,域名没配好照样进垃圾箱。

总结一下:原生 SMTP 客户端的关键就四件事——正确区分 465 与 587 的加密路径、用 `\r\n` 并处理多行响应、把用户名密码和正文字段按 MIME 规范编码、对每条响应码做校验并留下日志。做到这四点,一个零依赖的 PHP 发信实现,稳定性和可维护性完全不输引入第三方库的方案。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-401.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~