# LcMailer **Repository Path**: lczld/LcMailer ## Basic Information - **Project Name**: LcMailer - **Description**: LcMailer —— Typecho SMTP 邮件适配插件 - **Primary Language**: Unknown - **License**: GPL-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-14 - **Last Updated**: 2026-10-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: Typecho, 邮件 ## README # LcMailer —— Typecho SMTP 邮件适配插件 为 Typecho 站点补上「能真正发出去的邮件」能力。和授权码管理系统(LcLicense)分开打包、互不相干: LcMailer 只负责发信,LcLicense 只负责授权。 ## 一、为什么要单独一份 Typecho 内核没有内置 `SendMail` 类。LcLicense 发邮件时的逻辑是: ```php if (class_exists('SendMail')) { $sendmail = new SendMail(); return (bool)$sendmail->send($to_email, $subject, $content); } // 检测不到就回退 PHP 内置 mail() return @mail($to_email, $subject, $content, $headers); ``` 而 PHP 内置 `mail()` 依赖服务器的 sendmail/postfix,虚拟主机和 Docker 环境下几乎都不通 —— 结果就是「手动发放授权码」页面上显示`邮件发送失败`或干脆没有回执。 装上 LcMailer 后 `SendMail` 类存在,上面这段会走进 SMTP 分支,邮件真正发出去。 不装也不影响任何功能,只是退回 `mail()`。 **改动范围**:完全独立的新插件,**没有修改 LcLicense 的任何一行代码**。 ## 二、安装 1. 把 `LcMailer` 目录上传到 `usr/plugins/LcMailer/` 2. 后台 → 插件 → 启用 **LcMailer** 3. 后台 → 插件 → LcMailer 的**设置**,填写 SMTP 参数保存 4. 后台 → **邮件 → 邮件中心**,点一次「测试 SMTP 连接」 环境要求:PHP 7.4+,`openssl` 扩展(SSL / STARTTLS 需要)。缺项会在启用提示里直接告诉你。 ## 三、SMTP 参数 在 Typecho 官方「插件设置」里填写,全部走 Typecho 原生 Form 元素,和其他插件的设置页观感一致。 | 参数 | 说明 | | --- | --- | | 邮件发送开关 | 停用后所有邮件不投递、也不报错 | | SMTP 服务器地址 | 如 `smtp.qq.com` | | SMTP 端口 | SSL 用 465,STARTTLS 用 587 | | 加密方式 | SSL / STARTTLS / 不加密,**必须和端口配套** | | SMTP 账号 | 通常是完整邮箱地址 | | SMTP 密码 / 授权码 | QQ、163 等需在邮箱设置里生成**授权码**,不是登录密码 | | 发件邮箱 | 多数服务商要求与 SMTP 账号一致 | | 发件人名称 | 留空则只显示邮箱 | | 连接超时 | 默认 15 秒,最小按 3 秒处理 | | SSL 证书校验 | 服务器缺 CA 根证书导致连不上时,选「不校验」 | | 发送日志 | 写入 `usr/plugins/LcMailer/cache/mail.log`,**不含正文** | 常用组合: | 服务商 | 地址 | 端口 / 加密 | | --- | --- | --- | | QQ 邮箱 | smtp.qq.com | 465 / SSL | | 163 邮箱 | smtp.163.com | 465 / SSL | | 腾讯企业邮 | smtp.exmail.qq.com | 465 / SSL | | Gmail | smtp.gmail.com | 587 / STARTTLS | | Outlook | smtp.office365.com | 587 / STARTTLS | ## 四、邮件中心面板 启用后菜单里会多出「邮件 → 邮件中心」,做四件事: 1. **运行状态自检** —— 逐项列出网络组件、openssl、`SendMail` 是否加载、SMTP 配置缺什么, 以及两个协作插件(LcLicense、LcReplyMailer)的**三态**:未安装 / 已安装但未启用 / 已启用 2. **SMTP 连接测试** —— 只验证连通性和账号认证,不投递邮件,并打印完整 SMTP 会话日志 3. **发送测试邮件** —— 填一个收件箱发一封 HTML 邮件 4. **发送日志** —— tail 最近 30 条摘要,可一键清空 排版沿用 Typecho 官方 `admin/css/style.css` + `admin/css/grid.css`, 配 `col-tb-9` 主栏 / `col-tb-3` 侧栏双栏结构,只在 `assets/css/admin.css` 里补了官方没有的小组件。 上面 2、3、4 三个操作以及「检查更新」都是 POST,v1.1.8 起一律 **302 跳回面板的 GET 地址** 再显示结果(PRG),刷新 / 后退不会再触发浏览器的「确认重新提交表单」。 移动端:与 LcLicense 同策略 —— 官方负责双栏堆叠(`col-tb-*` → `col-mb-12`),本插件只补留白、日志区限高与键值表字号,共 2 个断点 8 条规则。 ## 五、和「手动发放授权码」的配合 在 **授权管理 → 授权码管理** 里手动发放时,表单上的**客户邮箱**填了且格式合法,就会触发发信: ``` 手动发放成功!授权码:LCBK-XXXX-XXXX-XXXX,邮件已发送至 buyer@example.com ``` 邮件正文使用 LcLicense 自带的模板(可在「基础配置」里自定义标题模板与正文模板), 可用变量:`{{blogname}}` `{{blogurl}}` `{{license_code}}` `{{app_name}}` `{{app_type}}` `{{order_id}}` `{{date}}` `{{license_page_url}}`。 没填邮箱或邮箱不合法时不发信,也不报错。 ## 六、与 LcReplyMailer(评论回复邮件通知)的配合 两个插件是**分工关系**,缺一不可: | | 负责什么 | | --- | --- | | LcReplyMailer | 什么时候发、发给谁、邮件正文长什么样 | | LcMailer | 真正把信投出去(SMTP 连接、认证、编码) | LcReplyMailer 完全不碰 SMTP,只调用 `SendMail`;所以只要本插件的 SMTP 没配好, 评论通知就会静默失败 —— 这也是面板上要把它俩的状态放一起看的原因。 面板「运行状态自检」里的这一行会显示三种状态之一: ``` LcReplyMailer 评论回复通知插件 未安装(装了才能在有新评论 / 回复时自动发邮件通知) LcReplyMailer 评论回复通知插件 已安装但未启用(启用后评论回复通知才会发出) LcReplyMailer 评论回复通知插件 已启用,评论回复通知由本插件投递 ``` - **未安装**:不影响本插件独立使用 - **已安装但未启用**:装上了但没启用,一条通知都不会发 - **已启用**:正常情况下状态点为绿。通知总开关关掉、或一个通知对象都没勾时转黄并说明 原因;本插件 SMTP 没配好时还会追加一句「SMTP 配置未完成,邮件暂不会发出」。 三态由 `Plugin::peerState()` 判定(插件目录里有 `Plugin.php` = 已安装, 出现在 `options.plugins.activated` 里 = 已启用),版本号从对方 `Plugin.php` 头部注释的 `@version` 读出,读不到就不显示。 ## 七、给其它插件用 任何 Typecho 插件都可以直接调用: ```php // 推荐:不用自己拼路径 require,也不用猜全局类有没有加载 $ok = \TypechoPlugin\LcMailer\Plugin::mailer()->send('to@example.com', '标题', '
正文
'); // 也可以直接用全局类(需保证本插件已启用,SendMail 已由早期钩子载入) $mail = new SendMail(); $mail->send('to@example.com', '标题', '正文,支持 HTML
'); ``` 进阶用法: ```php $mail = new SendMail(); $mail->send(['a@x.com', 'b@x.com'], '标题', '正文
', [ 'cc' => 'cc@x.com', 'bcc' => 'bcc@x.com', 'replyTo' => 'reply@x.com', 'fromName' => '临时换个发件名', 'fromMail' => 'other@x.com', 'isHtml' => false, // 纯文本 ]); $mail->lastError(); // 失败原因 $mail->log(); // 完整 SMTP 会话 $mail->readiness(); // ['missing'=>[], 'warnings'=>[], 'ready'=>true] $mail->test(); // 只测连接,不投递 ``` 多个收件人会被逐个 `RCPT TO`,非法邮箱自动丢弃;中文标题按 RFC 2047 做 Base64 编码; 正文统一 `Content-Transfer-Encoding: base64`,避免长行被服务器截断或拆错行。 探测其它协作插件的状态(写面板 / 做依赖检查时很有用): ```php use TypechoPlugin\LcMailer\Plugin; Plugin::peerState('LcReplyMailer'); // ['installed' => true, 'active' => true, 'version' => '1.1.1', 'dir' => '/…/usr/plugins/LcReplyMailer/'] Plugin::peerState('LcLicense'); // 同理 Plugin::peerVersion('/…/Plugin.php'); // 只读头部 2KB,读不到返回 '' ``` 任一步取不到都按「未安装」处理,不会因为状态探测让后台报错。 ## 八、在线更新 插件自己查版本、自己升级,**不用手动下载上传**。 ### 怎么用 面板顶部弹出「发现新版本」→ 进「在线更新」页签 → 点「立即更新」→ 等几十秒, 自动回到面板并显示结果(成功 / 失败原因都在横幅里)。 ### 它按顺序做了什么 1. **查版本**:读仓库 main 分支上的 `Plugin.php` 源码文件取版本号——静态地址, 不走接口、不吃 API 配额;读不到才退回 Gitee 接口。探测结果缓存 6 小时, 点「检查更新」可强制刷新。 2. **下载 Release 附件**:和你在 Gitee 上手动下载的是**同一个文件**。能解 zip 就用 `.zip`,没有 `ZipArchive` 就下 `.tar.gz` 用纯 PHP 解(只需 zlib),共享主机照样能更。 3. **校验 → 备份 → 换装 → 再校验**:包内版本对不上就当场取消(此时还没动线上文件); 换装后复核 `Plugin.php` 的 `@version`,对不上自动回滚并**验证插件确实回来了**。 4. **留结果**:写入 `cache/last-update.json`,面板横幅展示,失败会写明原因。 ### 为什么敢让它动线上目录 | 保障 | 说明 | | --- | --- | | 先备份 | `.lcm-backups/LcMailer-<时间戳>-v<旧版本>/`,面板里列得出来;回退 = 把目录整体移回 `usr/plugins/LcMailer/` | | 失败自动回滚 | 回滚成功会验证 `Plugin.php` 回来了;万一没成功,提示直接给出备份路径 | | 互斥锁 | 同一时刻只允许一个更新任务;锁超 30 分钟自动作废,不会把站点卡死 | | 保留 7 天 | 旧备份 7 天后随下一次更新自动清理 | | 配置不丢 | 配置在数据库,日志随旧目录迁到新版本 | ### 服务器要求 | 需要 | 说明 | | --- | --- | | `curl` 或 `allow_url_fopen` | 二选一,用于下载 | | `ZipArchive` 或 `zlib` | 二选一,用于解压;都没有时只能手动覆盖 | | `usr/plugins/` 可写 | 更新要写入文件、搬走旧目录 | | CA 证书包(可选) | 没配也能用,插件会自动降级为不校验证书重试一次 | 条件不满足时,面板会**逐条列出原因**并隐藏「立即更新」,同时给出 [Release 页面](https://gitee.com/lczld/LcMailer/releases) 的手动下载链接。 ## 九、常见问题 | 现象 | 原因 | 怎么办 | | --- | --- | --- | | SMTP 超时 / 连不上 | 端口与加密方式没对上(465↔SSL、587↔STARTTLS) | 对齐后再试 | | 报证书校验失败 | 服务器没配 CA 根证书 | 面板里改「不校验」;或在 `php.ini` 设 `openssl.cainfo` | | `535 Authentication failed` | 没开 SMTP,或填了登录密码 | 填**授权码**,不是密码 | | 日志成功但对方没收到 | 发件邮箱与 SMTP 账号不一致,或被判垃圾邮件 | 「发件邮箱」留空自动同账号;标题别太营销 | | 查更新说「无法连接 Gitee」 | 服务器不能出网 / 没 CA 证书 / 网络抖动 | 直接去 Release 页下载覆盖(和在线更新同一个包) | | 提示「安装包还没上传」 | main 分支版本号比发布更早,附件还没传 | 等发布完成再点 | | 点「立即更新」白屏 / 转圈很久 | 主机把这次耗时响应丢掉了(1panel 等) | 更新其实通常已跑完,刷新面板看横幅;结果不会丢 | | **更新后版本号看着没变** | 服务器字节码缓存(OPcache)没认出新文件 | v1.2.2 起插件自动处理;仍遇到就重启 PHP,或执行 `find usr/plugins/LcMailer -type f -exec touch {} +` | | 「已有另一个更新任务正在进行」 | 互斥锁在起作用,或上次中断留下未超时锁 | 等上一步完成(30 分钟后自动放行),别重复点击 | | 更新失败 / 回滚未成功 | 目录不可写、被安全软件拦截等 | 按提示把 `.lcm-backups/` 里对应目录整体移回 `usr/plugins/LcMailer/` | | 刷新时弹「确认重新提交表单」 | v1.1.7 及更早的旧行为 | 升级到 v1.1.8+ 即消失 | > v1.1.2 及更早版本的在线更新会必然失败,请先手动下载覆盖升级到新版,再用在线更新。 ## 十、自测情况 真实 Typecho 1.3.0 + SQLite 上验证;SMTP 用本地假服务器离线模拟, 更新链路含真实联网与本地假 HTTP 服务。 | 范围 | 结果 | | --- | --- | | SMTP 协议(无认证 6/6、AUTH LOGIN 6/6、AUTH PLAIN 6/6) | 通过;535 失败与 STARTTLS 分支均正确处理 | | 多收件人 + 中文标题 + 中文发件名 | 通过,5/5 | | 单元测试 `test/test_updater.php` | **129/129** | | 在线更新全流程(真实联网:下载 → 校验 → 备份 → 换装 → 复核 → 清理) | 通过,9/9 | | 更新窗口期守卫 / 加固回滚 / 残留清理 | 通过,78/78 | | 更新互斥锁 / 归档类型识别 / 附件就绪校验 | 通过,8/8、5/5、附件缺失正确拦截 | | 版本探测(三级通道 + 防 CDN 缓存滞后) | 通过,5/5 | | mail.log 轮转 / 更新结果持久化 / 成功横幅只显示一次 | 通过,31/31 | | PRG 端到端(真实 HTTP:POST → 302 → 刷新不重复执行) | 通过,16/16 | | 后台面板渲染(13 个预设:页签、提示条、更新结果、备份卡片、缓存陈旧提示…) | 全部通过,零失败断言 | | 发布脚本创建 Release + 上传附件(本地假 API) | 通过,附件内容与源文件 sha256 一致 | | `php -l` 全部文件 | 通过 | ## 十一、文件清单 ``` LcMailer/ ├── Plugin.php 插件主体:菜单面板注册、设置表单、早期钩子 ├── panel.php 邮件中心面板(自检 / 连接测试 / 发测试信 / 日志 / 在线更新) ├── lib/SendMail.php SMTP 客户端:SSL / STARTTLS、AUTH LOGIN / PLAIN ├── lib/Updater.php 在线更新:查版本(源码文件优先)、下 Release 附件、备份 / 安装 / 清理 ├── assets/css/admin.css 仅补充官方未提供的组件 └── assets/fonts/ Remix Icon 字体(Apache-2.0,仅保留 woff2) ```