邮件配置
这篇解决什么
业务模块要发邮件时,不要自己拼 SMTP。先在后台配好账号和模板,再调 MailSendApi。读完能改账号字段、模板占位符、发送链路和日志状态。
默认端口 48080,管理端前缀 /admin-api。菜单在系统管理 → 消息中心 → 邮箱管理。真正发信走 SMTP,落地用 Hutool MailUtil。
示意图:后台维护账号和模板;业务只调发送 API。真正连 SMTP 在异步线程里完成。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 邮箱账号 | SMTP 主机、端口、SSL / STARTTLS、授权密码 | ruoyi-office/yudao-module-system/.../dal/dataobject/mail/MailAccountDO.java |
| 邮件模板 | code、标题、HTML 正文、绑定账号 | .../dal/dataobject/mail/MailTemplateDO.java |
| 邮件日志 | 每次发送的收件人、渲染结果、状态 | .../dal/dataobject/mail/MailLogDO.java |
| 发送 API | 给管理员 / 会员各一条 | ruoyi-office/yudao-module-system-api/.../api/mail/MailSendApi.java |
| 后台页 | 账号、模板、记录三张列表 | ruoyi-office-vben/apps/web-antd/src/views/system/mail/ |
| 前端 API | /system/mail-account、mail-template、mail-log | ruoyi-office-vben/apps/web-antd/src/api/system/mail/ |
官方菜单 id:2130 邮箱管理,2131 邮箱账号,2136 邮件模版,2141 邮件记录。路由是 /system/messages/mail/mail-account、mail-template、mail-log。

本系统截图:系统管理 → 消息中心 → 邮箱管理 → 邮箱账号。

本系统截图:系统管理 → 消息中心 → 邮箱管理 → 邮件模板。

本系统截图:邮件记录。筛选是发送时间、用户、状态、账号、模板编号。
和 OA 办公邮箱不是同一套
OA 里的收件箱绑定 IMAP/SMTP,表是 oa_mail_account。这里只负责系统代发(验证码、告警、通知)。不要把办公邮箱账号填进 system_mail_account 当个人邮箱用。
表结构
三张表都带 @TenantIgnore,库里也没有 tenant_id。账号、模板、日志全实例共用。
按本仓库生成的 ER:模板必须挂一个账号;日志同时冗余账号和模板快照。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
system_mail_account | @TableName("system_mail_account")。密码明文存 | .../dal/dataobject/mail/MailAccountDO.java |
system_mail_template | @TableName("system_mail_template")。code 全局唯一 | .../dal/dataobject/mail/MailTemplateDO.java |
system_mail_log | @TableName("system_mail_log")。to_mails / cc_mails / bcc_mails 是地址列表 | .../dal/dataobject/mail/MailLogDO.java |
MailTemplateDO.params 不是手填。保存时用正则 \{(.*?)\} 扫标题和正文,去重后写入。发送时按这个列表校验,缺 key 抛 MAIL_SEND_TEMPLATE_PARAM_MISS。
密码按明文落库
password 没有加密字段。库备份、导出、越权查账号接口都能看到 SMTP 授权码。账号页权限只给运维,不要把授权码写进文档或日志。
实现原理
其它模块只依赖 yudao-module-system-api,注入 MailSendApi。单体里 MailSendApiImpl 就是本地 Bean;微服务里走 Feign,服务名 system-server。
发送先同步落日志,再异步连 SMTP。接口马上返回日志 id,不堵业务线程。
示意图:MailProducer 发的是 Spring 事件,不是 RocketMQ。MailSendConsumer 上的 @Async 才把 SMTP 挪到别的线程。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 管理端测试 | POST /system/mail-template/send-mail | .../controller/admin/mail/MailTemplateController.java |
| RPC | /rpc-api/system/mail/send/send-single-admin、send-single-member | .../api/mail/MailSendApi.java |
| 同步编排 | 校验模板 / 账号 / 参数,拼收件人 | .../service/mail/MailSendServiceImpl.java |
| 异步投递 | applicationContext.publishEvent | .../mq/producer/mail/MailProducer.java |
| 真正发信 | @EventListener + @Async | .../mq/consumer/mail/MailSendConsumer.java |
| SMTP | org.dromara.hutool.extra.mail.MailUtil,isHtml = true | MailSendServiceImpl#doSendMail |
| 账号缓存 | Redis mail_account:{id} | RedisKeyConstants.MAIL_ACCOUNT |
| 模板缓存 | Redis mail_template:{code} | RedisKeyConstants.MAIL_TEMPLATE |
| 统一消息邮件渠道 | 场景绑定后仍进 MailSendService | .../service/msg/channel/MailChannelAdapter.java |
发件人显示名来自模板 nickname:有值时 from 写成 昵称 <mail@example.com>,没有就只用账号邮箱。
模板关掉也不会报错
status 不是启用时,只写日志,状态 IGNORE = 30,不发事件。产品改文案或临时停发,不用改调用方、不用重启。
邮箱账号
菜单:系统管理 → 消息中心 → 邮箱管理 → 邮箱账号。接口 @RequestMapping("/system/mail-account")。

