协同办公

Odoo 邮件网关为什么要先分“新建还是续帖”:message_route、catchall、alias 与 Chatter 边界讲透

很多团队把邮件进 Odoo 理解成“收到邮件就创建记录”。但 mail.thread 源码真正先做的是分流:这封邮件究竟是在回复既有线程、命中新 alias、转发到别的模型,还是该直接 bounce。

协同办公
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

很多人理解邮件网关时,会把问题说得很简单:

  • 发到 Odoo 的邮箱
  • 系统创建一条记录
  • 之后邮件继续挂在上面

addons/mail/models/mail_thread.py 里的 message_route() 告诉我们, Odoo 真正先解决的不是“怎么创建记录”,而是:

这封邮件到底属于继续一条已有协作线程,还是应该开启一条新线程;如果两者都不成立,它甚至应该被挡在系统外。

所以邮件网关的核心不是“收邮件”,而是“判边界”。

边界判对了,邮件协作才顺;边界判错了,Chatter 会变得又脏又乱。


一、message_route() 为什么是真正的入口

很多人更熟悉 message_new()message_update(), 因为它们看起来像“创建记录”和“更新记录”。

但从整体链路看,真正的总开关是 message_route()

它先做的是路由判断:

  1. 这是不是对已有 mail.message.message_id 的回复
  2. 收件地址有没有命中 mail.alias
  3. 如果没有,能不能走 fallback model
  4. 如果还不行,而且又触到了 catchall / 不可路由地址,是否该 bounce

所以 message_new()message_update() 其实都只是 路由结果

它们不是第一判断。


二、为什么“回复旧线程”优先级这么高

message_route() 一上来就会根据:

  • References
  • In-Reply-To

去找历史 mail.message

一旦能找到,它就倾向把这封邮件视为:

  • 回复一条已有协作上下文

这很合理。

因为邮件协作最怕的事之一,就是同一条对话被系统误切成两条记录。

如果客户明明是在回原来的讨论, 系统却因为主题改了、收件人多了、正文格式不同了,就重新建档, 团队就会马上失去完整上下文。

所以 Odoo 先尊重 thread continuity, 再考虑要不要新建对象。


三、为什么转发到别的 alias 时,原回复关系会被打断

这段源码特别值得注意。

如果系统发现:

  • 这封邮件看上去像是在回复旧消息
  • 但收件地址命中了 另一个模型 的 alias

它会把这件事当成 forward,而不是 reply

也就是说,旧的 reply 语义会被取消, 路由重新回到 alias 创建链路。

这背后是一个非常聪明的边界判断:

  • 回复旧线程,说明你想继续原协作对象
  • 但如果你把邮件转发到了另一个业务入口,说明你要让邮件改投别的业务语义

比如:

  • 原来在任务线程里讨论
  • 现在把邮件转给销售线索 alias

系统若还执着于“它在回复旧 Message-Id”, 反而会把新业务入口压扁掉。

所以 Odoo 选择:

一旦目标 alias 指向不同模型,就优先尊重新的业务入口,而不是旧的回复血缘。


四、alias 为什么不只是“邮箱名映射模型”

message_route() 找到 alias 后, 它返回的 route 不只是模型名,还包括:

  • alias_force_thread_id
  • alias_defaults
  • user_id
  • alias 本身

这说明 alias 在 Odoo 里承担的是完整入口配置:

  • 要落到哪个模型
  • 是新建还是强制挂到固定线程
  • 新建时要带哪些默认值
  • 网关使用哪个用户上下文去处理

所以 alias 不是“收件地址的翻译表”, 而更像“业务协作入口定义”。

这也是为什么邮件协同做得成熟的团队,会认真设计 alias,而不是只把它当技术配置。


五、为什么 catchall 不能被直接当业务入口

message_route() 对 catchall 的态度很明确:

  • 如果邮件直接写到 catchall,可能直接 bounce
  • 如果邮件写到 catchall 加其他不可路由地址,也可能 bounce

很多人第一次看到这里,会觉得系统太严格。

但这其实是在保护协作边界。

catchall 的意义通常是:

  • 承接 reply-to
  • 兜底接住已有对话的回信

它不是让所有未知新邮件都无脑冲进系统。

如果把 catchall 当万能入口,就会出现:

  • 大量无法归属的邮件进入系统
  • 垃圾对话污染 Chatter
  • 协作对象和消息线程混乱绑定

所以 Odoo 宁可 bounce, 也不愿意让“无归属新邮件”轻易混进业务线程。


六、message_new()message_update() 的分工边界

当路由判定完成后,后续就清楚了:

  • 已有线程 → message_update()
  • 新线程 → message_new()

这两个方法表面很简单, 但语义边界非常重要。

message_new() 负责的是

  • 这封邮件应该生成一个新的业务对象
  • 需要用主题、发件人、alias 默认值等初始化记录

message_update() 负责的是

  • 这封邮件属于已有对象
  • 只是在这个对象上继续协作

也就是说, 创建与续帖不是“数据写法不同”, 而是两种完全不同的协作判断。


七、为什么 _message_route_process() 还要再做一层控制

_message_route_process() 里还有几个很值得注意的动作:

  • 处理新建 / 更新失败时,alias 可能被标记为 invalid 并反弹错误邮件
  • 进入 message_post() 前会区分内部 note 与外部 comment
  • 会延迟写入部分 partner_ids,避免重复通知
  • 若是新建线程,还会使用 creation subtype

这说明 mail gateway 不是“路由完就结束”。

Odoo 还要继续保证:

  • 发帖语义正确
  • 通知不要重复
  • 失败可以回弹给入口配置
  • 外部邮件不要轻易把系统用户污染成关注者或作者关系

这整套处理都在围绕同一个目标:

让邮件协作像继续一条业务对话,而不是像往数据库塞一段文本。


八、邮件网关和 Chatter 的真正边界是什么

很多实施里最常见的误区,是把邮件网关与 Chatter 的关系理解成:

  • 邮件只是 Chatter 的另一种输入法

这只说对了一半。

更准确的理解应该是:

  • Chatter 是业务对象上的协作线程
  • 邮件网关是把外部邮件翻译进这个线程体系的边界层

它要先决定:

  • 外部邮件是否配得上进入某条业务线程
  • 该进入旧线程还是新线程
  • 若没有清楚归属,是否干脆拒绝

所以 gateway 的价值不是“导入内容”, 而是“守住业务线程的边界完整性”。


九、实战里最容易踩的坑

1. 以为 alias 命中就一定新建记录

其实回复旧线程优先级更高;而命中不同模型 alias 时又可能反转成 forward 语义。

2. 把 catchall 当万能收件箱

这样最容易把协作边界冲烂。

3. 不理解 message_new() / message_update() 是路由结果,不是入口判断

后续定制就容易写反。

4. 只看邮件主题,不看 References / In-Reply-To

很容易误判为什么系统挂到旧线程上。

5. 遇到邮件“没进系统”就当 bug

有时是 Odoo 在主动保护线程边界。


一句话记忆法

Odoo 邮件网关首先不是在回答“要不要建记录”,而是在回答“这封邮件属于继续哪条协作线程、还是该新开入口、还是根本不该进来”。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。