FIX: Add random suffix to outbound Message-ID for email (#15179)

Currently the Message-IDs we send out for outbound email
are not unique; for a post they look like:

topic/TOPIC_ID/POST_ID@HOST

And for a topic they look like:

topic/TOPIC_ID@HOST

This commit changes the outbound Message-IDs to also have
a random suffix before the host, so the new format is
like this:

topic/TOPIC_ID/POST_ID.RANDOM_SUFFIX@HOST

Or:

topic/TOPIC_ID.RANDOM_SUFFIX@HOST

This should help with email deliverability. This change
is backwards-compatible, the old Message-ID format will
still be recognized in the mail receiver flow, so people
will still be able to reply using Message-IDs, In-Reply-To,
and References headers that have already been sent.

This commit also refactors Message-ID related logic
to a central location, and adds judicious amounts of
tests and documentation.
This commit is contained in:
Martin Brennan
2021-12-06 10:34:39 +10:00
committed by GitHub
parent 11d1c520ff
commit 3b13f1146b
12 changed files with 304 additions and 104 deletions

View File

@ -64,30 +64,4 @@ describe Email do
end
end
describe "message_id_rfc_format" do
it "returns message ID in RFC format" do
expect(Email.message_id_rfc_format("test@test")).to eq("<test@test>")
end
it "returns input if already in RFC format" do
expect(Email.message_id_rfc_format("<test@test>")).to eq("<test@test>")
end
end
describe "message_id_clean" do
it "returns message ID if in RFC format" do
expect(Email.message_id_clean("<test@test>")).to eq("test@test")
end
it "returns input if a clean message ID is not in RFC format" do
message_id = "<" + "@" * 50
expect(Email.message_id_clean(message_id)).to eq(message_id)
end
end
end

View File

@ -947,6 +947,52 @@ describe Email::Receiver do
ordered_posts[1..-1].each(&:trash!)
expect { process(:email_reply_4) }.to change { topic.posts.count }.by(1)
end
describe "replying with various message-id formats" do
let!(:topic) do
process(:email_reply_1)
Topic.last
end
let!(:post) { Fabricate(:post, topic: topic) }
def process_mail_with_message_id(message_id)
mail_string = <<~REPLY
Return-Path: <two@foo.com>
From: Two <two@foo.com>
To: one@foo.com
Subject: RE: Testing email threading
Date: Fri, 15 Jan 2016 00:12:43 +0100
Message-ID: <44@foo.bar.mail>
In-Reply-To: <#{message_id}>
Mime-Version: 1.0
Content-Type: text/plain
Content-Transfer-Encoding: 7bit
This is email reply testing with Message-ID formats.
REPLY
Email::Receiver.new(mail_string).process!
end
it "posts a reply using a message-id in the format topic/TOPIC_ID/POST_ID@HOST" do
expect { process_mail_with_message_id("topic/#{topic.id}/#{post.id}@test.localhost") }.to change { Post.count }.by(1)
expect(topic.reload.posts.last.raw).to include("This is email reply testing with Message-ID formats")
end
it "posts a reply using a message-id in the format topic/TOPIC_ID@HOST" do
expect { process_mail_with_message_id("topic/#{topic.id}@test.localhost") }.to change { Post.count }.by(1)
expect(topic.reload.posts.last.raw).to include("This is email reply testing with Message-ID formats")
end
it "posts a reply using a message-id in the format topic/TOPIC_ID/POST_ID.RANDOM_SUFFIX@HOST" do
expect { process_mail_with_message_id("topic/#{topic.id}/#{post.id}.rjc3yr79834y@test.localhost") }.to change { Post.count }.by(1)
expect(topic.reload.posts.last.raw).to include("This is email reply testing with Message-ID formats")
end
it "posts a reply using a message-id in the format topic/TOPIC_ID.RANDOM_SUFFIX@HOST" do
expect { process_mail_with_message_id("topic/#{topic.id}/#{post.id}.x3487nxy877843x@test.localhost") }.to change { Post.count }.by(1)
expect(topic.reload.posts.last.raw).to include("This is email reply testing with Message-ID formats")
end
end
end
it "supports any kind of attachments when 'allow_all_attachments_for_group_messages' is enabled" do
@ -1161,6 +1207,7 @@ describe Email::Receiver do
NotificationEmailer.enable
SiteSetting.disallow_reply_by_email_after_days = 10000
Jobs.run_immediately!
Email::MessageIdService.stubs(:random_suffix).returns("blah123")
end
def reply_as_group_user
@ -1185,7 +1232,7 @@ describe Email::Receiver do
it "creates an EmailLog when someone from the group replies, and does not create an IncomingEmail record for the reply" do
email_log, group_post = reply_as_group_user
expect(email_log.message_id).to eq("topic/#{original_inbound_email_topic.id}/#{group_post.id}@test.localhost")
expect(email_log.message_id).to eq("topic/#{original_inbound_email_topic.id}/#{group_post.id}.blah123@test.localhost")
expect(email_log.to_address).to eq("two@foo.com")
expect(email_log.email_type).to eq("user_private_message")
expect(email_log.post_id).to eq(group_post.id)

View File

@ -260,6 +260,7 @@ describe Email::Sender do
end
context "email threading" do
let(:random_message_id_suffix) { "5f1330cfd941f323d7f99b9e" }
fab!(:topic) { Fabricate(:topic) }
fab!(:post_1) { Fabricate(:post, topic: topic, post_number: 1) }
@ -271,14 +272,17 @@ describe Email::Sender do
let!(:post_reply_2_4) { PostReply.create(post: post_2, reply: post_4) }
let!(:post_reply_3_4) { PostReply.create(post: post_3, reply: post_4) }
before { message.header['X-Discourse-Topic-Id'] = topic.id }
before do
message.header['X-Discourse-Topic-Id'] = topic.id
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
message.header['X-Discourse-Post-Id'] = post_1.id
email_sender.send
expect(message.header['Message-Id'].to_s).to eq("<topic/#{topic.id}@test.localhost>")
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
end
@ -288,8 +292,8 @@ describe Email::Sender do
email_sender.send
expect(message.header['Message-Id'].to_s).to eq("<topic/#{topic.id}/#{post_2.id}@test.localhost>")
expect(message.header['In-Reply-To'].to_s).to eq("<topic/#{topic.id}@test.localhost>")
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>")
end
it "sets the 'In-Reply-To' header to the newest replied post" do
@ -297,8 +301,8 @@ describe Email::Sender do
email_sender.send
expect(message.header['Message-Id'].to_s).to eq("<topic/#{topic.id}/#{post_4.id}@test.localhost>")
expect(message.header['In-Reply-To'].to_s).to eq("<topic/#{topic.id}/#{post_3.id}@test.localhost>")
expect(message.header['Message-Id'].to_s).to eq("<topic/#{topic.id}/#{post_4.id}.#{random_message_id_suffix}@test.localhost>")
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
@ -307,9 +311,9 @@ describe Email::Sender do
email_sender.send
references = [
"<topic/#{topic.id}@test.localhost>",
"<topic/#{topic.id}/#{post_3.id}@test.localhost>",
"<topic/#{topic.id}/#{post_2.id}@test.localhost>",
"<topic/#{topic.id}.#{random_message_id_suffix}@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>",
]
expect(message.header['References'].to_s).to eq(references.join(" "))
@ -328,7 +332,7 @@ describe Email::Sender do
references = [
"<#{topic_incoming_email.message_id}>",
"<topic/#{topic.id}/#{post_3.id}@test.localhost>",
"<topic/#{topic.id}/#{post_3.id}.#{random_message_id_suffix}@test.localhost>",
"<#{post_2_incoming_email.message_id}>",
]