作者:Martin Brennan,来源:Discourse Meta,时间:2022 年 7 月 22 日 06:34
好的,这件事相当重要,请耐心听我说。首先,感谢
您提供如此详尽的回复以及调试/审查的提议,这真的
非常有帮助 今天早上我实际上一直在研究这个问题,
令人惊讶的是,在统一视图中,Thunderbird 中的线程功能在大多数情况 下都能正常工作,我认为 consistently 指向原始发帖人(OP)的 References 头有助于实现这一点(例如,此链中始终存在的主题 Reference 是
<topic/53@discoursehosted.martin-brennan.com>)。
我刚刚仔细重读了 RFC5322 第 3.6.4 节。它已不同于早期版本(822 和 2822),并合并了电子邮件 In-Reply-To 头、USENET References 头以及现代引用多条先前消息的回复功能。
简要总结:
Message-ID 是单条消息的唯一持久标识符
In-Reply-To 包含该消息直接回复的所有消息 ID,因此如果我回复两条消息,它将包含这两个消息 ID
References 是从原始发帖人(OP)到前一条消息的回复链中的前置消息 ID。因此,它确实应该始终 以 OP 的消息 ID 开头。
因此,对于像这样的讨论,假设标签就是消息 ID:
OP
-> reply1
-> reply2 ---+
-> reply3 |
-> reply4 |
-> reply5 <+
reply5 将包含:
message-id=reply5
in-reply-to=“reply2 reply4”
references=“OP reply3 reply4”
在 references 中包含 “reply1 reply2”(通往 reply5 的另一条链)也是合法的,但 RFC 明确建议不要这样做,因为某些客户端期望 references 是单一的线性回复链,而不是某种扁平化的有向图。
因此,我建议在构建 references 时,使用“主要”前置消息的 references,并附加该主要前置消息的消息 ID。这样,您总能得到一个顺序正确的线性链。
有趣的是,那里似乎有一些线程结构。
但请注意:顶部的帖子有一个小的“是回复”箭头。尽管它是第 1 号帖子。我想这是因为“主题”references 条目,这让 TB 认为存在一条更早的消息(当然实际上并没有)。
在 mutt 环境中,我们几乎看不到任何线程结构:
23Jul2022 06:24 Olha via Discus - ┌>[Py] [Users] I need an advise discuss-users 5.7K
22Jul2022 17:12 Paul Jurczak vi - ├>[Py] [Users] I need an advise discuss-users 5.5K
22Jul2022 13:21 Rob via Discuss - ├>[Py] [Users] I need an advise discuss-users 6.8K
22Jul2022 12:53 vasi-h via Disc - ├>[Py] [Users] I need an advise discuss-users 5.5K
22Jul2022 11:38 Cameron Simpson - ├>[Py] [Users] I need an advise discuss-users 14K
22Jul2022 10:27 Rob via Discuss - ├>[Py] [Users] I need an advise discuss-users 6.6K
22Jul2022 06:14 vasi-h via Disc r ┴>[Py] [Users] I need an advise discuss-users 6.5K
这是因为每条消息的 In-Reply-To 都直接指向虚构的“主题”消息 ID。Mutt 可能会忽略 References,因为它是邮件阅读器,而 References 起源于 USENET 新闻。也许 Thunderbird 正在使用 references,或者用 references 信息增强 in-reply-to。
您只需要查阅 In-Reply-To 或 References 其中之一即可实现线程化;前者来自电子邮件,后者来自 USENET。您同时支持两者(这很好!),因此我们需要使它们保持一致。
(题外话:关于 USENET 镜像也有讨论,因为几位 Python 用户通过 USENET 接口订阅列表。同样,这是一个独立的话题。)
[…]
[quote=“Cameron Simpson, post:8, topic:233499,
username:cameron-simpson”]
这看起来不错。它是否将此 ID 与数据库记录一起保存,以便将传入的
回复绑定到前置的论坛消息?
[/quote]
答案是——视情况而定。如果帖子是通过传入电子邮件在 Discourse 中创建的,例如您 这一封 ,当有人回复它时,我们会使用该帖子原始的传入 Message-ID 作为 In-Reply-To 和 References 头,如下所示:
discourse/lib/email/sender.rb at 98bacbd2c6b9fe57167cd32af5eb4839b4a5d1f6 · discourse/discourse · GitHub
否则,我们只是使用主题 OP 引用并生成一个新的引用,这显然就是导致所有问题的原因。在所有情况下,每次发送传出电子邮件时,我们都会生成一个新的 Message-ID,这似乎是正确的,并与其他邮件客户端保持一致。
唉,并不完全如此。如果您是消息的起源(即在 Discourse 中撰写),生成消息 ID 是可以的。如果没有消息 ID(这是非法的),生成一个则是标准做法(通常由 MTA 完成)。但如果您是在转发一条消息(在电子邮件中撰写),则应保留现有的消息 ID。
在我看来,您需要做三件事:
拥有一个稳定的消息 ID,不要替换传入消息的消息 ID
生成正确的 In-Reply-To,这很容易从直接的前置消息(即前置消息的 Message-ID)计算得出
生成正确的 References,这很容易计算为前置消息的 References + 前置消息的 Message-ID
针对第 1 点,查看您引用的代码,您可能会希望电子邮件消息 ID 为(Python 风格语法,抱歉):
def message_id(post):
return post.incoming_email.message_id or discourse_message_id(post)
即:如果帖子源自电子邮件,则使用其电子邮件消息 ID;否则,使用类似您在消息后面概述的算法生成 Discourse 消息 ID:任何 (a) 稳定且 (b) 语法有效的内容。
然后计算 In-Reply-To 和 References 字段就是第 2 点和第 3 点中所述的简单机械操作。
我想我明白您的意思了,是不是这样:
cameron 从 mutt 向 Discourse 发送电子邮件,获得 Message-ID: 74398756983476983@mail.com
Discourse 创建一个帖子,并将 Message-ID 与 IncomingEmail 记录一起存储在帖子中
正确。
johndoe 正在关注该主题,因此他们会收到来自 Discourse 的电子邮件,其中包含 Message-ID: topic/222/44@discourse.com,且没有对原始 Message-ID: 74398756983476983@mail.com 的引用
不。您确实需要将 IncomingEmail.message_id 作为发送给 johndoe 的电子邮件中的 Message-ID 传递过去。这是同一条消息。
这听起来正确吗?我们应该直接将那个 Message-ID “传递”给关注该主题的用户,而不是生成我们自己的,因为它已经是唯一的了?那么,如果 cameron 也在原始传出消息中抄送了他,johndoe 的邮件客户端会发生什么?这听起来确实像是一个独立的问题,所以最好为此开启一个新的 bug 话题。
通过传递它,原始消息(cameron->cc:johndoe)和 Discourse 转发的消息(cameron->Discourse->johndoe)具有相同的消息 ID 和相同的消息内容。接收邮件系统会存储这两条消息。邮件阅读器会看到这两条消息,并选择同时展示两者或只保留一条(这是邮件阅读器的策略决定——只保留一条很常见)。因为它们是完全相同的消息,通常保留哪一条都无所谓。
如果我们忽略 Discourse,考虑一条消息,它既通过列表副本发送,也通过直接电子邮件发送。它们是同一条消息,具有相同的消息 ID。
Cameron Simpson:
我和其他几个人使用 mutt。
我会在本地设置一个 mutt 客户端,看看您看到的也是什么情况。我从未在基于文本的客户端中测试过此功能(仅在 Gmail 和 Thunderbird 中测试过),所以我很想看看它看起来如何。
很乐意协助设置。对于线程视图,您需要将排序设置为线程化。Mutt 的可配置性非常高。
我今天早上解决这些问题的思路是,放弃我们在发送 Message-ID 头时生成的随机后缀,转而采用一种使用发送者和接收者 user_id 的方案。这样做的好处是,不需要在任何地方存储 Message-ID(除非传入电子邮件创建帖子),因此 References 和 In-Reply-To 头将始终一致。
是的,这样好多了。需要注意的是,传入电子邮件的消息 ID 应覆盖传出电子邮件中由 Discourse 生成的消息 ID。
(大多数邮件系统使用随机字符串,因为缺乏像 Discourse 主题消息结构这样的上下文环境——消息被视为独立的个体;但唯一真正的要求是持久的唯一性。)
让我举个例子。假设我们有这些用户:
martin - user_id 25
cameron - user_id 44
sam - user_id 78
bob - user_id 999
然后我们有一个主题,topic_id 233499,帖子从 post_id 100 开始作为 OP。格式将变为 topic/#{topic_id}/#{post_id}.s#{sender_user_id}r#{receiver_user_id}。
操作顺序如下:
martin 创建 OP
cameron 收到带有以下头的电子邮件:
Message-ID: topic/233499.s25r44@meta.discourse.org
References: topic/233499@meta.discourse.org
sam 收到带有以下头的电子邮件:
Message-ID: topic/233499.s25r78@meta.discourse.org
References: topic/233499@meta.discourse.org
OP 中不应 有 References 头。它对于线程化是不必要的,并且实际上假装存在某个不存在的“第 0 号帖子”。这意味着每个 OP (a) 看起来像一条回复(而它实际上不是),以及 (b) 看起来像是回复的对象在阅读器收件箱中缺失了。
这为 OP 的每个传出副本生成了不同的消息 ID。这很糟糕。它们必须是相同的。假设 sam 在回复中直接抄送了 cameron 。In-Reply-To 将引用 cameron 从未收到过的消息 ID。
您可以直接从消息 ID 字段中删除 sender_user_id 和 receiver_user_id,从而获得一个每个接收者都能看到的单一唯一 ID。
唯一性约束是帖子本身,而不是单个电子邮件层面的“消息”对象。
关于 References,OP 不应包含它。TB 和其他一切都会正常运作。如果它们使用 References 而不是 In-Reply-To 进行线程化,回复消息中的 References 就足够了。
这是 Mutt 中邮件列表讨论线程的开头:
16Jul2022 01:09 Rob Boehne - │├>[Python-Dev] Re: [SPAM] Re: Swit python-dev 9.2K
16Jul2022 01:33 Peter Wang - │├> python-dev 3.0K
16Jul2022 00:24 Skip Montanaro - ├>[Python-Dev] Re: Switching to Dis python-dev 4.2K
16Jul2022 04:49 Erlend Egeberg - ├>[Python-Dev] Re: Switching to Dis python-dev 10K
16Jul2022 04:20 Mariatta - ├>[Python-Dev] Re: Switching to Dis python-dev 10K
15Jul2022 21:18 Petr Viktorin - [Python-Dev] Switching to Discourse python-dev 4.2K
忽略我按最新邮件在顶部排序的习惯。看到初始帖子(在底部)没有箭头了吗?该消息没有 References,也没有 In-Reply-To。所有其他消息都有 In-Reply-To(可能还有 References,但这是一个电子邮件邮件列表,所以不一定;正如我之前提到的,它们是互补的。)
如果我重复我之前的 Discourse 示例:
23Jul2022 06:24 Olha via Discus - ┌>[Py] [Users] I need an advise discuss-users 5.7K
22Jul2022 17:12 Paul Jurczak vi - ├>[Py] [Users] I need an advise discuss-users 5.5K
22Jul2022 13:21 Rob via Discuss - ├>[Py] [Users] I need an advise discuss-users 6.8K
22Jul2022 12:53 vasi-h via Disc - ├>[Py] [Users] I need an advise discuss-users 5.5K
22Jul2022 11:38 Cameron Simpson - ├>[Py] [Users] I need an advise discuss-users 14K
22Jul2022 10:27 Rob via Discuss - ├>[Py] [Users] I need an advise discuss-users 6.6K
22Jul2022 06:14 vasi-h via Disc r ┴>[Py] [Users] I need an advise discuss-users 6.5K
看到它们全部 都有一个前导箭头吗?这是因为邮件客户端认为它们都是对某个共同的(且缺失 的)根消息的回复,这是由于 References 头中的“主题”消息 ID 造成的。而实际上,第 1 号帖子是上面显示的底部消息。
总结:
您的计划很好,前提是您从消息 ID 中删除发送者和接收者——它们是不必要的,事实上接收者会导致麻烦(发送者只是多余的)。
从 References 中删除“主题”伪消息 ID——它会误导邮件客户端(包括 TB,即使视觉上不明显)
cameron 通过电子邮件回复
discourse 收到来自 mutt 的带有以下头的电子邮件:
Message-ID: 43585349859734@test.com
References: topic/233499@meta.discourse.org topic/233499.s25r44@meta.discourse.org
In-Reply-To: topic/233499.s25r44@meta.discourse.org
是的,同样要注意,不应有“主题”引用。正如预期的那样,它引用了 OP 的消息 ID。尽管它应该是 sam 看到的 OP 的相同消息 ID。
discourse (作为来自上述电子邮件的 cameron )创建帖子 101
sam 收到来自 discourse 的带有以下头的电子邮件:
Message-ID: topic/233499/101.s44r78@meta.discourse.org
References: 43585349859734@test.com topic/233499@meta.discourse.org
In-Reply-To: 43585349859734@test.com
这里出错了。Message-ID 应该是来自 .incoming_post.message_id 字段的 43585349859734@test.com。(好吧,在我看来这是 post.message_id(),对于电子邮件生成的帖子,它返回 post.incoming_post.message_id,对于其他 Discourse 生成的帖子则返回 Discourse 生成的 ID)。
考虑一下:我撰写并发送我的回复,消息 ID 为 43585349859734@test.com。为了连续性,我在本地文件夹中保留了一份副本,其中显示为对 OP 的回复。理想情况下,Discourse 也会发送我帖子副本给我(这是许多邮件列表的策略设置),所以我也收到了 Discourse 的版本。这应该具有相同 的消息 ID,因为它是同一条消息,只是通过不同的途径。
Discourse 的消息不是 “回复”我的消息。它就是 我的消息,只是被转发了。
这种影响会延续到您随后的示例中。实际过程应该比您所做的更简单。
这样想。如果我通过电子邮件回复帖子,这实际上就像我通过 Discourse 给 sam (和其他人)发邮件一样。Discourse 将我的消息转发给接收电子邮件的订阅者,并“顺便”在论坛上保留一份副本
作为附注,我还研究了 GitHub 如何处理他们的通知电子邮件,注意到他们做了类似的事情,即他们有一个始终存在的 Reference(discourse/discourse/pull/252@github.com),用于所有与该“主题”相关的电子邮件,在这种情况下是一个 GitHub 拉取请求:
References: <discourse/discourse/pull/252@github.com> <discourse/discourse/pull/252/issue_event/7042100517@github.com>
In-Reply-To: <discourse/discourse/pull/252/issue_event/7042100517@github.com>
哦,GitHub。他们的议题电子邮件真是个灾难
然而,在他们的场景中,PR 就是 OP。所以直接引用拉取请求是合理的。您可以对第 1 号帖子使用“主题”消息 ID,前提是您不要同时也使用“主题/1”ID。但这似乎没什么意义——为第 1 号帖子做特殊处理需要额外的精力——我自己会直接使用“主题/1”。
增加一些复杂性。据我了解,管理员可以移动帖子或主题。这难道不会破坏“生成消息 ID”的方案吗,特别是如果他们只移动一个帖子时?我倾向于认为每个帖子 都应该有一个 _message_id 字段,从传入消息(来自电子邮件)填充,或者生成(通过 Discourse 发帖)。这样它就是持久的、稳定的,并且能够抵御帖子的任何重新排序或算法变更。
最后,还有一个小的安全考虑:如果传入的电子邮件消息 ID 声称是现有帖子的消息 ID,您应该忽略它(并可能退回该消息)。因为作为作者,我可以在该头中放入任何我喜欢的内容 我会选择直接丢弃消息 ID——接受帖子,但不要让它谎称是其他帖子——给您的副本赋予 Discourse 生成的 ID,然后按正常流程继续。