discourse/spec/lib/email/receiver_spec.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

2404 lines
88 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
require "email/receiver"
RSpec.describe Email::Receiver do
before do
SiteSetting.email_in = true
SiteSetting.reply_by_email_address = "reply+%{reply_key}@bar.com"
SiteSetting.alternative_reply_by_email_addresses = "alt+%{reply_key}@bar.com"
end
def process(email_name, opts = {})
Email::Receiver.new(email(email_name), opts).process!
end
it "raises an EmptyEmailError when 'mail_string' is blank" do
expect { Email::Receiver.new(nil) }.to raise_error(Email::Receiver::EmptyEmailError)
expect { Email::Receiver.new("") }.to raise_error(Email::Receiver::EmptyEmailError)
end
it "raises a ScreenedEmailError when email address is screened" do
ScreenedEmail.expects(:should_block?).with("screened@mail.com").returns(true)
expect { process(:screened_email) }.to raise_error(Email::Receiver::ScreenedEmailError)
end
it "raises EmailNotAllowed when email address is not on allowlist" do
SiteSetting.allowed_email_domains = "example.com|bar.com"
Fabricate(:group, incoming_email: "some_group@bar.com")
expect { process(:blocklist_allowlist_email) }.to raise_error(Email::Receiver::EmailNotAllowed)
end
it "raises EmailNotAllowed when email address is on blocklist" do
SiteSetting.blocked_email_domains = "email.com|mail.com"
Fabricate(:group, incoming_email: "some_group@bar.com")
expect { process(:blocklist_allowlist_email) }.to raise_error(Email::Receiver::EmailNotAllowed)
end
it "raises an UserNotFoundError when staged users are disabled" do
2016-03-24 01:56:03 +08:00
SiteSetting.enable_staged_users = false
expect { process(:user_not_found) }.to raise_error(Email::Receiver::UserNotFoundError)
end
it "raises an AutoGeneratedEmailError when the mail is auto generated" do
expect { process(:auto_generated_precedence) }.to raise_error(
Email::Receiver::AutoGeneratedEmailError,
)
expect { process(:auto_generated_header) }.to raise_error(
Email::Receiver::AutoGeneratedEmailError,
)
end
it "raises a NoBodyDetectedError when the body is blank" do
expect { process(:no_body) }.to raise_error(Email::Receiver::NoBodyDetectedError)
end
2014-12-02 02:21:14 +08:00
it "raises a NoSenderDetectedError when the From header is missing" do
expect { process(:no_from) }.to raise_error(Email::Receiver::NoSenderDetectedError)
end
it "raises an InactiveUserError when the sender is inactive" do
Fabricate(:user, email: "inactive@bar.com", active: false)
expect { process(:inactive_sender) }.to raise_error(Email::Receiver::InactiveUserError)
end
2017-11-11 01:18:08 +08:00
it "raises a SilencedUserError when the sender has been silenced" do
Fabricate(:user, email: "silenced@bar.com", silenced_till: 1.year.from_now)
2017-11-11 01:18:08 +08:00
expect { process(:silenced_sender) }.to raise_error(Email::Receiver::SilencedUserError)
end
it "doesn't raise an InactiveUserError when the sender is staged" do
user = Fabricate(:user, email: "staged@bar.com", active: false, staged: true)
post = Fabricate(:post)
post_reply_key =
Fabricate(
:post_reply_key,
user: user,
post: post,
reply_key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
)
expect { process(:staged_sender) }.not_to raise_error
end
it "raises a BadDestinationAddress when destinations aren't matching any of the incoming emails" do
expect { process(:bad_destinations) }.to raise_error(Email::Receiver::BadDestinationAddress)
end
it "raises an OldDestinationError when notification is too old" do
SiteSetting.disallow_reply_by_email_after_days = 2
topic = Fabricate(:topic)
post = Fabricate(:post, topic: topic)
user = Fabricate(:user, email: "discourse@bar.com")
mail = email(:old_destination).gsub("424242", topic.id.to_s)
expect { Email::Receiver.new(mail).process! }.to raise_error(
Email::Receiver::BadDestinationAddress,
)
IncomingEmail.destroy_all
post.update!(created_at: 3.days.ago)
expect { Email::Receiver.new(mail).process! }.to raise_error(
Email::Receiver::OldDestinationError,
)
expect(IncomingEmail.last.error).to eq("Email::Receiver::OldDestinationError")
SiteSetting.disallow_reply_by_email_after_days = 0
IncomingEmail.destroy_all
expect { Email::Receiver.new(mail).process! }.to raise_error(
Email::Receiver::BadDestinationAddress,
)
end
describe "bounces" do
it "raises a BouncerEmailError" do
expect { process(:bounced_email) }.to raise_error(Email::Receiver::BouncedEmailError)
expect(IncomingEmail.last.is_bounce).to eq(true)
expect { process(:bounced_email_multiple_status_codes) }.to raise_error(
Email::Receiver::BouncedEmailError,
)
expect(IncomingEmail.last.is_bounce).to eq(true)
end
describe "creating whisper post in PMs for staged users" do
let(:email_address) { "linux-admin@b-s-c.co.jp" }
fab!(:user1) { Fabricate(:user, refresh_auto_groups: true) }
let(:user2) { Fabricate(:staged, email: email_address) }
let(:topic) do
Fabricate(
:topic,
archetype: "private_message",
category_id: nil,
user: user1,
allowed_users: [user1, user2],
)
end
let(:post) { create_post(topic: topic, user: user1) }
before do
SiteSetting.enable_staged_users = true
SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}"
end
def create_post_reply_key(value)
Fabricate(:post_reply_key, reply_key: value, user: user2, post: post)
end
it "when bounce without verp" do
create_post_reply_key("4f97315cc828096c9cb34c6f1a0d6fe8")
expect { process(:bounced_email) }.to raise_error(Email::Receiver::BouncedEmailError)
post = Post.last
expect(post.whisper?).to eq(true)
expect(post.raw).to eq(
I18n.t(
"system_messages.email_bounced",
email: email_address,
raw: "Your email bounced",
).strip,
)
expect(IncomingEmail.last.is_bounce).to eq(true)
end
context "when bounce with verp" do
let(:bounce_key) { "14b08c855160d67f2e0c2f8ef36e251e" }
before do
SiteSetting.reply_by_email_address = "foo+%{reply_key}@discourse.org"
create_post_reply_key(bounce_key)
Fabricate(
:email_log,
to_address: email_address,
user: user2,
bounce_key: bounce_key,
post: post,
)
end
it "creates a post with the bounce error" do
expect { process(:hard_bounce_via_verp) }.to raise_error(
Email::Receiver::BouncedEmailError,
)
post = Post.last
expect(post.whisper?).to eq(true)
expect(post.raw).to eq(
I18n.t(
"system_messages.email_bounced",
email: email_address,
raw: "Your email bounced",
).strip,
)
expect(IncomingEmail.last.is_bounce).to eq(true)
end
it "updates the email log with the bounce error message" do
expect { process(:hard_bounce_via_verp) }.to raise_error(
Email::Receiver::BouncedEmailError,
)
email_log = EmailLog.find_by(bounce_key: bounce_key)
expect(email_log.bounced).to eq(true)
expect(email_log.bounce_error_code).to eq("5.1.1")
end
end
end
end
it "logs a blank error" do
Email::Receiver.any_instance.stubs(:process_internal).raises(RuntimeError, "")
begin
process(:existing_user)
rescue StandardError
RuntimeError
end
expect(IncomingEmail.last.error).to eq("RuntimeError")
end
it "strips null bytes from the subject" do
expect do process(:null_byte_in_subject) end.to raise_error(
Email::Receiver::BadDestinationAddress,
)
end
describe "bounces to VERP" do
2016-05-03 05:15:32 +08:00
let(:bounce_key) { "14b08c855160d67f2e0c2f8ef36e251e" }
let(:bounce_key_2) { "b542fb5a9bacda6d28cc061d18e4eb83" }
fab!(:user) { Fabricate(:user, email: "linux-admin@b-s-c.co.jp") }
let!(:email_log) do
Fabricate(:email_log, to_address: user.email, user: user, bounce_key: bounce_key)
end
let!(:email_log_2) do
Fabricate(:email_log, to_address: user.email, user: user, bounce_key: bounce_key_2)
end
2016-05-03 05:15:32 +08:00
it "deals with soft bounces" do
expect { process(:soft_bounce_via_verp) }.to raise_error(Email::Receiver::BouncedEmailError)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.soft_bounce_score)
2016-05-03 05:15:32 +08:00
end
it "deals with hard bounces" do
expect { process(:hard_bounce_via_verp) }.to raise_error(Email::Receiver::BouncedEmailError)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
2016-05-03 05:15:32 +08:00
expect { process(:hard_bounce_via_verp_2) }.to raise_error(Email::Receiver::BouncedEmailError)
email_log_2.reload
expect(email_log_2.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score * 2)
expect(email_log_2.bounced).to eq(true)
2016-05-03 05:15:32 +08:00
end
it "works when the final recipient is different" do
expect { process(:verp_bounce_different_final_recipient) }.to raise_error(
Email::Receiver::BouncedEmailError,
)
email_log.reload
expect(email_log.bounced).to eq(true)
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.soft_bounce_score)
end
it "sends a system message once they reach the 'bounce_score_threshold'" do
expect(user.active).to eq(true)
user.user_stat.bounce_score = SiteSetting.bounce_score_threshold - 1
user.user_stat.save!
SystemMessage.expects(:create_from_system_user).with(user, :email_revoked)
expect { process(:hard_bounce_via_verp) }.to raise_error(Email::Receiver::BouncedEmailError)
end
2016-05-03 05:15:32 +08:00
end
describe "reply" do
let(:reply_key) { "4f97315cc828096c9cb34c6f1a0d6fe8" }
fab!(:category)
fab!(:user) { Fabricate(:user, email: "discourse@bar.com", refresh_auto_groups: true) }
fab!(:topic) { create_topic(category: category, user: user) }
fab!(:post) { create_post(topic: topic) }
let!(:post_reply_key) do
Fabricate(:post_reply_key, reply_key: reply_key, user: user, post: post)
end
let :topic_user do
TopicUser.find_by(topic_id: topic.id, user_id: user.id)
end
it "uses MD5 of 'mail_string' there is no message_id" do
mail_string = email(:missing_message_id)
expect { Email::Receiver.new(mail_string).process! }.to change { IncomingEmail.count }
expect(IncomingEmail.last.message_id).to eq(Digest::MD5.hexdigest(mail_string))
end
it "raises a ReplyUserNotMatchingError when the email address isn't matching the one we sent the notification to" do
Fabricate(:user, email: "someone_else@bar.com")
expect { process(:reply_user_not_matching) }.to raise_error(
Email::Receiver::ReplyUserNotMatchingError,
)
end
it "raises a FromReplyByAddressError when the email is from the reply by email address" do
expect { process(:from_reply_by_email_address) }.to raise_error(
Email::Receiver::FromReplyByAddressError,
)
end
2018-07-03 19:51:22 +08:00
it "accepts reply from secondary email address" do
Fabricate(:secondary_email, email: "someone_else@bar.com", user: user)
expect { process(:reply_user_not_matching) }.to change { topic.posts.count }
post = Post.last
expect(post.raw).to eq("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
expect(post.user).to eq(user)
end
it "raises a ReplyNotAllowedError when user without permissions is replying" do
Fabricate(:user, email: "bob@bar.com")
category.set_permissions(admins: :full)
category.save
expect { process(:reply_user_not_matching_but_known) }.to raise_error(
Email::Receiver::ReplyNotAllowedError,
)
end
it "raises a TopicNotFoundError when the topic was deleted" do
topic.update_columns(deleted_at: 1.day.ago)
expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicNotFoundError)
2015-12-10 01:44:01 +08:00
end
context "with a closed topic" do
before { topic.update_columns(closed: true) }
it "raises a TopicClosedError when the topic was closed" do
expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicClosedError)
end
it "Can watch topics via the watch command" do
# TODO support other locales as well, the tricky thing is that these string live in
# client.yml not on server yml so it is a bit tricky to find
topic.update_columns(closed: true)
process(:watch)
expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:watching])
end
2015-12-10 01:44:01 +08:00
it "Can mute topics via the mute command" do
process(:mute)
expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:muted])
end
it "can track a topic via the track command" do
process(:track)
expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:tracking])
end
end
it "raises an InvalidPost when there was an error while creating the post" do
expect { process(:too_small) }.to raise_error(Email::Receiver::TooShortPost)
end
it "raises an InvalidPost when there are too may mentions" do
SiteSetting.max_mentions_per_post = 1
Fabricate(:user, username: "user1")
Fabricate(:user, username: "user2")
expect { process(:too_many_mentions) }.to raise_error(Email::Receiver::InvalidPost)
end
it "raises an InvalidPostAction when they aren't allowed to like a post" do
topic.update_columns(archived: true)
expect { process(:like) }.to raise_error(Email::Receiver::InvalidPostAction)
end
it "creates a new reply post" do
handler_calls = 0
handler = proc { |_| handler_calls += 1 }
DiscourseEvent.on(:topic_created, &handler)
expect { process(:text_reply) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq(
"This is a text reply :)\n\nEmail parsing should not break because of a UTF-8 character: ",
)
expect(topic.posts.last.via_email).to eq(true)
expect(topic.posts.last.cooked).not_to match(/<br/)
expect { process(:html_reply) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq("This is a **HTML** reply ;)")
DiscourseEvent.off(:topic_created, &handler)
expect(handler_calls).to eq(0)
end
it "stores the created_via source against the incoming email" do
process(:text_reply, source: :handle_mail)
expect(IncomingEmail.last.created_via).to eq(IncomingEmail.created_via_types[:handle_mail])
process(:text_and_html_reply, source: :imap)
expect(IncomingEmail.last.created_via).to eq(IncomingEmail.created_via_types[:imap])
end
FEATURE: Overhaul email threading (#17996) See https://meta.discourse.org/t/discourse-email-messages-are-incorrectly-threaded/233499 for thorough reasoning. This commit changes how we generate Message-IDs and do email threading for emails sent from Discourse. The main changes are as follows: * Introduce an outbound_message_id column on Post that is either a) filled with a Discourse-generated Message-ID the first time that post is used for an outbound email or b) filled with an original Message-ID from an external mail client or service if the post was created from an incoming email. * Change Discourse-generated Message-IDs to be more consistent and static, in the format `discourse/post/:post_id@:host` * Do not send References or In-Reply-To headers for emails sent for the OP of topics. * Make sure that In-Reply-To is filled with either a) the OP's Message-ID if the post is not a direct reply or b) the parent post's Message-ID * Make sure that In-Reply-To has all referenced post's Message-IDs * Make sure that References is filled with a chain of Message-IDs from the OP down to the parent post of the new post. We also are keeping X-Discourse-Post-Id and X-Discourse-Topic-Id, headers that we previously removed, for easier visual debugging of outbound emails. Finally, we backfill the `outbound_message_id` for posts that have a linked `IncomingEmail` record, using the `message_id` of that record. We do not need to do that for posts that don't have an incoming email since they are backfilled at runtime if `outbound_message_id` is missing.
2022-09-26 07:14:24 +08:00
it "stores the message_id of the incoming email against the post as outbound_message_id" do
expect { process(:text_reply, source: :handle_mail) }.to change(Post, :count)
message_id = IncomingEmail.last.message_id
expect(Post.last.outbound_message_id).to eq(message_id)
end
it "automatically elides gmail quotes" do
SiteSetting.always_show_trimmed_content = true
expect { process(:gmail_html_reply) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq(
"This is a **GMAIL** reply ;)\n\n<details class='elided'>\n<summary title='Show trimmed content'>&#183;&#183;&#183;</summary>\n\nThis is the *elided* part!\n\n</details>",
)
end
it "doesn't process email with same message-id more than once" do
expect do
process(:text_reply)
process(:text_reply)
end.to change { topic.posts.count }.by(1)
end
it "handles different encodings correctly" do
expect { process(:hebrew_reply) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq("שלום! מה שלומך היום?")
expect { process(:chinese_reply) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq("您好! 你今天好吗?")
expect { process(:reply_with_weird_encoding) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq("This is a reply with a weird encoding.")
expect { process(:reply_with_8bit_encoding) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq("hab vergessen kritische zeichen einzufügen:\näöüÄÖÜß")
end
2018-02-21 18:26:41 +08:00
it "prefers text over html when site setting is disabled" do
SiteSetting.incoming_email_prefer_html = false
expect { process(:text_and_html_reply) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq("This is the *text* part.")
2014-10-14 18:12:01 +08:00
end
it "prefers html over text when site setting is enabled" do
SiteSetting.incoming_email_prefer_html = true
expect { process(:text_and_html_reply) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq("This is the **html** part.")
end
it "uses text when prefer_html site setting is enabled but no html is available" do
SiteSetting.incoming_email_prefer_html = true
expect { process(:text_reply) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq(
"This is a text reply :)\n\nEmail parsing should not break because of a UTF-8 character: ",
)
end
it "removes the 'on <date>, <contact> wrote' quoting line" do
expect { process(:on_date_contact_wrote) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq("This is the actual reply.")
end
it "removes the 'Previous Replies' marker" do
expect { process(:previous_replies) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq(
"This will not include the previous discussion that is present in this email.",
)
end
it "removes the translated 'Previous Replies' marker" do
expect { process(:previous_replies_de) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq(
"This will not include the previous discussion that is present in this email.",
)
end
it "removes the 'type reply above' marker" do
expect { process(:reply_above) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq(
"This will not include the previous discussion that is present in this email.",
)
end
it "removes the translated 'Previous Replies' marker" do
expect { process(:reply_above_de) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq(
"This will not include the previous discussion that is present in this email.",
)
end
it "handles multiple paragraphs" do
expect { process(:paragraphs) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq(
"Do you like liquorice?\n\nI really like them. One could even say that I am *addicted* to liquorice. And if\nyou can mix it up with some anise, then I'm in heaven ;)",
)
end
it "raises a NoSenderDetectedError when the From header can't be parsed" do
expect { process(:invalid_from_1) }.to raise_error(Email::Receiver::NoSenderDetectedError)
end
it "raises a NoSenderDetectedError when the From header doesn't contain an email address" do
expect { process(:invalid_from_2) }.to raise_error(Email::Receiver::NoSenderDetectedError)
end
it "doesn't raise an AutoGeneratedEmailError due to an X-Auto-Response-Suppress header" do
expect { process(:quirks_exchange_xars) }.to change { topic.posts.count }
end
it "doesn't raise an AutoGeneratedEmailError when the mail is auto generated but is allowlisted" do
SiteSetting.auto_generated_allowlist = "foo@bar.com|discourse@bar.com"
expect { process(:auto_generated_allowlisted) }.to change { topic.posts.count }
end
it "doesn't raise an AutoGeneratedEmailError when block_auto_generated_emails is disabled" do
SiteSetting.block_auto_generated_emails = false
expect { process(:auto_generated_unblocked) }.to change { topic.posts.count }
end
it "allows staged users to reply to a restricted category" do
user.update_columns(staged: true)
category.email_in = "category@bar.com"
category.email_in_allow_strangers = true
category.set_permissions(Group[:trust_level_4] => :full)
2019-06-04 16:40:10 +08:00
category.save!
expect { process(:staged_reply_restricted) }.to change { topic.posts.count }
end
it "posts a reply to the topic when the post was deleted" do
post.update_columns(deleted_at: 1.day.ago)
expect { process(:reply_user_matching) }.to change { topic.posts.count }
expect(topic.ordered_posts.last.reply_to_post_number).to be_nil
end
2016-01-20 17:25:25 +08:00
describe "Unsubscribing via email" do
let(:last_email) { ActionMailer::Base.deliveries.last }
describe "unsubscribe_subject.eml" do
it "sends an email asking the user to confirm the unsubscription" do
expect { process("unsubscribe_subject") }.to change {
ActionMailer::Base.deliveries.count
}.by(1)
expect(last_email.to.length).to eq 1
expect(last_email.from.length).to eq 1
expect(last_email.from).to include "noreply@#{Discourse.current_hostname}"
expect(last_email.to).to include "discourse@bar.com"
expect(last_email.subject).to eq I18n.t(:"unsubscribe_mailer.subject_template").gsub(
"%{site_title}",
SiteSetting.title,
)
end
it "does nothing unless unsubscribe_via_email is turned on" do
SiteSetting.unsubscribe_via_email = false
2016-01-20 17:25:25 +08:00
before_deliveries = ActionMailer::Base.deliveries.count
expect { process("unsubscribe_subject") }.to raise_error {
Email::Receiver::BadDestinationAddress
}
expect(before_deliveries).to eq ActionMailer::Base.deliveries.count
end
end
describe "unsubscribe_body.eml" do
it "sends an email asking the user to confirm the unsubscription" do
expect { process("unsubscribe_body") }.to change {
ActionMailer::Base.deliveries.count
}.by(1)
expect(last_email.to.length).to eq 1
expect(last_email.from.length).to eq 1
expect(last_email.from).to include "noreply@#{Discourse.current_hostname}"
expect(last_email.to).to include "discourse@bar.com"
expect(last_email.subject).to eq I18n.t(:"unsubscribe_mailer.subject_template").gsub(
"%{site_title}",
SiteSetting.title,
)
end
it "does nothing unless unsubscribe_via_email is turned on" do
SiteSetting.unsubscribe_via_email = false
2016-01-20 17:25:25 +08:00
before_deliveries = ActionMailer::Base.deliveries.count
expect { process("unsubscribe_body") }.to raise_error { Email::Receiver::InvalidPost }
expect(before_deliveries).to eq ActionMailer::Base.deliveries.count
end
end
it "raises an UnsubscribeNotAllowed and does not send an unsubscribe email" do
before_deliveries = ActionMailer::Base.deliveries.count
expect { process(:unsubscribe_new_user) }.to raise_error {
Email::Receiver::UnsubscribeNotAllowed
}
expect(before_deliveries).to eq ActionMailer::Base.deliveries.count
end
2016-01-20 17:25:25 +08:00
end
it "handles inline reply" do
expect { process(:inline_reply) }.to change { topic.posts.count }
2017-07-31 17:29:39 +08:00
expect(topic.posts.last.raw).to eq("And this is *my* reply :+1:")
end
it "retrieves the first part of multiple replies" do
expect { process(:inline_mixed_replies) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq(
"> WAT <https://bar.com/users/wat> November 28\n>\n> This is the previous post.\n\nAnd this is *my* reply :+1:\n\n> This is another post.\n\nAnd this is **another** reply.",
)
end
it "strips mobile/webmail signatures" do
expect { process(:iphone_signature) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq("This is not the signature you're looking for.")
end
it "strips 'original message' context" do
expect { process(:original_message) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq("This is a reply :)")
end
it "add the 'elided' part of the original message only for private messages" do
topic.update_columns(category_id: nil, archetype: Archetype.private_message)
topic.allowed_users << user
topic.save
expect { process(:original_message) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq(
"This is a reply :)\n\n<details class='elided'>\n<summary title='Show trimmed content'>&#183;&#183;&#183;</summary>\n\n---Original Message---\nThis part should not be included\n\n</details>",
)
end
it "doesn't include the 'elided' part of the original message when always_show_trimmed_content is disabled" do
SiteSetting.always_show_trimmed_content = false
expect { process(:original_message) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq("This is a reply :)")
end
it "adds the 'elided' part of the original message for public replies when always_show_trimmed_content is enabled" do
SiteSetting.always_show_trimmed_content = true
expect { process(:original_message) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq(
"This is a reply :)\n\n<details class='elided'>\n<summary title='Show trimmed content'>&#183;&#183;&#183;</summary>\n\n---Original Message---\nThis part should not be included\n\n</details>",
)
end
it "doesn't trim the message when trim_incoming_emails is disabled" do
SiteSetting.trim_incoming_emails = false
expect { process(:original_message) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq(
"This is a reply :)\n\n---Original Message---\nThis part should not be included",
)
end
it "supports attached images in TEXT part" do
2018-02-21 18:26:41 +08:00
SiteSetting.incoming_email_prefer_html = false
2017-04-15 12:11:02 +08:00
expect { process(:no_body_with_image) }.to change { topic.posts.count }
post = topic.posts.last
upload = post.uploads.first
expect(post.raw).to include(
"![#{upload.original_filename}|#{upload.width}x#{upload.height}](#{upload.short_url})",
)
expect { process(:inline_image) }.to change { topic.posts.count }
post = topic.posts.last
upload = post.uploads.first
expect(post.raw).to include(
"![#{upload.original_filename}|#{upload.width}x#{upload.height}](#{upload.short_url})",
)
end
it "supports attached images in HTML part" do
SiteSetting.incoming_email_prefer_html = true
expect { process(:inline_image) }.to change { topic.posts.count }
post = topic.posts.last
upload = post.uploads.last
expect(post.raw).to eq(<<~MD.chomp)
**Before**
<img src="#{upload.short_url}" alt="内嵌图片 1">
*After*
MD
end
it "gracefully handles malformed images in HTML part" do
expect { process(:inline_image_2) }.to change { topic.posts.count }
post = topic.posts.last
upload = post.uploads.last
expect(post.raw).to eq(<<~MD.chomp)
[image:#{"0" * 5000}
![#{upload.original_filename}|#{upload.width}x#{upload.height}](#{upload.short_url})
MD
end
it "supports attached images in signature" do
SiteSetting.incoming_email_prefer_html = true
SiteSetting.always_show_trimmed_content = true
expect { process(:body_with_image) }.to change { topic.posts.count }
post = topic.posts.last
upload = post.uploads.last
expect(post.raw).to eq(<<~MD.chomp)
This is a **GMAIL** reply ;)
<details class='elided'>
<summary title='Show trimmed content'>&#183;&#183;&#183;</summary>
<img src="upload://qUm0DGR49PAZshIi7HxMd3cAlzn.png" width="300" height="200">
</details>
MD
end
it "supports attachments" do
SiteSetting.authorized_extensions = "txt|jpg"
expect { process(:attached_txt_file) }.to change { topic.posts.count }
2019-01-29 01:40:52 +08:00
post = topic.posts.last
upload = post.uploads.first
expect(post.raw).to eq(<<~MD.chomp)
Please find some text file attached.
[#{upload.original_filename}|attachment](#{upload.short_url}) (20 Bytes)
MD
expect { process(:apple_mail_attachment) }.to change { topic.posts.count }
2019-01-29 01:40:52 +08:00
post = topic.posts.last
upload = post.uploads.first
expect(post.raw).to eq(<<~MD.chomp)
Picture below.
<img apple-inline="yes" id="06C04C58-783E-4753-9B6B-D57403903060" src="#{upload.short_url}" class="">
Picture above.
MD
end
it "tries not to repeat duplicate attachments" do
SiteSetting.authorized_extensions = "jpg"
expect { process(:logo_1) }.to change { UploadReference.count }.by(1)
expect(topic.posts.last.raw).to match %r{upload://}
expect { process(:logo_2) }.not_to change { UploadReference.count }
expect(topic.posts.last.raw).not_to match %r{upload://}
end
it "tries not to repeat duplicate secure attachments" do
setup_s3
stub_s3_store
SiteSetting.secure_uploads = true
SiteSetting.authorized_extensions = "jpg"
expect { process(:logo_1) }.to change { UploadReference.count }.by(1)
expect(topic.posts.last.raw).to match %r{upload://}
expect { process(:logo_2) }.not_to change { UploadReference.count }
expect(topic.posts.last.raw).not_to match %r{upload://}
end
it "works with removed attachments" do
SiteSetting.authorized_extensions = "jpg"
expect { process(:removed_attachments) }.to change { topic.posts.count }
post = topic.posts.last
expect(post.uploads).to be_empty
end
it "supports eml attachments" do
SiteSetting.authorized_extensions = "eml"
expect { process(:attached_eml_file) }.to change { topic.posts.count }
2019-01-29 01:40:52 +08:00
post = topic.posts.last
upload = post.uploads.first
expect(post.raw).to eq(<<~MD.chomp)
Please find the eml file attached.
[#{upload.original_filename}|attachment](#{upload.short_url}) (193 Bytes)
MD
end
it "can decode attachments" do
SiteSetting.authorized_extensions = "pdf"
Fabricate(:group, incoming_email: "one@foo.com")
process(:encoded_filename)
expect(Upload.last.original_filename).to eq("This is a test.pdf")
end
context "when attachment is rejected" do
it "sends out the warning email" do
expect { process(:attached_txt_file) }.to change { EmailLog.count }.by(1)
expect(EmailLog.last.email_type).to eq("email_reject_attachment")
2019-01-29 01:40:52 +08:00
expect(topic.posts.last.uploads.size).to eq 0
end
it "doesn't send out the warning email if sender is staged user" do
user.update_columns(staged: true)
expect { process(:attached_txt_file) }.not_to change { EmailLog.count }
2019-01-29 01:40:52 +08:00
expect(topic.posts.last.uploads.size).to eq 0
end
it "creates the post with attachment missing message" do
missing_attachment_regex =
Regexp.escape(I18n.t("emails.incoming.missing_attachment", filename: "text.txt"))
expect { process(:attached_txt_file) }.to change { topic.posts.count }
2019-01-29 01:40:52 +08:00
post = topic.posts.last
expect(post.raw).to match(/#{missing_attachment_regex}/)
expect(post.uploads.size).to eq 0
end
end
it "supports emails with just an attachment" do
SiteSetting.authorized_extensions = "pdf"
expect { process(:attached_pdf_file) }.to change { topic.posts.count }
2019-01-29 01:40:52 +08:00
post = topic.posts.last
upload = post.uploads.last
expect(post.raw).to include(
"[#{upload.original_filename}|attachment](#{upload.short_url}) (64 KB)",
)
end
it "supports liking via email" do
expect { process(:like) }.to change(PostAction, :count)
end
it "ensures posts aren't dated in the future" do
# PostCreator doesn't provide sub-second accuracy for created_at
now = freeze_time Time.zone.now.round
expect { process(:from_the_future) }.to change { topic.posts.count }
expect(topic.posts.last.created_at).to eq_time(now)
end
it "accepts emails with wrong reply key if the system knows about the forwarded email" do
Fabricate(:user, email: "bob@bar.com", refresh_auto_groups: true)
Fabricate(
:incoming_email,
raw: <<~RAW,
2017-11-13 22:20:36 +08:00
Return-Path: <discourse@bar.com>
From: Alice <discourse@bar.com>
To: dave@bar.com, reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com
CC: carol@bar.com, bob@bar.com
Subject: Hello world
Date: Fri, 15 Jan 2016 00:12:43 +0100
Message-ID: <10@foo.bar.mail>
Mime-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
This post was created by email.
RAW
from_address: "discourse@bar.com",
to_addresses: "dave@bar.com;reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com",
cc_addresses: "carol@bar.com;bob@bar.com",
topic: topic,
post: post,
user: user,
)
expect { process(:reply_user_not_matching_but_known) }.to change { topic.posts.count }
2017-11-12 00:27:28 +08:00
end
it "re-enables user's PM email notifications when user replies to a private topic" do
topic.update_columns(category_id: nil, archetype: Archetype.private_message)
topic.allowed_users << user
topic.save
user.user_option.update_columns(email_messages_level: UserOption.email_level_types[:never])
expect { process(:reply_user_matching) }.to change { topic.posts.count }
user.reload
expect(user.user_option.email_messages_level).to eq(UserOption.email_level_types[:always])
end
end
shared_examples "creates topic with forwarded message as quote" do |destination, address|
it "creates topic with forwarded message as quote" do
expect { process(:forwarded_email_1) }.to change(Topic, :count)
topic = Topic.last
if destination == :category
expect(topic.category).to eq(Category.where(email_in: address).first)
else
expect(topic.archetype).to eq(Archetype.private_message)
expect(topic.allowed_groups).to eq(Group.where(incoming_email: address))
end
post = Post.last
expect(post.user.email).to eq("ba@bar.com")
expect(post.raw).to eq(<<~RAW.chomp)
@team, can you have a look at this email below?
[quote]
From: Some One &lt;some@one\\.com&gt;
To: Ba Bar &lt;ba@bar\\.com&gt;
Date: Mon, 1 Dec 2016 00:13:37 \\+0100
Subject: Discoursing much?
Hello Ba Bar,
Discoursing much today?
XoXo
[/quote]
RAW
end
end
describe "new message to a group" do
fab!(:group) { Fabricate(:group, incoming_email: "team@bar.com|meat@bar.com") }
it "handles encoded display names" do
expect { process(:encoded_display_name) }.to change(Topic, :count)
topic = Topic.last
expect(topic.title).to eq("I need help")
expect(topic.private_message?).to eq(true)
expect(topic.allowed_groups).to include(group)
user = topic.user
expect(user.staged).to eq(true)
expect(user.username).to eq("random.name")
expect(user.name).to eq("Случайная Имя")
end
it "handles email with no subject" do
expect { process(:no_subject) }.to change(Topic, :count)
2017-01-20 06:01:51 +08:00
expect(Topic.last.title).to eq("This topic needs a title")
end
it "invites everyone in the chain but emails configured as 'incoming' (via reply, group or category)" do
expect { process(:cc) }.to change(Topic, :count)
topic = Topic.last
emails = topic.allowed_users.joins(:user_emails).pluck(:"user_emails.email")
expect(emails).to contain_exactly("someone@else.com", "discourse@bar.com", "wat@bar.com")
expect(topic.topic_users.count).to eq(3)
end
2018-07-03 19:51:22 +08:00
it "invites users with a secondary email in the chain" do
user1 =
Fabricate(
:user,
trust_level: SiteSetting.email_in_min_trust,
user_emails: [
Fabricate.build(:secondary_email, email: "discourse@bar.com"),
Fabricate.build(:secondary_email, email: "someone@else.com"),
],
refresh_auto_groups: true,
2018-07-03 19:51:22 +08:00
)
user2 =
Fabricate(
:user,
trust_level: SiteSetting.email_in_min_trust,
user_emails: [
Fabricate.build(:secondary_email, email: "team@bar.com"),
Fabricate.build(:secondary_email, email: "wat@bar.com"),
],
refresh_auto_groups: true,
2018-07-03 19:51:22 +08:00
)
expect { process(:cc) }.to change(Topic, :count)
expect(Topic.last.allowed_users).to contain_exactly(user1, user2)
end
it "cap the number of staged users created per email" do
SiteSetting.maximum_staged_users_per_email = 1
expect { process(:cc) }.to change(Topic, :count).by(1).and change(User, :count).by(1)
expect(Topic.last.ordered_posts[-1].post_type).to eq(Post.types[:moderator_action])
end
it "cap the number of staged users existing per email" do
Fabricate(:user, email: "discourse@bar.com", staged: true) # from
Fabricate(:user, email: "someone@else.com", staged: true) # to
SiteSetting.maximum_staged_users_per_email = 1
expect { process(:cc) }.to change(Topic, :count).and not_change(User, :count)
expect(Topic.last.ordered_posts[-1].post_type).to eq(Post.types[:moderator_action])
end
it "rejects messages with too many recipients" do
SiteSetting.maximum_recipients_per_new_group_email = 3
expect { process(:cc) }.to raise_error(Email::Receiver::TooManyRecipientsError)
end
FEATURE: Overhaul email threading (#17996) See https://meta.discourse.org/t/discourse-email-messages-are-incorrectly-threaded/233499 for thorough reasoning. This commit changes how we generate Message-IDs and do email threading for emails sent from Discourse. The main changes are as follows: * Introduce an outbound_message_id column on Post that is either a) filled with a Discourse-generated Message-ID the first time that post is used for an outbound email or b) filled with an original Message-ID from an external mail client or service if the post was created from an incoming email. * Change Discourse-generated Message-IDs to be more consistent and static, in the format `discourse/post/:post_id@:host` * Do not send References or In-Reply-To headers for emails sent for the OP of topics. * Make sure that In-Reply-To is filled with either a) the OP's Message-ID if the post is not a direct reply or b) the parent post's Message-ID * Make sure that In-Reply-To has all referenced post's Message-IDs * Make sure that References is filled with a chain of Message-IDs from the OP down to the parent post of the new post. We also are keeping X-Discourse-Post-Id and X-Discourse-Topic-Id, headers that we previously removed, for easier visual debugging of outbound emails. Finally, we backfill the `outbound_message_id` for posts that have a linked `IncomingEmail` record, using the `message_id` of that record. We do not need to do that for posts that don't have an incoming email since they are backfilled at runtime if `outbound_message_id` is missing.
2022-09-26 07:14:24 +08:00
it "uses the incoming_email message-id as the new post's outbound_message_id" do
expect { process(:cc) }.to change(Topic, :count)
message_id = IncomingEmail.last.message_id
expect(Topic.last.first_post.outbound_message_id).to eq(message_id)
end
describe "reply-to header" do
before { SiteSetting.block_auto_generated_emails = false }
it "extracts address and uses it for comparison" do
expect { process(:reply_to_whitespaces) }.to change(Topic, :count).by(1)
user = User.last
incoming = IncomingEmail.find_by(message_id: "TXULO4v6YU0TzeL2buFAJNU2MK21c7t4@example.com")
topic = incoming.topic
expect(incoming.from_address).to eq("johndoe@example.com")
expect(user.email).to eq("johndoe@example.com")
end
it "handles emails where there is a Reply-To address, using that instead of the from address, if X-Original-From is present" do
expect { process(:reply_to_different_to_from) }.to change(Topic, :count).by(1)
user = User.last
incoming = IncomingEmail.find_by(message_id: "3848c3m98r439c348mc349@test.mailinglist.com")
topic = incoming.topic
expect(incoming.from_address).to eq("arthurmorgan@reddeadtest.com")
expect(user.email).to eq("arthurmorgan@reddeadtest.com")
end
it "allows for quotes around the display name of the Reply-To address" do
expect { process(:reply_to_different_to_from_quoted_display_name) }.to change(
Topic,
:count,
).by(1)
user = User.last
incoming = IncomingEmail.find_by(message_id: "3848c3m98r439c348mc349@test.mailinglist.com")
topic = incoming.topic
expect(incoming.from_address).to eq("johnmarston@reddeadtest.com")
expect(user.email).to eq("johnmarston@reddeadtest.com")
end
it "does not use the reply-to address if an X-Original-From header is not present" do
expect { process(:reply_to_different_to_from_no_x_original) }.to change(Topic, :count).by(1)
user = User.last
incoming = IncomingEmail.find_by(message_id: "3848c3m98r439c348mc349@test.mailinglist.com")
topic = incoming.topic
expect(incoming.from_address).to eq("westernsupport@test.mailinglist.com")
expect(user.email).to eq("westernsupport@test.mailinglist.com")
end
it "does not use the reply-to address if the X-Original-From header is different from the reply-to address" do
expect { process(:reply_to_different_to_from_x_original_different) }.to change(
Topic,
:count,
).by(1)
user = User.last
incoming = IncomingEmail.find_by(message_id: "3848c3m98r439c348mc349@test.mailinglist.com")
topic = incoming.topic
expect(incoming.from_address).to eq("westernsupport@test.mailinglist.com")
expect(user.email).to eq("westernsupport@test.mailinglist.com")
end
end
describe "when 'find_related_post_with_key' is disabled" do
before { SiteSetting.find_related_post_with_key = false }
it "associates email replies using both 'In-Reply-To' and 'References' headers" do
expect { process(:email_reply_1) }.to change(Topic, :count).by(1) &
change(Post, :count).by(3)
topic = Topic.last
ordered_posts = topic.ordered_posts
expect(ordered_posts.first.raw).to eq("This is email reply **1**.")
ordered_posts[1..-1].each do |post|
expect(post.action_code).to eq("invited_user")
expect(post.user.email).to eq("one@foo.com")
expect(%w[two three].include?(post.custom_fields["action_code_who"])).to eq(true)
end
expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1)
expect { process(:email_reply_3) }.to change { topic.posts.count }.by(1)
ordered_posts[1..-1].each(&:trash!)
expect { process(:email_reply_4) }.to change { topic.posts.count }.by(1)
end
FEATURE: Overhaul email threading (#17996) See https://meta.discourse.org/t/discourse-email-messages-are-incorrectly-threaded/233499 for thorough reasoning. This commit changes how we generate Message-IDs and do email threading for emails sent from Discourse. The main changes are as follows: * Introduce an outbound_message_id column on Post that is either a) filled with a Discourse-generated Message-ID the first time that post is used for an outbound email or b) filled with an original Message-ID from an external mail client or service if the post was created from an incoming email. * Change Discourse-generated Message-IDs to be more consistent and static, in the format `discourse/post/:post_id@:host` * Do not send References or In-Reply-To headers for emails sent for the OP of topics. * Make sure that In-Reply-To is filled with either a) the OP's Message-ID if the post is not a direct reply or b) the parent post's Message-ID * Make sure that In-Reply-To has all referenced post's Message-IDs * Make sure that References is filled with a chain of Message-IDs from the OP down to the parent post of the new post. We also are keeping X-Discourse-Post-Id and X-Discourse-Topic-Id, headers that we previously removed, for easier visual debugging of outbound emails. Finally, we backfill the `outbound_message_id` for posts that have a linked `IncomingEmail` record, using the `message_id` of that record. We do not need to do that for posts that don't have an incoming email since they are backfilled at runtime if `outbound_message_id` is missing.
2022-09-26 07:14:24 +08:00
describe "replying with various message-id formats using In-Reply-To header" 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 = <<~EMAIL
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.
EMAIL
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
FEATURE: Overhaul email threading (#17996) See https://meta.discourse.org/t/discourse-email-messages-are-incorrectly-threaded/233499 for thorough reasoning. This commit changes how we generate Message-IDs and do email threading for emails sent from Discourse. The main changes are as follows: * Introduce an outbound_message_id column on Post that is either a) filled with a Discourse-generated Message-ID the first time that post is used for an outbound email or b) filled with an original Message-ID from an external mail client or service if the post was created from an incoming email. * Change Discourse-generated Message-IDs to be more consistent and static, in the format `discourse/post/:post_id@:host` * Do not send References or In-Reply-To headers for emails sent for the OP of topics. * Make sure that In-Reply-To is filled with either a) the OP's Message-ID if the post is not a direct reply or b) the parent post's Message-ID * Make sure that In-Reply-To has all referenced post's Message-IDs * Make sure that References is filled with a chain of Message-IDs from the OP down to the parent post of the new post. We also are keeping X-Discourse-Post-Id and X-Discourse-Topic-Id, headers that we previously removed, for easier visual debugging of outbound emails. Finally, we backfill the `outbound_message_id` for posts that have a linked `IncomingEmail` record, using the `message_id` of that record. We do not need to do that for posts that don't have an incoming email since they are backfilled at runtime if `outbound_message_id` is missing.
2022-09-26 07:14:24 +08:00
it "posts a reply using a message-id in the format discourse/post/POST_ID@HOST" do
expect {
process_mail_with_message_id("discourse/post/#{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
end
end
it "supports any kind of attachments when 'allow_all_attachments_for_group_messages' is enabled" do
SiteSetting.allow_all_attachments_for_group_messages = true
expect { process(:attached_rb_file) }.to change(Topic, :count)
post = Topic.last.first_post
upload = post.uploads.first
expect(post.raw).to include(
"[#{upload.original_filename}|attachment](#{upload.short_url}) (#{upload.filesize} Bytes)",
)
end
it "reenables user's PM email notifications when user emails new topic to group" do
user = Fabricate(:user, email: "existing@bar.com")
user.user_option.update_columns(email_messages_level: UserOption.email_level_types[:never])
expect { process(:group_existing_user) }.to change(Topic, :count)
user.reload
expect(user.user_option.email_messages_level).to eq(UserOption.email_level_types[:always])
end
context "with forwarded emails behaviour set to create replies" do
before do
Fabricate(:group, incoming_email: "some_group@bar.com")
SiteSetting.forwarded_emails_behaviour = "create_replies"
end
it "handles forwarded emails" do
expect { process(:forwarded_email_1) }.to change(Topic, :count)
forwarded_post, last_post = *Post.last(2)
expect(forwarded_post.user.email).to eq("some@one.com")
expect(last_post.user.email).to eq("ba@bar.com")
expect(forwarded_post.raw).to match(/XoXo/)
expect(last_post.raw).to match(/can you have a look at this email below/)
expect(last_post.post_type).to eq(Post.types[:regular])
end
it "handles weirdly forwarded emails" do
group.add(Fabricate(:user, email: "ba@bar.com"))
group.save
SiteSetting.forwarded_emails_behaviour = "create_replies"
expect { process(:forwarded_email_2) }.to change(Topic, :count)
forwarded_post, last_post = *Post.last(2)
expect(forwarded_post.user.email).to eq("some@one.com")
expect(last_post.user.email).to eq("ba@bar.com")
expect(forwarded_post.raw).to match(/XoXo/)
expect(last_post.raw).to match(/can you have a look at this email below/)
expect(last_post.post_type).to eq(Post.types[:whisper])
end
# Who thought this was a good idea?!
it "doesn't blow up with localized email headers" do
expect { process(:forwarded_email_3) }.to change(Topic, :count)
end
it "adds a small action post to explain who forwarded the email when the sender didn't write anything" do
expect { process(:forwarded_email_4) }.to change(Topic, :count)
forwarded_post, last_post = *Post.last(2)
expect(forwarded_post.user.email).to eq("some@one.com")
expect(forwarded_post.raw).to match(/XoXo/)
expect(last_post.user.email).to eq("ba@bar.com")
expect(last_post.post_type).to eq(Post.types[:small_action])
expect(last_post.action_code).to eq("forwarded")
end
end
context "with forwarded emails behaviour set to quote" do
before { SiteSetting.forwarded_emails_behaviour = "quote" }
include_examples "creates topic with forwarded message as quote",
:group,
"team@bar.com|meat@bar.com"
end
FEATURE: Use group SMTP settings for sending user notification emails (initial) (#13220) This PR changes the `UserNotification` class to send outbound `user_private_message` using the group's SMTP settings, but only if: * The first allowed_group on the topic has SMTP configured and enabled * SiteSetting.enable_smtp is true * The group does not have IMAP enabled, if this is enabled the `GroupSMTPMailer` handles things The email is sent using the group's `email_username` as both the `from` and `reply-to` address, so when the user replies from their email it will go through the group's SMTP inbox, which needs to have email forwarding set up to send the message on to a location (such as a hosted site email address like meta@discoursemail.com) where it can be POSTed into discourse's handle_mail route. Also includes a fix to `EmailReceiver#group_incoming_emails_regex` to include the `group.email_username` so the group does not get a staged user created and invited to the topic (which was a problem for IMAP), as well as updating `Group.find_by_email` to find using the `email_username` as well for inbound emails with that as the TO address. #### Note This is safe to merge without impacting anyone seriously. If people had SMTP enabled for a group they would have IMAP enabled too currently, and that is a very small amount of users because IMAP is an alpha product, and also because the UserNotification change has a guard to make sure it is not used if IMAP is enabled for the group. The existing IMAP tests work, and I tested this functionality by manually POSTing replies to the SMTP address into my local discourse. There will probably be more work needed on this, but it needs to be tested further in a real hosted environment to continue.
2021-06-03 12:47:32 +08:00
context "when a reply is sent to a group's email_username" do
let!(:topic) do
group.update(email_username: "team@somesmtpaddress.com")
process(:email_reply_1)
Topic.last
end
it "does not invite the group email_username as a staged user" do
process(:email_reply_to_group_email_username)
expect(User.find_by_email("team@somesmtpaddress.com")).to eq(nil)
end
it "creates the reply when the sender and referenced messsage id are known" do
expect { process(:email_reply_to_group_email_username) }.to change { topic.posts.count }.by(
1,
).and not_change { Topic.count }
FEATURE: Use group SMTP settings for sending user notification emails (initial) (#13220) This PR changes the `UserNotification` class to send outbound `user_private_message` using the group's SMTP settings, but only if: * The first allowed_group on the topic has SMTP configured and enabled * SiteSetting.enable_smtp is true * The group does not have IMAP enabled, if this is enabled the `GroupSMTPMailer` handles things The email is sent using the group's `email_username` as both the `from` and `reply-to` address, so when the user replies from their email it will go through the group's SMTP inbox, which needs to have email forwarding set up to send the message on to a location (such as a hosted site email address like meta@discoursemail.com) where it can be POSTed into discourse's handle_mail route. Also includes a fix to `EmailReceiver#group_incoming_emails_regex` to include the `group.email_username` so the group does not get a staged user created and invited to the topic (which was a problem for IMAP), as well as updating `Group.find_by_email` to find using the `email_username` as well for inbound emails with that as the TO address. #### Note This is safe to merge without impacting anyone seriously. If people had SMTP enabled for a group they would have IMAP enabled too currently, and that is a very small amount of users because IMAP is an alpha product, and also because the UserNotification change has a guard to make sure it is not used if IMAP is enabled for the group. The existing IMAP tests work, and I tested this functionality by manually POSTing replies to the SMTP address into my local discourse. There will probably be more work needed on this, but it needs to be tested further in a real hosted environment to continue.
2021-06-03 12:47:32 +08:00
end
end
context "when a group forwards an email to its inbox" do
before do
group.update!(
email_username: "team@somesmtpaddress.com",
incoming_email: "team@somesmtpaddress.com|support+team@bar.com",
smtp_server: "smtp.test.com",
smtp_port: 587,
smtp_ssl: true,
smtp_enabled: true,
)
end
it "does not use the team's address as the from_address; it uses the original sender address" do
process(:forwarded_by_group_to_inbox)
topic = Topic.last
expect(topic.incoming_email.first.to_addresses).to include("support+team@bar.com")
expect(topic.incoming_email.first.from_address).to eq("fred@bedrock.com")
end
context "with forwarded emails behaviour set to create replies" do
before { SiteSetting.forwarded_emails_behaviour = "create_replies" }
it "does not use the team's address as the from_address; it uses the original sender address" do
process(:forwarded_by_group_to_inbox)
topic = Topic.last
expect(topic.incoming_email.first.to_addresses).to include("support+team@bar.com")
expect(topic.incoming_email.first.from_address).to eq("fred@bedrock.com")
end
it "does not say the email was forwarded by the original sender, it says the email is forwarded by the group" do
expect { process(:forwarded_by_group_to_inbox) }.to change {
User.where(staged: true).count
}.by(4)
topic = Topic.last
forwarded_small_post = topic.ordered_posts.last
expect(forwarded_small_post.action_code).to eq("forwarded")
expect(forwarded_small_post.user).to eq(User.find_by_email("team@somesmtpaddress.com"))
end
it "keeps track of the cc addresses of the forwarded email and creates staged users for them" do
expect { process(:forwarded_by_group_to_inbox) }.to change {
User.where(staged: true).count
}.by(4)
topic = Topic.last
cc_user1 = User.find_by_email("terry@ccland.com")
cc_user2 = User.find_by_email("don@ccland.com")
fred_user = User.find_by_email("fred@bedrock.com")
team_user = User.find_by_email("team@somesmtpaddress.com")
expect(topic.incoming_email.first.cc_addresses).to eq("terry@ccland.com;don@ccland.com")
expect(topic.topic_allowed_users.pluck(:user_id)).to match_array(
[fred_user.id, team_user.id, cc_user1.id, cc_user2.id],
)
end
it "keeps track of the cc addresses of the final forwarded email as well" do
expect { process(:forwarded_by_group_to_inbox_double_cc) }.to change {
User.where(staged: true).count
}.by(5)
topic = Topic.last
cc_user1 = User.find_by_email("terry@ccland.com")
cc_user2 = User.find_by_email("don@ccland.com")
fred_user = User.find_by_email("fred@bedrock.com")
team_user = User.find_by_email("team@somesmtpaddress.com")
someother_user = User.find_by_email("someotherparty@test.com")
expect(topic.incoming_email.first.cc_addresses).to eq(
"someotherparty@test.com;terry@ccland.com;don@ccland.com",
)
expect(topic.topic_allowed_users.pluck(:user_id)).to match_array(
[fred_user.id, team_user.id, someother_user.id, cc_user1.id, cc_user2.id],
)
end
context "when staged user for the team email already exists" do
let!(:staged_team_user) do
User.create!(
email: "team@somesmtpaddress.com",
username: UserNameSuggester.suggest("team@somesmtpaddress.com"),
name: "team teamson",
staged: true,
)
end
it "uses that and does not create another staged user" do
expect { process(:forwarded_by_group_to_inbox) }.to change {
User.where(staged: true).count
}.by(3)
topic = Topic.last
forwarded_small_post = topic.ordered_posts.last
expect(forwarded_small_post.action_code).to eq("forwarded")
expect(forwarded_small_post.user).to eq(staged_team_user)
end
end
end
end
context "when emailing a group by email_username and following reply flow" do
let!(:original_inbound_email_topic) do
group.update!(
email_username: "team@somesmtpaddress.com",
incoming_email: "team@somesmtpaddress.com|suppor+team@bar.com",
smtp_server: "smtp.test.com",
smtp_port: 587,
smtp_ssl: true,
smtp_enabled: true,
)
process(:email_to_group_email_username_1)
Topic.last
end
fab!(:user_in_group) do
u = Fabricate(:user)
Fabricate(:group_user, user: u, group: group)
u
end
before do
NotificationEmailer.enable
SiteSetting.disallow_reply_by_email_after_days = 10_000
Jobs.run_immediately!
end
def reply_as_group_user
group_post =
PostCreator.create(
user_in_group,
raw: "Thanks for your request. Please try to restart.",
topic_id: original_inbound_email_topic.id,
)
email_log = EmailLog.last
[email_log, group_post]
end
it "the inbound processed email creates an incoming email and topic record correctly, and adds the group to the topic" do
incoming = IncomingEmail.find_by(topic: original_inbound_email_topic)
user = User.find_by_email("two@foo.com")
expect(original_inbound_email_topic.topic_allowed_users.first.user_id).to eq(user.id)
expect(original_inbound_email_topic.topic_allowed_groups.first.group_id).to eq(group.id)
expect(incoming.to_addresses).to eq("team@somesmtpaddress.com")
expect(incoming.from_address).to eq("two@foo.com")
expect(incoming.message_id).to eq("u4w8c9r4y984yh98r3h69873@foo.bar.mail")
end
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
FEATURE: Overhaul email threading (#17996) See https://meta.discourse.org/t/discourse-email-messages-are-incorrectly-threaded/233499 for thorough reasoning. This commit changes how we generate Message-IDs and do email threading for emails sent from Discourse. The main changes are as follows: * Introduce an outbound_message_id column on Post that is either a) filled with a Discourse-generated Message-ID the first time that post is used for an outbound email or b) filled with an original Message-ID from an external mail client or service if the post was created from an incoming email. * Change Discourse-generated Message-IDs to be more consistent and static, in the format `discourse/post/:post_id@:host` * Do not send References or In-Reply-To headers for emails sent for the OP of topics. * Make sure that In-Reply-To is filled with either a) the OP's Message-ID if the post is not a direct reply or b) the parent post's Message-ID * Make sure that In-Reply-To has all referenced post's Message-IDs * Make sure that References is filled with a chain of Message-IDs from the OP down to the parent post of the new post. We also are keeping X-Discourse-Post-Id and X-Discourse-Topic-Id, headers that we previously removed, for easier visual debugging of outbound emails. Finally, we backfill the `outbound_message_id` for posts that have a linked `IncomingEmail` record, using the `message_id` of that record. We do not need to do that for posts that don't have an incoming email since they are backfilled at runtime if `outbound_message_id` is missing.
2022-09-26 07:14:24 +08:00
expect(email_log.message_id).to eq("discourse/post/#{group_post.id}@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)
expect(IncomingEmail.exists?(post_id: group_post.id)).to eq(false)
end
it "processes a reply from the OP user to the group SMTP username, linking the reply_to_post_number correctly by
matching in_reply_to to the email log" do
email_log, group_post = reply_as_group_user
reply_email = email(:email_to_group_email_username_2)
reply_email.gsub!("MESSAGE_ID_REPLY_TO", email_log.message_id)
expect do Email::Receiver.new(reply_email).process! end.to not_change {
Topic.count
}.and change { Post.count }.by(1)
reply_post = Post.last
expect(reply_post.reply_to_user).to eq(user_in_group)
expect(reply_post.reply_to_post_number).to eq(group_post.post_number)
end
it "processes the reply from the user as a brand new topic if they have replied from a different address (e.g. auto forward) and allow_unknown_sender_topic_replies is disabled" do
email_log, group_post = reply_as_group_user
reply_email = email(:email_to_group_email_username_2_as_unknown_sender)
reply_email.gsub!("MESSAGE_ID_REPLY_TO", email_log.message_id)
expect do Email::Receiver.new(reply_email).process! end.to change { Topic.count }.by(
1,
).and change { Post.count }.by(1)
reply_post = Post.last
expect(reply_post.topic_id).not_to eq(original_inbound_email_topic.id)
end
it "processes the reply from the user as a reply if they have replied from a different address (e.g. auto forward) and allow_unknown_sender_topic_replies is enabled" do
group.update!(allow_unknown_sender_topic_replies: true)
email_log, group_post = reply_as_group_user
reply_email = email(:email_to_group_email_username_2_as_unknown_sender)
reply_email.gsub!("MESSAGE_ID_REPLY_TO", email_log.message_id)
expect do Email::Receiver.new(reply_email).process! end.to not_change {
Topic.count
}.and change { Post.count }.by(1)
reply_post = Post.last
expect(reply_post.topic_id).to eq(original_inbound_email_topic.id)
end
it "creates a new topic with a reference back to the original if replying to a too old topic" do
SiteSetting.disallow_reply_by_email_after_days = 2
email_log, group_post = reply_as_group_user
group_post.update(created_at: 10.days.ago)
group_post.topic.update(created_at: 10.days.ago)
reply_email = email(:email_to_group_email_username_2)
reply_email.gsub!("MESSAGE_ID_REPLY_TO", email_log.message_id)
expect do Email::Receiver.new(reply_email).process! end.to change { Topic.count }.by(
1,
).and change { Post.count }.by(1)
reply_post = Post.last
new_topic = Topic.last
expect(reply_post.topic).to eq(new_topic)
expect(reply_post.raw).to include(
I18n.t(
"emails.incoming.continuing_old_discussion",
url: group_post.topic.url,
title: group_post.topic.title,
count: SiteSetting.disallow_reply_by_email_after_days,
),
)
end
end
context "when message sent to a group has no key and find_related_post_with_key is enabled" do
let!(:topic) do
process(:email_reply_1)
Topic.last
end
it "creates a reply when the sender and referenced message id are known" do
expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1).and not_change {
Topic.count
}
end
it "creates a new topic when the sender is not known and the group does not allow unknown senders to reply to topics" do
IncomingEmail.where(message_id: "34@foo.bar.mail").update(cc_addresses: "three@foo.com")
group.update(allow_unknown_sender_topic_replies: false)
expect { process(:email_reply_2) }.to not_change { topic.posts.count }.and change {
Topic.count
}.by(1)
end
it "creates a new topic when the referenced message id is not known" do
IncomingEmail.where(message_id: "34@foo.bar.mail").update(message_id: "99@foo.bar.mail")
expect { process(:email_reply_2) }.to not_change { topic.posts.count }.and change {
Topic.count
}.by(1)
end
it "includes the sender on the topic when the message id is known, the sender is not known, and the group allows unknown senders to reply to topics" do
IncomingEmail.where(message_id: "34@foo.bar.mail").update(cc_addresses: "three@foo.com")
group.update(allow_unknown_sender_topic_replies: true)
expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1).and not_change {
Topic.count
}
end
context "when the sender is not in the topic allowed users" do
before do
user = User.find_by_email("two@foo.com")
topic.topic_allowed_users.find_by(user: user).destroy
end
it "adds them to the topic at the same time" do
IncomingEmail.where(message_id: "34@foo.bar.mail").update(cc_addresses: "three@foo.com")
group.update(allow_unknown_sender_topic_replies: true)
expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1).and not_change {
Topic.count
}
end
end
end
end
describe "new topic in a category" do
fab!(:category) do
Fabricate(
:category,
email_in: "category@bar.com|category@foo.com",
email_in_allow_strangers: false,
)
end
it "raises a StrangersNotAllowedError when 'email_in_allow_strangers' is disabled" do
expect { process(:new_user) }.to raise_error(Email::Receiver::StrangersNotAllowedError)
end
it "raises an InsufficientTrustLevelError when user's trust level isn't enough" do
Fabricate(:user, email: "existing@bar.com", trust_level: TrustLevel[3])
SiteSetting.email_in_allowed_groups = Group::AUTO_GROUPS[:trust_level_4]
expect { process(:existing_user) }.to raise_error(
Email::Receiver::InsufficientTrustLevelError,
)
end
it "works" do
handler_calls = 0
handler =
proc do |topic|
expect(topic.incoming_email_addresses).to contain_exactly(
"discourse@bar.com",
"category@foo.com",
)
handler_calls += 1
end
DiscourseEvent.on(:topic_created, &handler)
user =
Fabricate(
:user,
email: "existing@bar.com",
trust_level: SiteSetting.email_in_min_trust,
refresh_auto_groups: true,
)
group = Fabricate(:group)
group.add(user)
group.save
category.set_permissions(group => :create_post)
2019-06-04 16:40:10 +08:00
category.save!
# raises an InvalidAccess when the user doesn't have the privileges to create a topic
expect { process(:existing_user) }.to raise_error(Discourse::InvalidAccess)
category.update_columns(email_in_allow_strangers: true)
# allows new user to create a topic
expect { process(:new_user) }.to change(Topic, :count)
DiscourseEvent.off(:topic_created, &handler)
expect(handler_calls).to eq(1)
end
it "creates visible topic for ham" do
SiteSetting.email_in_spam_header = "none"
Fabricate(
:user,
email: "existing@bar.com",
trust_level: SiteSetting.email_in_min_trust,
refresh_auto_groups: true,
)
expect { process(:existing_user) }.to change { Topic.count }.by(1) # Topic created
topic = Topic.last
expect(topic.visible).to eq(true)
post = Post.last
expect(post.hidden).to eq(false)
expect(post.hidden_at).to eq(nil)
expect(post.hidden_reason_id).to eq(nil)
end
it "creates hidden topic for X-Spam-Flag" do
SiteSetting.email_in_spam_header = "X-Spam-Flag"
user =
Fabricate(
:user,
email: "existing@bar.com",
trust_level: SiteSetting.email_in_min_trust,
refresh_auto_groups: true,
)
expect { process(:spam_x_spam_flag) }.to change { ReviewableQueuedPost.count }.by(1)
expect(user.reload.silenced?).to be(true)
end
it "creates hidden topic for X-Spam-Status" do
SiteSetting.email_in_spam_header = "X-Spam-Status"
user =
Fabricate(
:user,
email: "existing@bar.com",
trust_level: SiteSetting.email_in_min_trust,
refresh_auto_groups: true,
)
expect { process(:spam_x_spam_status) }.to change { ReviewableQueuedPost.count }.by(1)
expect(user.reload.silenced?).to be(true)
end
it "creates hidden topic for X-SES-Spam-Verdict" do
SiteSetting.email_in_spam_header = "X-SES-Spam-Verdict"
user =
Fabricate(
:user,
email: "existing@bar.com",
trust_level: SiteSetting.email_in_min_trust,
refresh_auto_groups: true,
)
expect { process(:spam_x_ses_spam_verdict) }.to change { ReviewableQueuedPost.count }.by(1)
expect(user.reload.silenced?).to be(true)
end
it "creates hidden topic for failed Authentication-Results header" do
SiteSetting.email_in_authserv_id = "example.com"
user =
Fabricate(
:user,
email: "existing@bar.com",
trust_level: SiteSetting.email_in_min_trust,
refresh_auto_groups: true,
)
expect { process(:dmarc_fail) }.to change { ReviewableQueuedPost.count }.by(1)
expect(user.reload.silenced?).to be(false)
end
it "adds the 'elided' part of the original message when always_show_trimmed_content is enabled" do
SiteSetting.always_show_trimmed_content = true
Fabricate(
:user,
email: "existing@bar.com",
trust_level: SiteSetting.email_in_min_trust,
refresh_auto_groups: true,
)
expect { process(:forwarded_email_to_category) }.to change { Topic.count }.by(1) # Topic created
new_post, = Post.last
expect(new_post.raw).to include(
"Hi everyone, can you have a look at the email below?",
"<summary title='Show trimmed content'>&#183;&#183;&#183;</summary>",
"Discoursing much today?",
)
end
it "works when approving is enabled" do
SiteSetting.approve_unless_allowed_groups = Group::AUTO_GROUPS[:trust_level_4]
Fabricate(:user, email: "tl3@bar.com", trust_level: TrustLevel[3])
Fabricate(:user, email: "tl4@bar.com", trust_level: TrustLevel[4])
category.set_permissions(Group[:trust_level_4] => :full)
2019-06-04 16:40:10 +08:00
category.save!
expect { process(:tl3_user) }.to raise_error(Email::Receiver::InvalidPost)
expect { process(:tl4_user) }.to change(Topic, :count)
end
it "ignores by case-insensitive title" do
SiteSetting.ignore_by_title = "foo"
expect { process(:ignored) }.to_not change(Topic, :count)
end
2018-07-03 19:51:22 +08:00
it "associates email from a secondary address with user" do
user =
Fabricate(
:user,
trust_level: SiteSetting.email_in_min_trust,
user_emails: [Fabricate.build(:secondary_email, email: "existing@bar.com")],
refresh_auto_groups: true,
2018-07-03 19:51:22 +08:00
)
expect { process(:existing_user) }.to change(Topic, :count).by(1)
topic = Topic.last
expect(topic.posts.last.raw).to eq("Hey, this is a topic from an existing user ;)")
expect(topic.user).to eq(user)
end
end
describe "new topic in a category that allows strangers" do
fab!(:category) do
Fabricate(
:category,
email_in: "category@bar.com|category@foo.com",
email_in_allow_strangers: true,
)
end
it "lets an email in from a stranger" do
expect { process(:new_user) }.to change(Topic, :count)
end
it "lets an email in from a high-TL user" do
Fabricate(:user, email: "tl4@bar.com", trust_level: TrustLevel[4])
expect { process(:tl4_user) }.to change(Topic, :count)
end
it "fails on email from a low-TL user" do
SiteSetting.email_in_allowed_groups = Group::AUTO_GROUPS[:trust_level_4]
Fabricate(:user, email: "tl3@bar.com", trust_level: TrustLevel[3])
expect { process(:tl3_user) }.to raise_error(Email::Receiver::InsufficientTrustLevelError)
end
end
describe "#reply_by_email_address_regex" do
before do
SiteSetting.reply_by_email_address = nil
SiteSetting.alternative_reply_by_email_addresses = nil
end
it "it matches nothing if there is not reply_by_email_address" do
expect(Email::Receiver.reply_by_email_address_regex).to eq(/$a/)
end
it "uses 'reply_by_email_address' site setting" do
SiteSetting.reply_by_email_address = "foo+%{reply_key}@bar.com"
expect(Email::Receiver.reply_by_email_address_regex).to eq(/foo\+?(\h{32})?@bar\.com/)
end
it "uses 'alternative_reply_by_email_addresses' site setting" do
SiteSetting.alternative_reply_by_email_addresses = "alt.foo+%{reply_key}@bar.com"
expect(Email::Receiver.reply_by_email_address_regex).to eq(/alt\.foo\+?(\h{32})?@bar\.com/)
end
it "combines both 'reply_by_email' settings" do
SiteSetting.reply_by_email_address = "foo+%{reply_key}@bar.com"
SiteSetting.alternative_reply_by_email_addresses = "alt.foo+%{reply_key}@bar.com"
expect(Email::Receiver.reply_by_email_address_regex).to eq(
/foo\+?(\h{32})?@bar\.com|alt\.foo\+?(\h{32})?@bar\.com/,
)
end
end
describe "check_address" do
before { SiteSetting.reply_by_email_address = "foo+%{reply_key}@bar.com" }
it "returns nil when the key is invalid" do
expect(Email::Receiver.check_address("fake@fake.com")).to be_nil
expect(
Email::Receiver.check_address("foo+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com"),
).to be_nil
end
context "with a valid reply" do
it "returns the destination when the key is valid" do
post_reply_key = Fabricate(:post_reply_key, reply_key: "4f97315cc828096c9cb34c6f1a0d6fe8")
dest = Email::Receiver.check_address("foo+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com")
expect(dest).to eq(post_reply_key)
end
end
end
describe "staged users" do
before { SiteSetting.enable_staged_users = true }
shared_examples "does not create staged users" do |email_name, expected_exception|
it "does not create staged users" do
staged_user_count = User.where(staged: true).count
User.expects(:create).never
User.expects(:create!).never
if expected_exception
expect { process(email_name) }.to raise_error(expected_exception)
else
process(email_name)
end
expect(User.where(staged: true).count).to eq(staged_user_count)
end
end
shared_examples "cleans up staged users" do |email_name, expected_exception|
it "cleans up staged users" do
staged_user_count = User.where(staged: true).count
expect { process(email_name) }.to raise_error(expected_exception)
expect(User.where(staged: true).count).to eq(staged_user_count)
end
end
context "when email address is screened" do
before { ScreenedEmail.expects(:should_block?).with("screened@mail.com").returns(true) }
include_examples "does not create staged users",
:screened_email,
Email::Receiver::ScreenedEmailError
end
context "when the mail is auto generated" do
include_examples "does not create staged users",
:auto_generated_header,
Email::Receiver::AutoGeneratedEmailError
end
context "when email is a bounced email" do
include_examples "does not create staged users",
:bounced_email,
Email::Receiver::BouncedEmailError
end
context "when the body is blank" do
include_examples "does not create staged users",
:no_body,
Email::Receiver::NoBodyDetectedError
end
context "when unsubscribe via email is not allowed" do
include_examples "does not create staged users",
:unsubscribe_new_user,
Email::Receiver::UnsubscribeNotAllowed
end
context "when From email address is not on allowlist" do
before do
SiteSetting.allowed_email_domains = "example.com|bar.com"
Fabricate(:group, incoming_email: "some_group@bar.com")
end
include_examples "does not create staged users",
:blocklist_allowlist_email,
Email::Receiver::EmailNotAllowed
end
context "when From email address is on blocklist" do
before do
SiteSetting.blocked_email_domains = "email.com|mail.com"
Fabricate(:group, incoming_email: "some_group@bar.com")
end
include_examples "does not create staged users",
:blocklist_allowlist_email,
Email::Receiver::EmailNotAllowed
end
context "with blocklist and allowlist for To and Cc" do
before { Fabricate(:group, incoming_email: "some_group@bar.com") }
it "does not create staged users for email addresses not on allowlist" do
SiteSetting.allowed_email_domains = "mail.com|example.com"
process(:blocklist_allowlist_email)
expect(User.find_by_email("alice@foo.com")).to be_nil
expect(User.find_by_email("bob@foo.com")).to be_nil
expect(User.find_by_email("carol@example.com")).to be_present
end
it "does not create staged users for email addresses on blocklist" do
SiteSetting.blocked_email_domains = "email.com|foo.com"
process(:blocklist_allowlist_email)
expect(User.find_by_email("alice@foo.com")).to be_nil
expect(User.find_by_email("bob@foo.com")).to be_nil
expect(User.find_by_email("carol@example.com")).to be_present
end
end
context "when destinations aren't matching any of the incoming emails" do
include_examples "does not create staged users",
:bad_destinations,
Email::Receiver::BadDestinationAddress
end
context "when email is sent to category" do
context "when email is sent by a new user and category does not allow strangers" do
fab!(:category) do
Fabricate(:category, email_in: "category@foo.com", email_in_allow_strangers: false)
end
include_examples "does not create staged users",
:new_user,
Email::Receiver::StrangersNotAllowedError
end
context "when email has no date" do
fab!(:category) do
Fabricate(:category, email_in: "category@foo.com", email_in_allow_strangers: true)
end
it "includes the translated string in the error" do
expect { process(:no_date) }.to raise_error(Email::Receiver::InvalidPost).with_message(
I18n.t("system_messages.email_reject_invalid_post_specified.date_invalid"),
)
end
include_examples "does not create staged users", :no_date, Email::Receiver::InvalidPost
end
context "with forwarded emails behaviour set to quote" do
before { SiteSetting.forwarded_emails_behaviour = "quote" }
context "with a category which allows strangers" do
fab!(:category) do
Fabricate(:category, email_in: "team@bar.com", email_in_allow_strangers: true)
end
include_examples "creates topic with forwarded message as quote",
:category,
"team@bar.com"
end
context "with a category which doesn't allow strangers" do
fab!(:category) do
Fabricate(:category, email_in: "team@bar.com", email_in_allow_strangers: false)
end
include_examples "cleans up staged users",
:forwarded_email_1,
Email::Receiver::StrangersNotAllowedError
end
end
end
context "when email is a reply" do
let(:reply_key) { "4f97315cc828096c9cb34c6f1a0d6fe8" }
fab!(:category)
fab!(:user) { Fabricate(:user, email: "discourse@bar.com", refresh_auto_groups: true) }
fab!(:user2) { Fabricate(:user, email: "someone_else@bar.com") }
fab!(:topic) { create_topic(category: category, user: user) }
fab!(:post) { create_post(topic: topic, user: user) }
let!(:post_reply_key) do
Fabricate(:post_reply_key, reply_key: reply_key, user: user, post: post)
end
context "when the email address isn't matching the one we sent the notification to" do
include_examples "does not create staged users",
:reply_user_not_matching,
Email::Receiver::ReplyUserNotMatchingError
end
context "with forwarded emails behaviour set to create replies" do
before { SiteSetting.forwarded_emails_behaviour = "create_replies" }
context "when a reply contains a forwarded email" do
include_examples "does not create staged users", :reply_and_forwarded
end
context "with forwarded email to category that doesn't allow strangers" do
before { category.update!(email_in: "team@bar.com", email_in_allow_strangers: false) }
include_examples "cleans up staged users",
:forwarded_email_1,
Email::Receiver::StrangersNotAllowedError
end
end
end
context "when replying without key is allowed" do
fab!(:group) { Fabricate(:group, incoming_email: "team@bar.com") }
let!(:topic) do
SiteSetting.find_related_post_with_key = false
process(:email_reply_1)
Topic.last
end
context "when the topic was deleted" do
before { topic.update_columns(deleted_at: 1.day.ago) }
include_examples "cleans up staged users",
:email_reply_staged,
Email::Receiver::TopicNotFoundError
end
context "when the topic was closed" do
before { topic.update_columns(closed: true) }
include_examples "cleans up staged users",
:email_reply_staged,
Email::Receiver::TopicClosedError
end
context "when they aren't allowed to like a post" do
before { topic.update_columns(archived: true) }
include_examples "cleans up staged users",
:email_reply_like,
Email::Receiver::InvalidPostAction
end
end
it "does not remove the incoming email record when staged users are deleted" do
expect { process(:bad_destinations) }.to change { IncomingEmail.count }.and raise_error(
Email::Receiver::BadDestinationAddress,
)
expect(IncomingEmail.last.message_id).to eq("9@foo.bar.mail")
end
end
describe "mailman mirror" do
fab!(:category) { Fabricate(:mailinglist_mirror_category) }
it "uses 'from' email address" do
expect { process(:mailman_1) }.to change { Topic.count }
user = Topic.last.user
expect(user.email).to eq("some@one.com")
expect(user.name).to eq("Some One")
end
it "uses 'reply-to' email address" do
expect { process(:mailman_2) }.to change { Topic.count }
user = Topic.last.user
expect(user.email).to eq("some@one.com")
expect(user.name).to eq("Some")
end
it "uses 'x-mailfrom' email address and name from CC" do
expect { process(:mailman_3) }.to change { Topic.count }
user = Topic.last.user
expect(user.email).to eq("some@one.com")
expect(user.name).to eq("Some One")
end
it "uses 'x-original-from' email address" do
expect { process(:mailman_4) }.to change { Topic.count }
user = Topic.last.user
expect(user.email).to eq("some@one.com")
expect(user.name).to eq("Some")
end
end
describe "mailing list mirror" do
fab!(:category) { Fabricate(:mailinglist_mirror_category) }
before { SiteSetting.block_auto_generated_emails = true }
it "should allow creating topic even when email is autogenerated" do
expect { process(:mailinglist) }.to change { Topic.count }
expect(IncomingEmail.last.is_auto_generated).to eq(false)
end
it "should allow replying without reply key" do
process(:mailinglist)
topic = Topic.last
expect { process(:mailinglist_reply) }.to change { topic.posts.count }
end
it "should skip validations for staged users" do
Fabricate(:user, email: "alice@foo.com", staged: true)
expect { process(:mailinglist_short_message) }.to change { Topic.count }
end
it "should skip validations for regular users" do
Fabricate(:user, email: "alice@foo.com", refresh_auto_groups: true)
expect { process(:mailinglist_short_message) }.to change { Topic.count }
end
context "with read-only category" do
before do
category.set_permissions(everyone: :readonly)
2019-06-04 16:40:10 +08:00
category.save!
Fabricate(:user, email: "alice@foo.com", refresh_auto_groups: true)
Fabricate(:user, email: "bob@bar.com", refresh_auto_groups: true)
end
it "should allow creating topic within read-only category" do
expect { process(:mailinglist) }.to change { Topic.count }
end
it "should allow replying within read-only category" do
process(:mailinglist)
topic = Topic.last
expect { process(:mailinglist_reply) }.to change { topic.posts.count }
end
end
it "ignores unsubscribe email" do
SiteSetting.unsubscribe_via_email = true
Fabricate(:user, email: "alice@foo.com", refresh_auto_groups: true)
expect { process("mailinglist_unsubscribe") }.to_not change {
ActionMailer::Base.deliveries.count
}
end
it "ignores dmarc fails" do
expect { process("mailinglist_dmarc_fail") }.to change { Topic.count }
post = Topic.last.first_post
expect(post.hidden).to eq(false)
expect(post.hidden_reason_id).to be_nil
end
end
it "tries to fix unparsable email addresses in To and CC headers" do
expect { process(:unparsable_email_addresses) }.to raise_error(
Email::Receiver::BadDestinationAddress,
)
email = IncomingEmail.last
expect(email.to_addresses).to eq("foo@bar.com")
expect(email.cc_addresses).to eq("bob@example.com;carol@example.com")
end
describe "#select_body" do
let(:email) { <<~EMAIL }
MIME-Version: 1.0
Date: Tue, 01 Jan 2019 00:00:00 +0300
Subject: An email with whitespaces
From: Foo <foo@discourse.org>
To: bar@discourse.org
Content-Type: text/plain; charset="UTF-8"
This is a line that will be stripped
This is another line that will be stripped
This is a line that will not be touched.
This is another line that will not be touched.
* list
* sub-list
- list
- sub-list
+ list
+ sub-list
[code]
1.upto(10).each do |i|
puts i
end
```
# comment
[/code]
This is going to be stripped too.
```
1.upto(10).each do |i|
puts i
end
[/code]
# comment
```
This is going to be stripped too.
Bye!
EMAIL
let(:stripped_text) { <<~MD }
This is a line that will be stripped
This is another line that will be stripped
This is a line that will not be touched.
This is another line that will not be touched.
* list
* sub-list
- list
- sub-list
+ list
+ sub-list
[code]
1.upto(10).each do |i|
puts i
end
```
# comment
[/code]
This is going to be stripped too.
```
1.upto(10).each do |i|
puts i
end
[/code]
# comment
```
This is going to be stripped too.
Bye!
MD
it "strips lines if strip_incoming_email_lines is enabled" do
SiteSetting.strip_incoming_email_lines = true
receiver = Email::Receiver.new(email)
text, elided, format = receiver.select_body
expect(text).to eq(stripped_text)
end
it "works with empty mail body" do
SiteSetting.strip_incoming_email_lines = true
email = <<~EMAIL
Date: Tue, 01 Jan 2019 00:00:00 +0300
Subject: An email with whitespaces
From: Foo <foo@discourse.org>
To: bar@discourse.org
Content-Type: text/plain; charset="UTF-8"
--
my signature
EMAIL
receiver = Email::Receiver.new(email)
text, _elided, _format = receiver.select_body
expect(text).to be_blank
end
end
describe "replying to digest" do
fab!(:user)
fab!(:digest_message_id) { "7402d8ae-1c6e-44bc-9948-48e007839bcc@localhost" }
fab!(:email_log) do
Fabricate(
:email_log,
user: user,
email_type: "digest",
to_address: user.email,
message_id: digest_message_id,
)
end
let(:email) { <<~EMAIL }
MIME-Version: 1.0
Date: Tue, 01 Jan 2019 00:00:00 +0300
From: someone <#{user.email}>
To: Discourse <#{SiteSetting.notification_email}>
Message-ID: <CANtGPwC3ZmWSxnnEuJHfosbtc9d0-ZV02b_7KuyircDt4peDC2@mail.gmail.com>
In-Reply-To: <#{digest_message_id}>
Subject: Re: [Discourse] Summary
References: <#{digest_message_id}>
Content-Type: text/plain; charset="UTF-8"
hello there! I like the digest!
EMAIL
before { Jobs.run_immediately! }
it "returns a ReplyToDigestError" do
expect { Email::Receiver.new(email).process! }.to raise_error(
Email::Receiver::ReplyToDigestError,
)
end
end
describe "find_related_post" do
let(:user) { Fabricate(:user) }
let(:group) { Fabricate(:group, users: [user]) }
let(:email_1) { <<~EMAIL }
MIME-Version: 1.0
Date: Wed, 01 Jan 2019 12:00:00 +0200
Message-ID: <7aN1uwcokt2xkfG3iYrpKmiuVhy4w9b5@mail.gmail.com>
Subject: Lorem ipsum dolor sit amet
From: Dan Ungureanu <dan@discourse.org>
To: team-test@discourse.org
Content-Type: text/plain; charset="UTF-8"
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas
semper, erat tempor sodales commodo, mi diam tempus lorem, in vehicula
leo libero quis lacus. Nullam justo nunc, sagittis nec metus placerat,
auctor condimentum neque. Sed risus purus, fermentum eget purus
porttitor, finibus efficitur orci. Integer tempus mi nec odio
elementum pulvinar. Pellentesque sed fringilla nulla, ac mollis quam.
Vivamus semper lacinia scelerisque. Cras urna magna, porttitor nec
libero quis, congue viverra sapien. Nulla sodales ac tellus a
suscipit.
EMAIL
let(:post_2) do
incoming_email =
IncomingEmail.find_by(message_id: "7aN1uwcokt2xkfG3iYrpKmiuVhy4w9b5@mail.gmail.com")
PostCreator.create(
user,
raw:
"Vestibulum rutrum tortor vitae arcu varius, non vestibulum ipsum tempor. Integer nibh libero, dignissim eu velit vel, interdum posuere mi. Aliquam erat volutpat. Pellentesque id nulla ultricies, eleifend ipsum non, fringilla purus. Aliquam pretium dolor lobortis urna volutpat, vel consectetur arcu porta. In non erat quis nibh gravida pharetra consequat vel risus. Aliquam rutrum consectetur est ac posuere. Praesent mattis nunc risus, a molestie lectus accumsan porta.",
topic_id: incoming_email.topic_id,
)
end
let(:email_3) { <<~EMAIL }
MIME-Version: 1.0
Date: Wed, 01 Jan 2019 12:00:00 +0200
References: <7aN1uwcokt2xkfG3iYrpKmiuVhy4w9b5@mail.gmail.com> <topic/#{post_2.topic_id}/#{post_2.id}@test.localhost>
In-Reply-To: <topic/#{post_2.topic_id}/#{post_2.id}@test.localhost>
Message-ID: <w1vdxT8ebJjZQQp7XyDdEJaSscE9qRjr@mail.gmail.com>
Subject: Re: Lorem ipsum dolor sit amet
From: Dan Ungureanu <dan@discourse.org>
To: team-test@discourse.org
Content-Type: text/plain; charset="UTF-8"
Integer mattis suscipit facilisis. Ut ullamcorper libero at faucibus
sodales. Ut suscipit elit ac dui porta consequat. Suspendisse potenti.
Nam ut accumsan dui, eget commodo sapien. Etiam ultrices elementum
cursus. Vivamus et diam et orci lobortis porttitor. Aliquam
scelerisque ex a imperdiet ornare. Donec interdum laoreet posuere.
Nulla sagittis, velit id posuere sollicitudin, elit nunc laoreet
libero, vitae aliquet tortor eros at est. Donec vitae massa vehicula,
aliquet libero non, porttitor ipsum. Phasellus pellentesque sodales
lacus eu sagittis. Aliquam ut condimentum nisi. Nulla in placerat
felis. Sed pellentesque, massa auctor venenatis gravida, risus lorem
iaculis mi, at hendrerit nisi turpis sit amet metus. Nulla egestas
ante eget nisi luctus consectetur.
EMAIL
def receive(email_string)
Email::Receiver.new(email_string, destinations: [group]).process!
end
it "makes all posts in same topic" do
expect { receive(email_1) }.to change { Topic.count }.by(1).and change {
Post.where(post_type: Post.types[:regular]).count
}.by(1)
expect { post_2 }.to not_change { Topic.count }.and change {
Post.where(post_type: Post.types[:regular]).count
}.by(1)
expect { receive(email_3) }.to not_change { Topic.count }.and change {
Post.where(post_type: Post.types[:regular]).count
}.by(1)
end
end
it "fixes valid addresses in embedded emails" do
Fabricate(:group, incoming_email: "group-fwd@example.com")
process(:long_embedded_email_headers)
incoming_email = IncomingEmail.find_by(message_id: "example1@mail.gmail.com")
expect(incoming_email.cc_addresses).to include("a@example.com")
expect(incoming_email.cc_addresses).to include("c@example.com")
end
end