FIX: References header leading to broken email threading (#15206)

Since 3b13f1146b the email threading
in mail clients has been broken, because the random suffix meant
that the References header would always be different for non-group
SMTP email notifications sent out.

This commit fixes the issue by always using the "canonical" topic
reference ID inside the References header in the format:

topic/TOPIC_ID@HOST

Which was the old format. We also add the References header to
notifications sent for the first post arriving, so the threading
works for subsequent emails. The Message-ID header is still random
as per the previous change.
This commit is contained in:
Martin Brennan 2021-12-08 08:14:48 +10:00 committed by GitHub
parent a7fdcb921a
commit f26b8b448d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 60 additions and 21 deletions

View File

@ -37,14 +37,18 @@ module Email
"<topic/#{post.topic_id}/#{post.id}.#{random_suffix}@#{host}>"
end
def generate_for_topic(topic, use_incoming_email_if_present: false)
def generate_for_topic(topic, use_incoming_email_if_present: false, canonical: false)
first_post = topic.ordered_posts.first
if use_incoming_email_if_present && first_post.incoming_email&.message_id.present?
return "<#{first_post.incoming_email.message_id}>"
end
"<topic/#{topic.id}.#{random_suffix}@#{host}>"
if canonical
"<topic/#{topic.id}@#{host}>"
else
"<topic/#{topic.id}.#{random_suffix}@#{host}>"
end
end
def find_post_from_message_ids(message_ids)

View File

@ -122,8 +122,20 @@ module Email
add_attachments(post)
topic_message_id = Email::MessageIdService.generate_for_topic(topic, use_incoming_email_if_present: true)
post_message_id = Email::MessageIdService.generate_for_post(post, use_incoming_email_if_present: true)
# If the topic was created from an incoming email, then the Message-ID from
# that email will be the canonical reference, otherwise the canonical reference
# will be <topic/TOPIC_ID@host>. The canonical reference is used in the
# References header.
#
# This is so the sender of the original email still gets their nice threading
# maintained (because their mail client will initiate threading based on
# the Message-ID it generated) in the case where there is an incoming email.
#
# In the latter case, everyone will start their thread with the canonical reference,
# because we send it in the References header for all emails.
topic_canonical_reference_id = Email::MessageIdService.generate_for_topic(
topic, canonical: true, use_incoming_email_if_present: true
)
referenced_posts = Post.includes(:incoming_email)
.joins("INNER JOIN post_replies ON post_replies.post_id = posts.id ")
@ -135,23 +147,29 @@ module Email
"<#{referenced_post.incoming_email.message_id}>"
else
if referenced_post.post_number == 1
Email::MessageIdService.generate_for_topic(topic)
topic_canonical_reference_id
else
Email::MessageIdService.generate_for_post(referenced_post)
end
end
end
# https://www.ietf.org/rfc/rfc2822.txt
# See https://www.ietf.org/rfc/rfc2822.txt for the message format
# specification, more useful information can be found in Email::MessageIdService
#
# The References header is how mail clients handle threading. The Message-ID
# must always be unique.
if post.post_number == 1
@message.header['Message-ID'] = topic_message_id
@message.header['Message-ID'] = Email::MessageIdService.generate_for_topic(topic)
@message.header['References'] = [topic_canonical_reference_id]
else
@message.header['Message-ID'] = post_message_id
@message.header['In-Reply-To'] = referenced_post_message_ids[0] || topic_message_id
@message.header['References'] = [topic_message_id, referenced_post_message_ids].flatten.compact.uniq
@message.header['Message-ID'] = Email::MessageIdService.generate_for_post(post)
@message.header['In-Reply-To'] = referenced_post_message_ids[0] || topic_canonical_reference_id
@message.header['References'] = [topic_canonical_reference_id, referenced_post_message_ids].flatten.compact.uniq
end
# https://www.ietf.org/rfc/rfc2919.txt
# See https://www.ietf.org/rfc/rfc2919.txt for the List-ID
# specification.
if topic&.category && !topic.category.uncategorized?
list_id = "#{SiteSetting.title} | #{topic.category.name} <#{topic.category.name.downcase.tr(' ', '-')}.#{host}>"

View File

@ -277,23 +277,34 @@ describe Email::Sender do
Email::MessageIdService.stubs(:random_suffix).returns(random_message_id_suffix)
end
it "doesn't set the 'In-Reply-To' and 'References' headers on the first post" do
it "doesn't set the 'In-Reply-To' header but does set the 'References' header on the first post" do
message.header['X-Discourse-Post-Id'] = post_1.id
email_sender.send
expect(message.header['Message-Id'].to_s).to eq("<topic/#{topic.id}.#{random_message_id_suffix}@test.localhost>")
expect(message.header['In-Reply-To'].to_s).to be_blank
expect(message.header['References'].to_s).to be_blank
expect(message.header['References'].to_s).to eq("<topic/#{topic.id}@test.localhost>")
end
it "sets the 'In-Reply-To' header to the topic by default" do
it "sets the 'References' header with the incoming email Message-ID if it exists on the first post" do
incoming = Fabricate(:incoming_email, topic: topic, post: post_1, message_id: "blah1234@someemailprovider.com")
message.header['X-Discourse-Post-Id'] = post_1.id
email_sender.send
expect(message.header['Message-Id'].to_s).to eq("<topic/#{topic.id}.#{random_message_id_suffix}@test.localhost>")
expect(message.header['In-Reply-To'].to_s).to be_blank
expect(message.header['References'].to_s).to eq("<blah1234@someemailprovider.com>")
end
it "sets the 'In-Reply-To' header to the topic canonical reference by default" do
message.header['X-Discourse-Post-Id'] = post_2.id
email_sender.send
expect(message.header['Message-Id'].to_s).to eq("<topic/#{topic.id}/#{post_2.id}.#{random_message_id_suffix}@test.localhost>")
expect(message.header['In-Reply-To'].to_s).to eq("<topic/#{topic.id}.#{random_message_id_suffix}@test.localhost>")
expect(message.header['In-Reply-To'].to_s).to eq("<topic/#{topic.id}@test.localhost>")
end
it "sets the 'In-Reply-To' header to the newest replied post" do
@ -305,13 +316,13 @@ describe Email::Sender do
expect(message.header['In-Reply-To'].to_s).to eq("<topic/#{topic.id}/#{post_3.id}.#{random_message_id_suffix}@test.localhost>")
end
it "sets the 'References' header to the topic and all replied posts" do
it "sets the 'References' header to the topic canonical reference and all replied posts" do
message.header['X-Discourse-Post-Id'] = post_4.id
email_sender.send
references = [
"<topic/#{topic.id}.#{random_message_id_suffix}@test.localhost>",
"<topic/#{topic.id}@test.localhost>",
"<topic/#{topic.id}/#{post_3.id}.#{random_message_id_suffix}@test.localhost>",
"<topic/#{topic.id}/#{post_2.id}.#{random_message_id_suffix}@test.localhost>",
]
@ -319,7 +330,7 @@ describe Email::Sender do
expect(message.header['References'].to_s).to eq(references.join(" "))
end
it "uses the incoming_email message_id when available" do
it "uses the incoming_email message_id when available, but always uses a random message-id" do
topic_incoming_email = IncomingEmail.create(topic: topic, post: post_1, message_id: "foo@bar")
post_2_incoming_email = IncomingEmail.create(topic: topic, post: post_2, message_id: "bar@foo")
post_4_incoming_email = IncomingEmail.create(topic: topic, post: post_4, message_id: "wat@wat")
@ -328,7 +339,7 @@ describe Email::Sender do
email_sender.send
expect(message.header['Message-Id'].to_s).to eq("<#{post_4_incoming_email.message_id}>")
expect(message.header['Message-Id'].to_s).to eq("<topic/#{topic.id}/#{post_4.id}.5f1330cfd941f323d7f99b9e@test.localhost>")
references = [
"<#{topic_incoming_email.message_id}>",

View File

@ -24,15 +24,21 @@ describe Email::MessageIdService do
describe "#generate_for_topic" do
it "generates for the topic using the message_id on the first post's incoming_email" do
Fabricate(:incoming_email, message_id: "test@test.localhost", post: post)
Fabricate(:incoming_email, message_id: "test213428@somemailservice.com", post: post)
post.reload
expect(subject.generate_for_topic(topic, use_incoming_email_if_present: true)).to eq("<test@test.localhost>")
expect(subject.generate_for_topic(topic, use_incoming_email_if_present: true)).to eq("<test213428@somemailservice.com>")
end
it "generates for the topic without an incoming_email record" do
expect(subject.generate_for_topic(topic)).to match(subject.message_id_topic_id_regexp)
expect(subject.generate_for_topic(topic, use_incoming_email_if_present: true)).to match(subject.message_id_topic_id_regexp)
end
it "generates canonical for the topic" do
canonical_topic_id = subject.generate_for_topic(topic, canonical: true)
expect(canonical_topic_id).to match(subject.message_id_topic_id_regexp)
expect(canonical_topic_id).to eq("<topic/#{topic.id}@test.localhost>")
end
end
describe "find_post_from_message_ids" do