discourse/spec/components/email/receiver_spec.rb

1211 lines
48 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.

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)
Email::Receiver.new(email(email_name)).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 whitelist" do
SiteSetting.email_domains_whitelist = "example.com|bar.com"
expect { process(:blacklist_whitelist_email) }.to raise_error(Email::Receiver::EmailNotAllowed)
end
it "raises EmailNotAllowed when email address is on blacklist" do
SiteSetting.email_domains_blacklist = "email.com|mail.com"
expect { process(:blacklist_whitelist_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)
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" }
let(:user1) { user1 = Fabricate(:user) }
let(:user2) { 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" }
let!(: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 "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
it "automatically deactive users once they reach the 'bounce_score_threshold_deactivate' threshold" do
expect(user.active).to eq(true)
user.user_stat.bounce_score = SiteSetting.bounce_score_threshold_deactivate - 1
user.user_stat.save!
expect { process(:soft_bounce_via_verp) }.to raise_error(Email::Receiver::BouncedEmailError)
user.reload
email_log.reload
expect(email_log.bounced).to eq(true)
expect(user.active).to eq(false)
end
end
context "reply" do
let(:reply_key) { "4f97315cc828096c9cb34c6f1a0d6fe8" }
let(:category) { Fabricate(:category) }
let(:user) { Fabricate(:user, email: "discourse@bar.com") }
let(:topic) { create_topic(category: category, user: user) }
let(:post) { create_post(topic: topic) }
let!(:post_reply_key) do
Fabricate(:post_reply_key,
reply_key: reply_key,
user: user,
post: post
)
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
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 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
it "raises a TopicClosedError when the topic was closed" do
topic.update_columns(closed: true)
expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicClosedError)
end
it "does not raise TopicClosedError when performing a like action" do
topic.update_columns(closed: true)
expect { process(:like) }.to change(PostAction, :count)
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 "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 "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. Anf 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 whitelisted" do
SiteSetting.auto_generated_whitelist = "foo@bar.com|discourse@bar.com"
expect { process(:auto_generated_whitelisted) }.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 }.from(1).to(2)
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 }.from(1).to(2)
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 "supports attached images in TEXT part" do
SiteSetting.incoming_email_prefer_html = false
expect { process(:no_body_with_image) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to match(/<img/)
expect { process(:inline_image) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to match(/Before\s+<img.+>\s+After/)
end
it "supports attached images in HTML part" do
SiteSetting.incoming_email_prefer_html = true
expect { process(:inline_image) }.to change { topic.posts.count }
expect(topic.posts.last.raw).to match(/\*\*Before\*\*\s+<img.+>\s+\*After\*/)
end
it "supports attachments" do
SiteSetting.authorized_extensions = "txt|jpg"
expect { process(:attached_txt_file) }.to change { topic.posts.count }
post = topic.posts.last
expect(post.raw).to match(/\APlease find some text file attached\.\s+<a class='attachment' href='\/uploads\/default\/original\/.+?txt'>text\.txt<\/a> \(20 Bytes\)\z/)
expect(post.uploads.size).to eq 1
expect { process(:apple_mail_attachment) }.to change { topic.posts.count }
post = topic.posts.last
expect(post.raw).to match(/\APicture below\.\s+<img.+?src="\/uploads\/default\/original\/.+?jpeg" class="">\s+Picture above\.\z/)
expect(post.uploads.size).to eq 1
end
it "supports eml attachments" do
SiteSetting.authorized_extensions = "eml"
expect { process(:attached_eml_file) }.to change { topic.posts.count }
post = topic.posts.last
expect(post.raw).to match(/\APlease find the eml file attached\.\s+<a class='attachment' href='\/uploads\/default\/original\/.+?eml'>sample\.eml<\/a> \(193 Bytes\)\z/)
expect(post.uploads.size).to eq 1
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
expect(post.raw).to match(/\A\s+<a class='attachment' href='\/uploads\/default\/original\/.+?pdf'>discourse\.pdf<\/a> \(64 KB\)\z/)
expect(post.uploads.size).to eq 1
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
expect { process(:from_the_future) }.to change { topic.posts.count }
expect(topic.posts.last.created_at).to be_within(1.minute).of(DateTime.now)
end
it "accepts emails with wrong reply key if the system knows about the forwarded email" do
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 email_private_messages setting 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_private_messages: false)
expect { process(:reply_user_matching) }.to change { topic.posts.count }
user.reload
expect(user.user_option.email_private_messages).to eq(true)
end
end
context "new message to a group" do
let!(: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 "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)
expect(Post.last.raw).to match(/<a\sclass='attachment'[^>]*>discourse\.rb<\/a>/)
expect(Post.last.uploads.length).to eq 1
end
it "enables user's email_private_messages setting when user emails new topic to group" do
user = Fabricate(:user, email: "existing@bar.com")
user.user_option.update_columns(email_private_messages: false)
expect { process(:group_existing_user) }.to change(Topic, :count)
user.reload
expect(user.user_option.email_private_messages).to eq(true)
end
context "with forwarded emails enabled" do
before { SiteSetting.enable_forwarded_emails = true }
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.enable_forwarded_emails = true
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
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" do
IncomingEmail.where(message_id: '34@foo.bar.mail').update(cc_addresses: 'three@foo.com')
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
end
end
context "new topic in a category" do
let!(: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'
Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust)
expect { process(:spam_x_spam_flag) }.to change { Topic.count }.by(1) # Topic created
topic = Topic.last
expect(topic.visible).to eq(false)
post = Post.last
expect(post.hidden).to eq(true)
expect(post.hidden_at).not_to eq(nil)
expect(post.hidden_reason_id).to eq(Post.hidden_reasons[:email_spam_header_found])
end
it "creates hidden topic for X-Spam-Status" do
SiteSetting.email_in_spam_header = 'X-Spam-Status'
Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust)
expect { process(:spam_x_spam_status) }.to change { Topic.count }.by(1) # Topic created
topic = Topic.last
expect(topic.visible).to eq(false)
post = Post.last
expect(post.hidden).to eq(true)
expect(post.hidden_at).not_to eq(nil)
expect(post.hidden_reason_id).to eq(Post.hidden_reasons[:email_spam_header_found])
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
let!(: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 maches 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 be_present
expect(dest[:type]).to eq(:reply)
expect(dest[:obj]).to eq(post_reply_key)
end
end
end
context "staged users" do
before do
SiteSetting.enable_staged_users = true
end
shared_examples "no staged users" do |email_name, expected_exception|
it "does not create 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 "no staged users", :screened_email, Email::Receiver::ScreenedEmailError
end
context "when the mail is auto generated" do
include_examples "no staged users", :auto_generated_header, Email::Receiver::AutoGeneratedEmailError
end
context "when email is a bounced email" do
include_examples "no staged users", :bounced_email, Email::Receiver::BouncedEmailError
end
context "when the body is blank" do
include_examples "no staged users", :no_body, Email::Receiver::NoBodyDetectedError
end
context "when unsubscribe via email is not allowed" do
include_examples "no staged users", :unsubscribe_new_user, Email::Receiver::UnsubscribeNotAllowed
end
context "when From email address is not on whitelist" do
before do
SiteSetting.email_domains_whitelist = "example.com|bar.com"
end
include_examples "no staged users", :blacklist_whitelist_email, Email::Receiver::EmailNotAllowed
end
context "when From email address is on blacklist" do
before do
SiteSetting.email_domains_blacklist = "email.com|mail.com"
end
include_examples "no staged users", :blacklist_whitelist_email, Email::Receiver::EmailNotAllowed
end
context "blacklist and whitelist 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 whitelist" do
SiteSetting.email_domains_whitelist = "mail.com|example.com"
process(:blacklist_whitelist_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 blacklist" do
SiteSetting.email_domains_blacklist = "email.com|foo.com"
process(:blacklist_whitelist_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 "no 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
let!(:category) { Fabricate(:category, email_in: "category@foo.com", email_in_allow_strangers: false) }
include_examples "no staged users", :new_user, Email::Receiver::StrangersNotAllowedError
end
context "when email has no date" do
let!(:category) { Fabricate(:category, email_in: "category@foo.com", email_in_allow_strangers: true) }
include_examples "no staged users", :no_date, Email::Receiver::InvalidPost
end
end
context "email is a reply" do
let(:reply_key) { "4f97315cc828096c9cb34c6f1a0d6fe8" }
let(:category) { Fabricate(:category) }
let(:user) { Fabricate(:user, email: "discourse@bar.com") }
let(:topic) { create_topic(category: category, user: user) }
let(: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 "no staged users", :reply_user_not_matching, Email::Receiver::ReplyUserNotMatchingError
end
end
context "replying without key is allowed" do
let!(: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 "no 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 "no 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 "no 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
let!(: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
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
end