框架深潜

Odoo 收件 alias 真正难的不是命中模型,而是命中之后如何处理 bounce:incoming alias、退信识别、重置与线程恢复边界讲透

很多团队以为 incoming alias 的工作在 route 成功那一刻就结束了,但 mail_thread 源码显示,真正成熟的邮件网关还要继续做 bounce 识别、被退回对象归因、message_bounce 重置,以及对 catchall/loop 的二次保护。也就是说,收件入口和退信恢复其实是同一套边界治理。

框架
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

很多人理解 Odoo 邮件网关时,会把重心放在:

  • 这封邮件命中了哪个 alias;
  • 是不是创建了记录;
  • 是不是挂回原线程。

这些当然重要,但从 /home/ubuntu/odoo-temp/addons/mail/models/mail_thread.py 看,真正成熟的邮件入口还要继续处理另一半问题:

如果这封邮件是退信怎么办?如果之前退信过、现在又收到了对方正常来信怎么办?如果有人直接写到 catchall,或者回复了一封 bounce 通知,又该怎么避免线程被污染?

也就是说,incoming alias 不是“命中路由就结束”,而是和 bounce 处理、线程恢复、反循环保护共同组成一套边界治理。

message_route() 开头就先判断 bounce,不是偶然

message_route() 里,官方的处理顺序非常值得注意。

在真正进入 reply / alias / fallback 路由前,源码先做:

  • 如果 message_dict.get('is_bounce'),就 _routing_handle_bounce() 然后直接 return []
  • 否则继续 _routing_reset_bounce()

这说明 Odoo 的思路非常明确:

bounce 不是普通来信的一个附带状态

它是要 抢先分流 的特殊类型。

因为退信邮件的目标,不是进入业务线程继续协作,而是:

  • 更新送达状态;
  • 找到被退回的对象;
  • 修正联系人的 bounce 计数;
  • 必要时阻止后续错误路由。

如果把 bounce 当普通来信处理,很快就会出现:

  • 退信正文被挂进 chatter;
  • 业务对象被错误更新;
  • catchall 线程被退信风暴污染。

_detect_is_bounce() 说明 bounce 识别本身就是网关能力

源码对 bounce 的识别,并不只是看正文里有没有“Undelivered Mail”。

它会结合:

  • 收件地址是否命中 bounce_alias@domain
  • 头信息与邮件结构
  • 原始 message payload

这说明在 Odoo 里,退信并不是“SMTP 层已经处理完的东西”,而是应用层仍然要理解的事件。

为什么这点重要

因为业务系统真正关心的不是“技术上收到了一个退信包”,而是:

  • 哪个联系人邮箱不可达;
  • 哪次通知失败了;
  • 哪条业务记录应该被标记有送达问题;
  • 后续如果对方恢复正常,是否该清掉旧的失败状态。

这已经超出了纯粹 SMTP 配置范畴。

_routing_handle_bounce() 真正在做“失败归因”

这段代码非常值得仔细看。

它会尝试拿到:

  • bounced_email
  • bounced_partner
  • bounced_msg_ids
  • bounced_message

然后分几条路径传播退信信息:

  1. 如果能定位到原始业务对象,就在对应记录上触发 _message_receive_bounce()
  2. 如果某些带黑名单 / bounce 逻辑的模型能按 email_normalized 找到记录,也会逐个传播;
  3. 如果能定位到原始 mail.message,还会把相关 notification 标记成 bounce / mail_bounce

这说明 bounce 处理的关键不是“告诉用户失败了”,而是:

把一次技术失败准确归因到联系人、通知和业务对象上。

如果归因不到位,退信就只是日志; 归因准确,它才会变成可治理的业务信号。

_routing_reset_bounce() 说明 Odoo 把“恢复送达”也当成一等公民

这段逻辑很容易被忽略,但其实特别成熟。

在非 bounce 来信进入时,Odoo 会根据发件人地址:

  • 找到那些 message_bounce > 0email_normalized 相同的记录;
  • _message_reset_bounce(normalized_from)

这背后的设计思想非常好理解:

  • 如果这个邮箱现在都能主动给我发邮件了;
  • 那么之前积累的“它不可达”状态,就不该继续无脑维持。

也就是说,Odoo 不把 bounce 当永久污点,而把它当 可恢复的送达信号

这对真实运营很重要。

否则业务会越来越偏:

  • 对方邮箱明明恢复了;
  • 系统还一直认为它失联;
  • 销售、招聘、项目协作的触达判断都会被旧状态拖偏。

catchall 不是收件垃圾桶,而是线程兜底入口

message_route() 对 catchall 的态度一直很强硬。

如果发现:

  • 直接写到 catchall;
  • 或 catchall 混着其他不可路由地址一起写;

源码可能直接调用 _routing_create_bounce_email() 回弹,并 return []

这和 bounce 处理放在一起看,逻辑就更清楚了:

  • catchall 主要是为了接住已有线程的回复;
  • 它不是业务新入口;
  • 也不应该承接模糊、不明归属的新邮件。

所以官方宁可把这种邮件退回去,也不愿让它们污染线程边界。

为什么还要专门忽略“回复 bounce 通知”的邮件

源码里还有个很细的保护:

  • 如果 references 里出现 -loop-detection-bounce-email@ 这样的标记,就认为这是在回复 bounce 通知,直接忽略。

这一步看似小,价值却很大。

因为退信邮件最容易引发的不是一次性错误,而是:

  • 自动系统再回一封;
  • 对方邮箱又自动答复;
  • 两边不断回环;
  • 业务线程、SMTP 和队列一起被刷爆。

官方显然非常清楚这个问题,所以专门在回弹时种一个可识别引用,再在后续入口处短路掉。

incoming alias 与 bounce 其实在守同一条边界

把这些代码连起来看,就能发现一个很有意思的事实:

  • alias 命中是在决定“这封外部邮件值不值得进入业务系统”;
  • bounce 归因是在决定“失败信号该不该反向写回业务系统”;
  • bounce reset 是在决定“旧的失败标签是否该撤销”;
  • catchall / loop guard 是在决定“异常邮件是否该继续污染线程”。

它们看起来像四件事,实际上都在守同一条边界:

外部邮件世界和内部业务线程世界,应该怎样安全、干净、可恢复地互相映射。

实战里最该怎么排

如果现场出现“收件 alias 有时正常、有时被退”“明明邮箱恢复了系统还说退信”“catchall 好像吞邮件”这类问题,我建议按这个顺序查:

  1. 先确认来信是不是先被识别成 bounce,而不是普通 incoming mail
  2. 检查 _routing_handle_bounce() 是否成功归因到 partner / notification / 原业务对象
  3. 看非 bounce 来信时 _routing_reset_bounce() 是否被触发,旧状态有没有被清掉
  4. 确认是不是直接写到了 catchall,或 catchall 与不可路由地址混写导致被回弹
  5. 检查是否碰到了 loop-detection 保护,把对 bounce 通知的回复直接忽略了。

一句话记忆

Odoo 的 incoming alias 不只是“把邮件收进来”,它还必须和 bounce 识别、失败归因、状态恢复与反循环保护一起工作,才能真正守住业务线程边界。

DISCUSSION

评论区

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