本系统截图:新增邮箱账号。SSL 默认开,STARTTLS 默认关。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 列表 / 新增 | GET /page,POST /create | MailAccountController |
| 精简列表 | 模板下拉用 GET /simple-list | 同上 |
| 权限 | system:mail-account:create 等 | 同上 |
| 请求体 | mail / username / password / host / port / sslEnable / starttlsEnable | .../vo/account/MailAccountSaveReqVO.java |
mail 是对外显示的发件地址,username 是 SMTP 登录名,多数邮箱两者相同。password 填服务商 SMTP 授权码,不是网页登录密码。
常见组合:465 + SSL;587 + STARTTLS。两个都开或端口和开关对不上,Hutool 会连不上。
有模板挂着的账号删不掉,错误码 MAIL_ACCOUNT_RELATE_TEMPLATE_EXISTS。
本地先验证 SMTP 是否通,可以用 Hutool 直发一封(不要把授权码提交进仓库):
MailAccount account = new MailAccount()
.setFrom("noreply@example.com")
.setHost("smtp.example.com").setPort(465).setSslEnable(true)
.setAuth(true).setUser("noreply@example.com").setPass("your-auth-code");
MailUtil.send(account, "you@example.com", "连通性测试", "正文", false);类在 org.dromara.hutool.extra.mail。后台保存成功不代表 SMTP 已通,要用模板「测试」或上面这段确认。
邮件模板
菜单:邮件模板。接口 @RequestMapping("/system/mail-template")。

