Email Account 是什么
一句话总结
Email Account 是 Frappe 里存 SMTP 发信配置的数据库记录——每封邮件发出去之前,Frappe 都来查这张表:“用什么服务器、什么端口、要不要用户名密码”。没有它,邮件发不出去。
🤔 为什么会有它 / 解决了什么问题
Frappe 需要支持”一个站点用多个邮箱发信”的场景——比如系统通知用 noreply@,销售跟进用 sales@,技术支持用 support@。每个邮箱的 SMTP 服务器、端口、密码都不同。
如果这些配置直接写死在配置文件里,改一次就要重启整个服务。Email Account 把这个配置做成数据库记录,可以动态增删改,不用重启。
另外,common_site_config.json 里的 mail 字段只是 bench 级的默认值。Frappe 真正发信时不读它,而是读 Email Account。这个设计让 bench 管理员可以设一个全局默认 SMTP,同时允许单个站点覆盖成自己的邮箱。
🔍 具体原理 / 运作逻辑
想象你是一个邮递员(Frappe),你兜里有一张纸条(Email Account 记录),上面写着:
发信走东门 2525 号,不需要密码,报
noreply@getgrowth.app这个名字就行
每次有人叫你送信(frappe.sendmail()),你就掏出纸条看一眼,然后照着上面写的去送。
Frappe 代码里这行就是”掏纸条”的动作:
self.email_account_doc = frappe.get_cached_doc("Email Account", "Noreply")拿到这个记录后,读它的 smtp_server、smtp_port、login_id、密码,建立 SMTP 连接,把邮件发出去。
Email Account 记录的字段(点击展开)
字段 作用 我们的值 name唯一标识 "Noreply"(迁移站点默认名,非 “Frappe”)email_id发件人地址 noreply@getgrowth.appsmtp_serverSMTP 服务器地址 172.17.0.1(宿主机桥接)smtp_portSMTP 端口 2525no_smtp_authentication是否不需要 SMTP 认证 1(桥接走内网,认证在 CF API 层)default_outgoing是否默认发件账户 1enable_outgoing是否启用发信 1
🧭 什么情况下会用到 / 什么场景会遇到它
- 部署新站点时:必须确认 Email Account 存在且
smtp_server指向正确的桥接地址,否则邮件静默不发 - 邮件发不出去时:第一个要排查的就是 Email Account 的
smtp_server字段——很可能指向了127.0.0.1(旧桥接)而非172.17.0.1(宿主机桥接) - 切换邮件服务商时:改 Email Account 记录即可,不用改代码,不用重启
⚖️ 副作用 / 代价 / 取舍
- 每个站点必须独立维护:
common_site_config.json是 bench 级共享的,但 Email Account 是站点级数据库记录。新站点不会自动从 bench 配置继承,需要手动创建或脚本批量处理 - 名字不统一:迁移站点(如 gottenergy)的默认 Email Account 叫 “Noreply”,但 Frappe 代码里有些地方硬编码查找 “Frappe”。两个名字对不上就会
DoesNotExistError frappe.sendmail(now=True)路径不走回退:如果用now=True发信时 Email Account 不存在,直接报错,不会自动去读common_site_config.json
🕰️ 来历
- 谁创造的:Frappe 框架内置的 doctype(
frappe/email/doctype/email_account/) - 什么时候加入的:Frappe 早期版本就存在,作为邮件系统的核心配置单元。
common_site_config.json里的 mail 字段是更早期的全局配置方式,Email Account 是后来加入的多账户支持层 - 为什么名字是 “Noreply” 而非 “Frappe”:迁移站点在 frappecloud.io 上默认创建的是 “Noreply”,迁移后保留原名
🔗 关联 / 经常一起出现的概念
- Email Queue:Email Account 的”下游消费者”。Queue 里的每封邮件在发送时都会调用 Email Account 来获取 SMTP 配置
common_site_config.json:Email Account 的”备选方案”。但仅作为创建 Email Account 时的默认值参考,不是运行时回退frappe.sendmail():触发整个邮件链路的入口函数。它先创建 Email Queue 记录,Queue 发送时再读 Email Accountfrappe.email.queue.flush():手动触发队列发送的方法。测试邮件是否配通时,sendmail()→commit()→flush()三步比now=True更可靠- CF Email Bridge:我们部署在宿主机上的 SMTP-HTTP 桥接。Email Account 的
smtp_server指向它,它再把邮件转成 HTTPS 发给 Cloudflare
⚠️ 注意事项 / 常见误区
- ❌ 误区:配好
common_site_config.json就够了 → 实际上 Frappe 发信时不读它,必须存在 Email Account 记录 - ❌ 误区:Email Account 名字一定是 “Frappe” → 迁移站点默认叫 “Noreply”,直接
frappe.get_cached_doc("Email Account", "Frappe")会报错 - ❌ 误区:
frappe.sendmail(now=True)是最快的发送方式 → 实际上它的 after_commit hook 和手动flush()可能冲突,导致邮件静默丢失。三步法更可靠
🕳️ 踩坑案例
当时的现象:
frappe.sendmail(now=True) 报错 Password not found for Email Account Frappe password。后来 Email Account 配好了,日志也显示 200 OK,但把桥接从容器移到宿主机后,flush() 静默不发,桥接日志没有任何新连接。
排查过程:
- 先怀疑配置没生效 → 在 console 里打印
frappe.get_conf().get("mail_server"),显示172.17.0.1正确 - 再怀疑 flush 本身有问题 → 查 Email Queue 状态,全部 “Not Sent”,说明 flush 执行了但没发
- 直接
smtplib.SMTP('172.17.0.1', 2525)测试 → 通,桥接收到邮件 → 网络层面没问题 - 最后查到 Email Account “Noreply” 的
smtp_server仍是127.0.0.1(旧容器桥接地址)
最后发现:
Email Account 的名字是 “Noreply” 不是 “Frappe”,且 smtp_server 指向 127.0.0.1(旧桥接)。旧桥接停掉后,flush 连 127.0.0.1:2525 失败(Connection refused),但错误被 send() 的 try-except 静默吞掉了,队列状态甚至都没变成 “Error”。更新 Email Account 的 smtp_server 为 172.17.0.1 后立刻通了。
这件事之后我记住了什么:
- 邮件不通时,第一个查的永远是 Email Account 的
smtp_server字段 - 迁移站点的 Email Account 名字是 “Noreply”,不是 “Frappe”
common_site_config.json的 mail_server 改了不代表 Email Account 跟着变——两个独立存储,各管各的
📎 记忆锚点
Email Account = 邮递员兜里的纸条。每封邮件送之前,先掏出来看一眼地址。纸条写的是旧地址(127.0.0.1)→ 送到没人的地方 → 邮件丢了也没人告诉你。
📝 更新日志
| 日期 | 改动 | 原因 |
|---|---|---|
| 2026-07-06 | 首次创建 | CF Email 桥接部署过程中踩坑 Email Account 配置 |