discourse/spec/components/email/receiver_spec.rb

2036 lines
78 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
require "rails_helper"
require "email/receiver"
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
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
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
it "raises a SilencedUserError when the sender has been silenced" do
Fabricate(:user, email: "silenced@bar.com", silenced_till: 1.year.from_now)
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, id: 424242)
post = Fabricate(:post, topic: topic, id: 123456)
user = Fabricate(:user, email: "discourse@bar.com")
expect { process(:old_destination) }.to raise_error(
Email::Receiver::BadDestinationAddress
)
IncomingEmail.destroy_all
post.update!(created_at: 3.days.ago)
expect { process(:old_destination) }.to raise_error(
Email::Receiver::OldDestinationError
)
SiteSetting.disallow_reply_by_email_after_days = 0
IncomingEmail.destroy_all
expect { process(:old_destination) }.to raise_error(
Email::Receiver::BadDestinationAddress
)
end
context "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) }
let(:user2) { Fabricate(:staged, email: email_address) }
let(:topic) { Fabricate(:topic, archetype: 'private_message', category_id: nil, user: user1, allowed_users: [user1, user2]) }
let(:post) { create_post(topic: topic, user: user1) }
before do
SiteSetting.enable_staged_users = true
SiteSetting.enable_whispers = true
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
it "when bounce with verp" do
SiteSetting.reply_by_email_address = "foo+%{reply_key}@discourse.org"
bounce_key = "14b08c855160d67f2e0c2f8ef36e251e"
create_post_reply_key(bounce_key)
Fabricate(:email_log, to_address: email_address, user: user2, bounce_key: bounce_key, post: post)
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
end
end
it "logs a blank error" do
Email::Receiver.any_instance.stubs(:process_internal).raises(RuntimeError, "")
process(:existing_user) rescue RuntimeError
expect(IncomingEmail.last.error).to eq("RuntimeError")
end
it "matches the correct user" do
user = Fabricate(:user)
email_log = Fabricate(:email_log, to_address: user.email, user: user, bounce_key: nil)
email, name = Email::Receiver.new(email(:existing_user)).parse_from_field
expect(email).to eq("existing@bar.com")
expect(name).to eq("Foo Bar")
end
it "strips null bytes from the subject" do
expect do
process(:null_byte_in_subject)
end.to raise_error(Email::Receiver::BadDestinationAddress)
end
context "bounces to VERP" do
let(:bounce_key) { "14b08c855160d67f2e0c2f8ef36e251e" }
let(:bounce_key_2) { "b542fb5a9bacda6d28cc061d18e4eb83" }
fab!(:user) { Fabricate(:user, email: "linux-admin@b-s-c.co.jp") }
let!(:email_log) { Fabricate(:email_log, to_address: user.email, user: user, bounce_key: bounce_key) }
let!(:email_log_2) { Fabricate(:email_log, to_address: user.email, user: user, bounce_key: bounce_key_2) }
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)
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)
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)
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
end
context "reply" do
let(:reply_key) { "4f97315cc828096c9cb34c6f1a0d6fe8" }
fab!(:category) { Fabricate(:category) }
fab!(:user) { Fabricate(:user, email: "discourse@bar.com") }
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
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)
end
context "a closed topic" do
before do
topic.update_columns(closed: true)
end
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
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 "works" do
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 ;)")
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
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
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.")
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 trnaslated '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 "handles invalid from header" do
expect { process(:invalid_from_1) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to eq("This email was sent with an invalid from header field.")
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 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)
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
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
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
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
end
it "handles inline reply" do
expect { process(:inline_reply) }.to change { topic.posts.count }
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
SiteSetting.incoming_email_prefer_html = false
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**
![#{upload.original_filename}|#{upload.width}x#{upload.height}](#{upload.short_url})
*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>
![logo.png|300x200](upload://qUm0DGR49PAZshIi7HxMd3cAlzn.png)
</details>
MD
end
it "supports attachments" do
SiteSetting.authorized_extensions = "txt|jpg"
expect { process(:attached_txt_file) }.to change { topic.posts.count }
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 }
post = topic.posts.last
upload = post.uploads.first
expect(post.raw).to eq(<<~MD.chomp)
Picture below.
![#{upload.original_filename}|#{upload.width}x#{upload.height}](#{upload.short_url})
Picture above.
MD
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 }
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")
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 }
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 }
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 }
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")
Fabricate(:incoming_email,
raw: <<~RAW,
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 }
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(<<~EOF.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]
EOF
)
end
end
context "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)
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
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"),
]
)
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"),
]
)
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)
expect(Topic.last.ordered_posts[-1].post_type).to eq(Post.types[:moderator_action])
end
describe "reply-to header" do
before do
SiteSetting.block_auto_generated_emails = false
end
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 do
SiteSetting.find_related_post_with_key = false
end
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
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 do
SiteSetting.forwarded_emails_behaviour = "quote"
end
include_examples "creates topic with forwarded message as quote", :group, "team@bar.com|meat@bar.com"
end
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 change { Topic.count }.by(0)
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 do
SiteSetting.forwarded_emails_behaviour = "create_replies"
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
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 "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 = 10000
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
expect(email_log.message_id).to eq("topic/#{original_inbound_email_topic.id}/#{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 change { Topic.count }.by(0).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 change { Topic.count }.by(0).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 change { Topic.count }.by(0)
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 change { topic.posts.count }.by(0).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 change { topic.posts.count }.by(0).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 change { Topic.count }.by(0)
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 change { Topic.count }.by(0)
end
end
end
end
context "new topic in a category" do
fab!(:category) { Fabricate(:category, email_in: "category@bar.com|category@foo.com", email_in_allow_strangers: false) }
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: 3)
SiteSetting.email_in_min_trust = 4
expect { process(:existing_user) }.to raise_error(Email::Receiver::InsufficientTrustLevelError)
end
it "works" do
user = Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust)
group = Fabricate(:group)
group.add(user)
group.save
category.set_permissions(group => :create_post)
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)
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)
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)
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)
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)
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)
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)
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_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)
category.save!
Group.refresh_automatic_group!(:trust_level_4)
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
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")
]
)
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
context "new topic in a category that allows strangers" do
fab!(:category) { Fabricate(:category, email_in: "category@bar.com|category@foo.com", email_in_allow_strangers: true) }
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_min_trust = 4
Fabricate(:user, email: "tl3@bar.com", trust_level: TrustLevel[3])
expect { process(:tl3_user) }.to raise_error(Email::Receiver::InsufficientTrustLevelError)
end
end
context "#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
context "check_address" do
before do
SiteSetting.reply_by_email_address = "foo+%{reply_key}@bar.com"
end
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
context "staged users" do
before do
SiteSetting.enable_staged_users = true
end
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 do
ScreenedEmail.expects(:should_block?).with("screened@mail.com").returns(true)
end
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 "blocklist and allowlist for To and Cc" do
before do
Fabricate(:group, incoming_email: "some_group@bar.com")
end
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) { Fabricate(:category, email_in: "category@foo.com", email_in_allow_strangers: false) }
include_examples "does not create staged users", :new_user, Email::Receiver::StrangersNotAllowedError
end
context "when email has no date" do
fab!(:category) { Fabricate(:category, email_in: "category@foo.com", email_in_allow_strangers: true) }
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 do
SiteSetting.forwarded_emails_behaviour = "quote"
end
context "with a category which allows strangers" do
fab!(:category) { Fabricate(:category, email_in: "team@bar.com", email_in_allow_strangers: true) }
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) { Fabricate(:category, 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 "email is a reply" do
let(:reply_key) { "4f97315cc828096c9cb34c6f1a0d6fe8" }
fab!(:category) { Fabricate(:category) }
fab!(:user) { Fabricate(:user, email: "discourse@bar.com") }
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 do
SiteSetting.forwarded_emails_behaviour = "create_replies"
end
context "when a reply contains a forwarded email" do
include_examples "does not create staged users", :reply_and_forwarded
end
context "forwarded email to category that doesn't allow strangers" do
before do
category.update!(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 "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 do
topic.update_columns(deleted_at: 1.day.ago)
end
include_examples "cleans up staged users", :email_reply_staged, Email::Receiver::TopicNotFoundError
end
context "when the topic was closed" do
before do
topic.update_columns(closed: true)
end
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 do
topic.update_columns(archived: true)
end
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
context "mailing list mirror" do
fab!(:category) { Fabricate(:mailinglist_mirror_category) }
before do
SiteSetting.block_auto_generated_emails = true
end
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")
expect { process(:mailinglist_short_message) }.to change { Topic.count }
end
context "read-only category" do
before do
category.set_permissions(everyone: :readonly)
category.save!
Fabricate(:user, email: "alice@foo.com")
Fabricate(:user, email: "bob@bar.com")
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")
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
context "#select_body" do
let(:email) {
<<~EOF
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!
EOF
}
let(:stripped_text) {
<<~EOF
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!
EOF
}
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 = <<~EOF
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
EOF
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) { Fabricate(:user) }
fab!(:digest_message_id) { "7402d8ae-1c6e-44bc-9948-48e007839bcc@localhost" }
fab!(:email_log) { Fabricate(:email_log,
user: user,
email_type: 'digest',
to_address: user.email,
message_id: digest_message_id
)}
let(:email) {
<<~EOF
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!
EOF
}
before do
Jobs.run_immediately!
end
it 'returns a ReplyToDigestError' do
expect { Email::Receiver.new(email).process! }.to raise_error(Email::Receiver::ReplyToDigestError)
end
end
context "find_related_post" do
let(:user) { Fabricate(:user) }
let(:group) { Fabricate(:group, users: [user]) }
let (:email_1) { <<~EOF
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.
EOF
}
let (:post_2) {
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
)
}
let (:email_3) { <<~EOF
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.
EOF
}
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 change { Topic.count }.by(0).and change { Post.where(post_type: Post.types[:regular]).count }.by(1)
expect { receive(email_3) }.to change { Topic.count }.by(0).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