本系统截图:新增邮件模板。正文是富文本,发送时按 HTML 发出。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 邮箱账号 | 下拉来自账号精简列表 | accountId → MailAccountDO.id |
| 模板编码 | API 只认这个 code,不是主键 | MailTemplateDO.code |
| 发送人名称 | 可选,写入 SMTP From 显示名 | nickname |
| 模板标题 | 支持 {name} 这类占位符 | title |
| 模板内容 | 富文本;占位符同样是 {var} | content |
| 开启状态 | CommonStatusEnum,关则只记日志 | status |
code 必须唯一,重复抛 MAIL_TEMPLATE_CODE_EXISTS。改 code 或内容后会清空 mail_template 整组缓存。
占位符用 Hutool StrUtil.format。标题和正文里出现的 {key} 都会进 params。调用方 templateParams 必须带齐这些 key,值不能是 null。
渲染后还会做一次 HTML 反转义,并把最外层 <pre> 换成 <div>,避免富文本编辑器带出来的标签把邮件撑坏。
为什么要模板
标题、正文、甚至发件账号经常被产品改。改模板行就行,调用方继续传同一个 templateCode。
邮件日志
有模板后,行上点「测试」。权限 system:mail-template:send-mail。弹窗在 template/modules/send-form.vue。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 收件邮箱 | 必填,标签输入,Enter 添加 | MailTemplateSendReqVO.toMails |
| 抄送 / 密送 | 可选 | ccMails / bccMails |
| 模板参数 | 按 params 补齐,不能为 null | templateParams |
| 接口 | POST /system/mail-template/send-mail | MailTemplateController#sendMail |
当前登录人会作为 userId 再走管理员发送。测完先看自己的收件箱,再回邮件记录对状态。收件箱不是本系统页面。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 列表 | GET /system/mail-log/page | .../controller/admin/mail/MailLogController.java |
| 详情 | GET /get?id= | 同上 |
INIT | 0,已入队,还没走到 SMTP | MailSendStatusEnum |
SUCCESS | 10,写下 SMTP messageId | 同上 |
FAILURE | 20,sendException 是根因短句 | 同上 |
IGNORE | 30,模板关闭,故意不发 | 同上 |
日志里的 templateTitle 存的是模板原文,templateContent 是已经替换参数后的 HTML。fromMail、templateCode 是发送当时的冗余,账号后来改了也能回看。
system_mail_log.template_params 列只有 varchar(255)。参数很多或很长时可能被库截断,排查以当时能否发出为准。
邮件发送
MailSendApi
| 名称 | 说明 | 仓库路径 |
|---|---|---|
sendSingleMailToAdmin | userType = 2。userId 有值就追加 AdminUserDO.email | MailSendApi / MailSendService |
sendSingleMailToMember | userType = 1。邮箱来自 MemberService.getMemberUserEmail | 同上 |
| 请求体 | userId、toMails / ccMails / bccMails、templateCode、templateParams、attachments | .../api/mail/dto/MailSendSingleToUserReqDTO.java |
| 返回 | 日志 id,CommonResult<Long> | MailSendApiImpl |
userId 可空。为空时必须自己传合法 toMails。用户邮箱和 toMails 会去重合并;抄送、密送同样只保留能通过邮箱校验的地址。
合并后收件人为空,抛 MAIL_SEND_MAIL_NOT_EXISTS(文案是「邮箱不存在」)。管理员没填邮箱、会员没有邮箱、又没传 toMails,都会走到这里。
@Resource
private MailSendApi mailSendApi;
public void sendDemo() {
Map<String, Object> templateParams = new HashMap<>();
templateParams.put("name", "张三");
templateParams.put("code", "847291");
mailSendApi.sendSingleMailToAdmin(new MailSendSingleToUserReqDTO()
.setUserId(1L)
.setTemplateCode("test_01")
.setTemplateParams(templateParams));
}微服务模块的 pom.xml 要依赖 yudao-module-system-api,并在 RPC 配置里声明 MailSendApi。单体 yudao-server 已经在同一进程,直接 @Resource 即可。
现成调用在 IoT 告警:场景动作按接收方式选短信 / 邮件 / 站内信,邮件分支就是 sendSingleMailToAdmin。
路径:ruoyi-office/yudao-module-iot/.../rule/scene/action/IotAlertTriggerSceneRuleAction.java。
管理员邮箱为空不会自动改发短信
getUserMail 只读用户资料上的邮箱。没填就只会用你传入的 toMails。不要假设「有 userId 就一定能发出去」。
SMTP 附录
授权码、安全开关以各服务商控制台为准。下面只列常见默认值。
| 名称 | 适用 | 限制 |
|---|---|---|
| QQ 邮箱 | smtp.qq.com,常用 465 + SSL | 先开 SMTP,密码填授权码。见 QQ 邮箱帮助 |
| 网易 163 | smtp.163.com,常用 465 + SSL | 同上。见 163 邮箱帮助 |
| 腾讯企业邮 | smtp.exmail.qq.com,常用 465 + SSL | 企业管理员要先开通客户端授权。见 企业邮帮助 |
Hutool 发送参数见 MailUtil。本系统调用时 HTML 模式为 true,和后台富文本一致。
配置与操作
管理员先在 消息中心 → 邮箱账号 填 SMTP 主机、端口、账号密码,再在模板里绑账号、改标题和 HTML。保存后下一次 MailSendApi 就用新账号。
发信记录只读。改密码立刻生效,不必重启。开启见 系统功能设计。截图见上文账号和模板。
