# frozen_string_literal: true require "email/receiver" RSpec.describe Email::Receiver do before do SiteSetting.email_in = true SiteSetting.reply_by_email_address = "reply+%{reply_key}@bar.com" SiteSetting.alternative_reply_by_email_addresses = "alt+%{reply_key}@bar.com" end def process(email_name, opts = {}) Email::Receiver.new(email(email_name), opts).process! end it "raises an EmptyEmailError when 'mail_string' is blank" do expect { Email::Receiver.new(nil) }.to raise_error(Email::Receiver::EmptyEmailError) expect { Email::Receiver.new("") }.to raise_error(Email::Receiver::EmptyEmailError) end it "raises a ScreenedEmailError when email address is screened" do ScreenedEmail.expects(:should_block?).with("screened@mail.com").returns(true) expect { process(:screened_email) }.to raise_error(Email::Receiver::ScreenedEmailError) end it "raises EmailNotAllowed when email address is not on allowlist" do SiteSetting.allowed_email_domains = "example.com|bar.com" Fabricate(:group, incoming_email: "some_group@bar.com") expect { process(:blocklist_allowlist_email) }.to raise_error(Email::Receiver::EmailNotAllowed) end it "raises EmailNotAllowed when email address is on blocklist" do SiteSetting.blocked_email_domains = "email.com|mail.com" Fabricate(:group, incoming_email: "some_group@bar.com") expect { process(:blocklist_allowlist_email) }.to raise_error(Email::Receiver::EmailNotAllowed) end it "raises an UserNotFoundError when staged users are disabled" do 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) post = Fabricate(:post, topic: topic) user = Fabricate(:user, email: "discourse@bar.com") mail = email(:old_destination).gsub("424242", topic.id.to_s) expect { Email::Receiver.new(mail).process! }.to raise_error( Email::Receiver::BadDestinationAddress, ) IncomingEmail.destroy_all post.update!(created_at: 3.days.ago) expect { Email::Receiver.new(mail).process! }.to raise_error( Email::Receiver::OldDestinationError, ) expect(IncomingEmail.last.error).to eq("Email::Receiver::OldDestinationError") SiteSetting.disallow_reply_by_email_after_days = 0 IncomingEmail.destroy_all expect { Email::Receiver.new(mail).process! }.to raise_error( Email::Receiver::BadDestinationAddress, ) end describe "bounces" do it "raises a BouncerEmailError" do expect { process(:bounced_email) }.to raise_error(Email::Receiver::BouncedEmailError) expect(IncomingEmail.last.is_bounce).to eq(true) expect { process(:bounced_email_multiple_status_codes) }.to raise_error( Email::Receiver::BouncedEmailError, ) expect(IncomingEmail.last.is_bounce).to eq(true) end describe "creating whisper post in PMs for staged users" do let(:email_address) { "linux-admin@b-s-c.co.jp" } fab!(:user1) { Fabricate(:user) } let(:user2) { Fabricate(:staged, email: email_address) } let(:topic) do Fabricate( :topic, archetype: "private_message", category_id: nil, user: user1, allowed_users: [user1, user2], ) end let(:post) { create_post(topic: topic, user: user1) } before do SiteSetting.enable_staged_users = true SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}" end def create_post_reply_key(value) Fabricate(:post_reply_key, reply_key: value, user: user2, post: post) end it "when bounce without verp" do create_post_reply_key("4f97315cc828096c9cb34c6f1a0d6fe8") expect { process(:bounced_email) }.to raise_error(Email::Receiver::BouncedEmailError) post = Post.last expect(post.whisper?).to eq(true) expect(post.raw).to eq( I18n.t( "system_messages.email_bounced", email: email_address, raw: "Your email bounced", ).strip, ) expect(IncomingEmail.last.is_bounce).to eq(true) end context "when bounce with verp" do let(:bounce_key) { "14b08c855160d67f2e0c2f8ef36e251e" } before do SiteSetting.reply_by_email_address = "foo+%{reply_key}@discourse.org" create_post_reply_key(bounce_key) Fabricate( :email_log, to_address: email_address, user: user2, bounce_key: bounce_key, post: post, ) end it "creates a post with the bounce error" do expect { process(:hard_bounce_via_verp) }.to raise_error( Email::Receiver::BouncedEmailError, ) post = Post.last expect(post.whisper?).to eq(true) expect(post.raw).to eq( I18n.t( "system_messages.email_bounced", email: email_address, raw: "Your email bounced", ).strip, ) expect(IncomingEmail.last.is_bounce).to eq(true) end it "updates the email log with the bounce error message" do expect { process(:hard_bounce_via_verp) }.to raise_error( Email::Receiver::BouncedEmailError, ) email_log = EmailLog.find_by(bounce_key: bounce_key) expect(email_log.bounced).to eq(true) expect(email_log.bounce_error_code).to eq("5.1.1") end end end end it "logs a blank error" do Email::Receiver.any_instance.stubs(:process_internal).raises(RuntimeError, "") begin process(:existing_user) rescue StandardError RuntimeError end expect(IncomingEmail.last.error).to eq("RuntimeError") end it "strips null bytes from the subject" do expect do process(:null_byte_in_subject) end.to raise_error( Email::Receiver::BadDestinationAddress, ) end describe "bounces to VERP" do let(:bounce_key) { "14b08c855160d67f2e0c2f8ef36e251e" } let(:bounce_key_2) { "b542fb5a9bacda6d28cc061d18e4eb83" } fab!(:user) { Fabricate(:user, email: "linux-admin@b-s-c.co.jp") } let!(:email_log) do Fabricate(:email_log, to_address: user.email, user: user, bounce_key: bounce_key) end let!(:email_log_2) do Fabricate(:email_log, to_address: user.email, user: user, bounce_key: bounce_key_2) end 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 describe "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 "with a closed topic" do before { topic.update_columns(closed: true) } it "raises a TopicClosedError when the topic was closed" do expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicClosedError) end it "Can watch topics via the watch command" do # TODO support other locales as well, the tricky thing is that these string live in # client.yml not on server yml so it is a bit tricky to find topic.update_columns(closed: true) process(:watch) expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:watching]) end it "Can mute topics via the mute command" do process(:mute) expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:muted]) end it "can track a topic via the track command" do process(:track) expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:tracking]) end end it "raises an InvalidPost when there was an error while creating the post" do expect { process(:too_small) }.to raise_error(Email::Receiver::TooShortPost) end it "raises an InvalidPost when there are too may mentions" do SiteSetting.max_mentions_per_post = 1 Fabricate(:user, username: "user1") Fabricate(:user, username: "user2") expect { process(:too_many_mentions) }.to raise_error(Email::Receiver::InvalidPost) end it "raises an InvalidPostAction when they aren't allowed to like a post" do topic.update_columns(archived: true) expect { process(:like) }.to raise_error(Email::Receiver::InvalidPostAction) end it "creates a new reply post" do handler_calls = 0 handler = proc { |_| handler_calls += 1 } DiscourseEvent.on(:topic_created, &handler) expect { process(:text_reply) }.to change { topic.posts.count } expect(topic.posts.last.raw).to eq( "This is a text reply :)\n\nEmail parsing should not break because of a UTF-8 character: ’", ) expect(topic.posts.last.via_email).to eq(true) expect(topic.posts.last.cooked).not_to match(/<br/) expect { process(:html_reply) }.to change { topic.posts.count } expect(topic.posts.last.raw).to eq("This is a **HTML** reply ;)") DiscourseEvent.off(:topic_created, &handler) expect(handler_calls).to eq(0) end it "stores the created_via source against the incoming email" do process(:text_reply, source: :handle_mail) expect(IncomingEmail.last.created_via).to eq(IncomingEmail.created_via_types[:handle_mail]) process(:text_and_html_reply, source: :imap) expect(IncomingEmail.last.created_via).to eq(IncomingEmail.created_via_types[:imap]) end it "stores the message_id of the incoming email against the post as outbound_message_id" do expect { process(:text_reply, source: :handle_mail) }.to change(Post, :count) message_id = IncomingEmail.last.message_id expect(Post.last.outbound_message_id).to eq(message_id) end it "automatically elides gmail quotes" do SiteSetting.always_show_trimmed_content = true expect { process(:gmail_html_reply) }.to change { topic.posts.count } expect(topic.posts.last.raw).to eq( "This is a **GMAIL** reply ;)\n\n<details class='elided'>\n<summary title='Show trimmed content'>···</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 translated 'Previous Replies' marker" do expect { process(:previous_replies_de) }.to change { topic.posts.count } expect(topic.posts.last.raw).to eq( "This will not include the previous discussion that is present in this email.", ) end it "removes the 'type reply above' marker" do expect { process(:reply_above) }.to change { topic.posts.count } expect(topic.posts.last.raw).to eq( "This will not include the previous discussion that is present in this email.", ) end it "removes the translated 'Previous Replies' marker" do expect { process(:reply_above_de) }.to change { topic.posts.count } expect(topic.posts.last.raw).to eq( "This will not include the previous discussion that is present in this email.", ) end it "handles multiple paragraphs" do expect { process(:paragraphs) }.to change { topic.posts.count } expect(topic.posts.last.raw).to eq( "Do you like liquorice?\n\nI really like them. One could even say that I am *addicted* to liquorice. And if\nyou can mix it up with some anise, then I'm in heaven ;)", ) end it "raises a NoSenderDetectedError when the From header can't be parsed" do expect { process(:invalid_from_1) }.to raise_error(Email::Receiver::NoSenderDetectedError) end it "raises a NoSenderDetectedError when the From header doesn't contain an email address" do expect { process(:invalid_from_2) }.to raise_error(Email::Receiver::NoSenderDetectedError) end it "doesn't raise an AutoGeneratedEmailError due to an X-Auto-Response-Suppress header" do expect { process(:quirks_exchange_xars) }.to change { topic.posts.count } end it "doesn't raise an AutoGeneratedEmailError when the mail is auto generated but is allowlisted" do SiteSetting.auto_generated_allowlist = "foo@bar.com|discourse@bar.com" expect { process(:auto_generated_allowlisted) }.to change { topic.posts.count } end it "doesn't raise an AutoGeneratedEmailError when block_auto_generated_emails is disabled" do SiteSetting.block_auto_generated_emails = false expect { process(:auto_generated_unblocked) }.to change { topic.posts.count } end it "allows staged users to reply to a restricted category" do user.update_columns(staged: true) category.email_in = "category@bar.com" category.email_in_allow_strangers = true category.set_permissions(Group[:trust_level_4] => :full) 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'>···</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'>···</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** <img src="#{upload.short_url}" alt="内嵌图片 1"> *After* MD end it "gracefully handles malformed images in HTML part" do expect { process(:inline_image_2) }.to change { topic.posts.count } post = topic.posts.last upload = post.uploads.last expect(post.raw).to eq(<<~MD.chomp) [image:#{"0" * 5000} ![#{upload.original_filename}|#{upload.width}x#{upload.height}](#{upload.short_url}) MD end it "supports attached images in signature" do SiteSetting.incoming_email_prefer_html = true SiteSetting.always_show_trimmed_content = true expect { process(:body_with_image) }.to change { topic.posts.count } post = topic.posts.last upload = post.uploads.last expect(post.raw).to eq(<<~MD.chomp) This is a **GMAIL** reply ;) <details class='elided'> <summary title='Show trimmed content'>···</summary> <img src="upload://qUm0DGR49PAZshIi7HxMd3cAlzn.png" width="300" height="200"> </details> MD end it "supports attachments" do SiteSetting.authorized_extensions = "txt|jpg" expect { process(:attached_txt_file) }.to change { topic.posts.count } 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. <img apple-inline="yes" id="06C04C58-783E-4753-9B6B-D57403903060" src="#{upload.short_url}" class=""> 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(<<~RAW.chomp) @team, can you have a look at this email below? [quote] From: Some One <some@one\\.com> To: Ba Bar <ba@bar\\.com> Date: Mon, 1 Dec 2016 00:13:37 \\+0100 Subject: Discoursing much? Hello Ba Bar, Discoursing much today? XoXo [/quote] RAW end end describe "new message to a group" do fab!(:group) { Fabricate(:group, incoming_email: "team@bar.com|meat@bar.com") } it "handles encoded display names" do expect { process(:encoded_display_name) }.to change(Topic, :count) topic = Topic.last expect(topic.title).to eq("I need help") expect(topic.private_message?).to eq(true) expect(topic.allowed_groups).to include(group) user = topic.user expect(user.staged).to eq(true) expect(user.username).to eq("random.name") expect(user.name).to eq("Случайная Имя") end it "handles email with no subject" do expect { process(:no_subject) }.to change(Topic, :count) 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).by(1).and change(User, :count).by(1) expect(Topic.last.ordered_posts[-1].post_type).to eq(Post.types[:moderator_action]) end it "cap the number of staged users existing per email" do Fabricate(:user, email: "discourse@bar.com", staged: true) # from Fabricate(:user, email: "someone@else.com", staged: true) # to SiteSetting.maximum_staged_users_per_email = 1 expect { process(:cc) }.to change(Topic, :count).and not_change(User, :count) expect(Topic.last.ordered_posts[-1].post_type).to eq(Post.types[:moderator_action]) end it "rejects messages with too many recipients" do SiteSetting.maximum_recipients_per_new_group_email = 3 expect { process(:cc) }.to raise_error(Email::Receiver::TooManyRecipientsError) end it "uses the incoming_email message-id as the new post's outbound_message_id" do expect { process(:cc) }.to change(Topic, :count) message_id = IncomingEmail.last.message_id expect(Topic.last.first_post.outbound_message_id).to eq(message_id) end describe "reply-to header" do before { SiteSetting.block_auto_generated_emails = false } it "extracts address and uses it for comparison" do expect { process(:reply_to_whitespaces) }.to change(Topic, :count).by(1) user = User.last incoming = IncomingEmail.find_by(message_id: "TXULO4v6YU0TzeL2buFAJNU2MK21c7t4@example.com") topic = incoming.topic expect(incoming.from_address).to eq("johndoe@example.com") expect(user.email).to eq("johndoe@example.com") end it "handles emails where there is a Reply-To address, using that instead of the from address, if X-Original-From is present" do expect { process(:reply_to_different_to_from) }.to change(Topic, :count).by(1) user = User.last incoming = IncomingEmail.find_by(message_id: "3848c3m98r439c348mc349@test.mailinglist.com") topic = incoming.topic expect(incoming.from_address).to eq("arthurmorgan@reddeadtest.com") expect(user.email).to eq("arthurmorgan@reddeadtest.com") end it "allows for quotes around the display name of the Reply-To address" do expect { process(:reply_to_different_to_from_quoted_display_name) }.to change( Topic, :count, ).by(1) user = User.last incoming = IncomingEmail.find_by(message_id: "3848c3m98r439c348mc349@test.mailinglist.com") topic = incoming.topic expect(incoming.from_address).to eq("johnmarston@reddeadtest.com") expect(user.email).to eq("johnmarston@reddeadtest.com") end it "does not use the reply-to address if an X-Original-From header is not present" do expect { process(:reply_to_different_to_from_no_x_original) }.to change(Topic, :count).by(1) user = User.last incoming = IncomingEmail.find_by(message_id: "3848c3m98r439c348mc349@test.mailinglist.com") topic = incoming.topic expect(incoming.from_address).to eq("westernsupport@test.mailinglist.com") expect(user.email).to eq("westernsupport@test.mailinglist.com") end it "does not use the reply-to address if the X-Original-From header is different from the reply-to address" do expect { process(:reply_to_different_to_from_x_original_different) }.to change( Topic, :count, ).by(1) user = User.last incoming = IncomingEmail.find_by(message_id: "3848c3m98r439c348mc349@test.mailinglist.com") topic = incoming.topic expect(incoming.from_address).to eq("westernsupport@test.mailinglist.com") expect(user.email).to eq("westernsupport@test.mailinglist.com") end end describe "when 'find_related_post_with_key' is disabled" do before { SiteSetting.find_related_post_with_key = false } it "associates email replies using both 'In-Reply-To' and 'References' headers" do expect { process(:email_reply_1) }.to change(Topic, :count).by(1) & change(Post, :count).by(3) topic = Topic.last ordered_posts = topic.ordered_posts expect(ordered_posts.first.raw).to eq("This is email reply **1**.") ordered_posts[1..-1].each do |post| expect(post.action_code).to eq("invited_user") expect(post.user.email).to eq("one@foo.com") expect(%w[two three].include?(post.custom_fields["action_code_who"])).to eq(true) end expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1) expect { process(:email_reply_3) }.to change { topic.posts.count }.by(1) ordered_posts[1..-1].each(&:trash!) expect { process(:email_reply_4) }.to change { topic.posts.count }.by(1) end describe "replying with various message-id formats using In-Reply-To header" do let!(:topic) do process(:email_reply_1) Topic.last end let!(:post) { Fabricate(:post, topic: topic) } def process_mail_with_message_id(message_id) mail_string = <<~EMAIL Return-Path: <two@foo.com> From: Two <two@foo.com> To: one@foo.com Subject: RE: Testing email threading Date: Fri, 15 Jan 2016 00:12:43 +0100 Message-ID: <44@foo.bar.mail> In-Reply-To: <#{message_id}> Mime-Version: 1.0 Content-Type: text/plain Content-Transfer-Encoding: 7bit This is email reply testing with Message-ID formats. EMAIL Email::Receiver.new(mail_string).process! end it "posts a reply using a message-id in the format topic/TOPIC_ID/POST_ID@HOST" do expect { process_mail_with_message_id("topic/#{topic.id}/#{post.id}@test.localhost") }.to change { Post.count }.by(1) expect(topic.reload.posts.last.raw).to include( "This is email reply testing with Message-ID formats", ) end it "posts a reply using a message-id in the format topic/TOPIC_ID@HOST" do expect { process_mail_with_message_id("topic/#{topic.id}@test.localhost") }.to change { Post.count }.by(1) expect(topic.reload.posts.last.raw).to include( "This is email reply testing with Message-ID formats", ) end it "posts a reply using a message-id in the format topic/TOPIC_ID/POST_ID.RANDOM_SUFFIX@HOST" do expect { process_mail_with_message_id("topic/#{topic.id}/#{post.id}.rjc3yr79834y@test.localhost") }.to change { Post.count }.by(1) expect(topic.reload.posts.last.raw).to include( "This is email reply testing with Message-ID formats", ) end it "posts a reply using a message-id in the format topic/TOPIC_ID.RANDOM_SUFFIX@HOST" do expect { process_mail_with_message_id( "topic/#{topic.id}/#{post.id}.x3487nxy877843x@test.localhost", ) }.to change { Post.count }.by(1) expect(topic.reload.posts.last.raw).to include( "This is email reply testing with Message-ID formats", ) end it "posts a reply using a message-id in the format discourse/post/POST_ID@HOST" do expect { process_mail_with_message_id("discourse/post/#{post.id}@test.localhost") }.to change { Post.count }.by(1) expect(topic.reload.posts.last.raw).to include( "This is email reply testing with Message-ID formats", ) end end end it "supports any kind of attachments when 'allow_all_attachments_for_group_messages' is enabled" do SiteSetting.allow_all_attachments_for_group_messages = true expect { process(:attached_rb_file) }.to change(Topic, :count) post = Topic.last.first_post upload = post.uploads.first expect(post.raw).to include( "[#{upload.original_filename}|attachment](#{upload.short_url}) (#{upload.filesize} Bytes)", ) end it "reenables user's PM email notifications when user emails new topic to group" do user = Fabricate(:user, email: "existing@bar.com") user.user_option.update_columns(email_messages_level: UserOption.email_level_types[:never]) expect { process(:group_existing_user) }.to change(Topic, :count) user.reload expect(user.user_option.email_messages_level).to eq(UserOption.email_level_types[:always]) end context "with forwarded emails behaviour set to create replies" do before do Fabricate(:group, incoming_email: "some_group@bar.com") SiteSetting.forwarded_emails_behaviour = "create_replies" end it "handles forwarded emails" do expect { process(:forwarded_email_1) }.to change(Topic, :count) forwarded_post, last_post = *Post.last(2) expect(forwarded_post.user.email).to eq("some@one.com") expect(last_post.user.email).to eq("ba@bar.com") expect(forwarded_post.raw).to match(/XoXo/) expect(last_post.raw).to match(/can you have a look at this email below/) expect(last_post.post_type).to eq(Post.types[:regular]) end it "handles weirdly forwarded emails" do group.add(Fabricate(:user, email: "ba@bar.com")) group.save SiteSetting.forwarded_emails_behaviour = "create_replies" expect { process(:forwarded_email_2) }.to change(Topic, :count) forwarded_post, last_post = *Post.last(2) expect(forwarded_post.user.email).to eq("some@one.com") expect(last_post.user.email).to eq("ba@bar.com") expect(forwarded_post.raw).to match(/XoXo/) expect(last_post.raw).to match(/can you have a look at this email below/) expect(last_post.post_type).to eq(Post.types[:whisper]) end # Who thought this was a good idea?! it "doesn't blow up with localized email headers" do expect { process(:forwarded_email_3) }.to change(Topic, :count) end it "adds a small action post to explain who forwarded the email when the sender didn't write anything" do expect { process(:forwarded_email_4) }.to change(Topic, :count) forwarded_post, last_post = *Post.last(2) expect(forwarded_post.user.email).to eq("some@one.com") expect(forwarded_post.raw).to match(/XoXo/) expect(last_post.user.email).to eq("ba@bar.com") expect(last_post.post_type).to eq(Post.types[:small_action]) expect(last_post.action_code).to eq("forwarded") end end context "with forwarded emails behaviour set to quote" do before { SiteSetting.forwarded_emails_behaviour = "quote" } include_examples "creates topic with forwarded message as quote", :group, "team@bar.com|meat@bar.com" end context "when a reply is sent to a group's email_username" do let!(:topic) do group.update(email_username: "team@somesmtpaddress.com") process(:email_reply_1) Topic.last end it "does not invite the group email_username as a staged user" do process(:email_reply_to_group_email_username) expect(User.find_by_email("team@somesmtpaddress.com")).to eq(nil) end it "creates the reply when the sender and referenced messsage id are known" do expect { process(:email_reply_to_group_email_username) }.to change { topic.posts.count }.by( 1, ).and not_change { Topic.count } end end context "when a group forwards an email to its inbox" do before do group.update!( email_username: "team@somesmtpaddress.com", incoming_email: "team@somesmtpaddress.com|support+team@bar.com", smtp_server: "smtp.test.com", smtp_port: 587, smtp_ssl: true, smtp_enabled: true, ) end it "does not use the team's address as the from_address; it uses the original sender address" do process(:forwarded_by_group_to_inbox) topic = Topic.last expect(topic.incoming_email.first.to_addresses).to include("support+team@bar.com") expect(topic.incoming_email.first.from_address).to eq("fred@bedrock.com") end context "with forwarded emails behaviour set to create replies" do before { SiteSetting.forwarded_emails_behaviour = "create_replies" } it "does not use the team's address as the from_address; it uses the original sender address" do process(:forwarded_by_group_to_inbox) topic = Topic.last expect(topic.incoming_email.first.to_addresses).to include("support+team@bar.com") expect(topic.incoming_email.first.from_address).to eq("fred@bedrock.com") end it "does not say the email was forwarded by the original sender, it says the email is forwarded by the group" do expect { process(:forwarded_by_group_to_inbox) }.to change { User.where(staged: true).count }.by(4) topic = Topic.last forwarded_small_post = topic.ordered_posts.last expect(forwarded_small_post.action_code).to eq("forwarded") expect(forwarded_small_post.user).to eq(User.find_by_email("team@somesmtpaddress.com")) end it "keeps track of the cc addresses of the forwarded email and creates staged users for them" do expect { process(:forwarded_by_group_to_inbox) }.to change { User.where(staged: true).count }.by(4) topic = Topic.last cc_user1 = User.find_by_email("terry@ccland.com") cc_user2 = User.find_by_email("don@ccland.com") fred_user = User.find_by_email("fred@bedrock.com") team_user = User.find_by_email("team@somesmtpaddress.com") expect(topic.incoming_email.first.cc_addresses).to eq("terry@ccland.com;don@ccland.com") expect(topic.topic_allowed_users.pluck(:user_id)).to match_array( [fred_user.id, team_user.id, cc_user1.id, cc_user2.id], ) end it "keeps track of the cc addresses of the final forwarded email as well" do expect { process(:forwarded_by_group_to_inbox_double_cc) }.to change { User.where(staged: true).count }.by(5) topic = Topic.last cc_user1 = User.find_by_email("terry@ccland.com") cc_user2 = User.find_by_email("don@ccland.com") fred_user = User.find_by_email("fred@bedrock.com") team_user = User.find_by_email("team@somesmtpaddress.com") someother_user = User.find_by_email("someotherparty@test.com") expect(topic.incoming_email.first.cc_addresses).to eq( "someotherparty@test.com;terry@ccland.com;don@ccland.com", ) expect(topic.topic_allowed_users.pluck(:user_id)).to match_array( [fred_user.id, team_user.id, someother_user.id, cc_user1.id, cc_user2.id], ) end context "when staged user for the team email already exists" do let!(:staged_team_user) do User.create!( email: "team@somesmtpaddress.com", username: UserNameSuggester.suggest("team@somesmtpaddress.com"), name: "team teamson", staged: true, ) end it "uses that and does not create another staged user" do expect { process(:forwarded_by_group_to_inbox) }.to change { User.where(staged: true).count }.by(3) topic = Topic.last forwarded_small_post = topic.ordered_posts.last expect(forwarded_small_post.action_code).to eq("forwarded") expect(forwarded_small_post.user).to eq(staged_team_user) end end end end context "when emailing a group by email_username and following reply flow" do let!(:original_inbound_email_topic) do group.update!( email_username: "team@somesmtpaddress.com", incoming_email: "team@somesmtpaddress.com|suppor+team@bar.com", smtp_server: "smtp.test.com", smtp_port: 587, smtp_ssl: true, smtp_enabled: true, ) process(:email_to_group_email_username_1) Topic.last end fab!(:user_in_group) do u = Fabricate(:user) Fabricate(:group_user, user: u, group: group) u end before do NotificationEmailer.enable SiteSetting.disallow_reply_by_email_after_days = 10_000 Jobs.run_immediately! end def reply_as_group_user group_post = PostCreator.create( user_in_group, raw: "Thanks for your request. Please try to restart.", topic_id: original_inbound_email_topic.id, ) email_log = EmailLog.last [email_log, group_post] end it "the inbound processed email creates an incoming email and topic record correctly, and adds the group to the topic" do incoming = IncomingEmail.find_by(topic: original_inbound_email_topic) user = User.find_by_email("two@foo.com") expect(original_inbound_email_topic.topic_allowed_users.first.user_id).to eq(user.id) expect(original_inbound_email_topic.topic_allowed_groups.first.group_id).to eq(group.id) expect(incoming.to_addresses).to eq("team@somesmtpaddress.com") expect(incoming.from_address).to eq("two@foo.com") expect(incoming.message_id).to eq("u4w8c9r4y984yh98r3h69873@foo.bar.mail") end it "creates an EmailLog when someone from the group replies, and does not create an IncomingEmail record for the reply" do email_log, group_post = reply_as_group_user expect(email_log.message_id).to eq("discourse/post/#{group_post.id}@test.localhost") expect(email_log.to_address).to eq("two@foo.com") expect(email_log.email_type).to eq("user_private_message") expect(email_log.post_id).to eq(group_post.id) expect(IncomingEmail.exists?(post_id: group_post.id)).to eq(false) end it "processes a reply from the OP user to the group SMTP username, linking the reply_to_post_number correctly by matching in_reply_to to the email log" do email_log, group_post = reply_as_group_user reply_email = email(:email_to_group_email_username_2) reply_email.gsub!("MESSAGE_ID_REPLY_TO", email_log.message_id) expect do Email::Receiver.new(reply_email).process! end.to not_change { Topic.count }.and change { Post.count }.by(1) reply_post = Post.last expect(reply_post.reply_to_user).to eq(user_in_group) expect(reply_post.reply_to_post_number).to eq(group_post.post_number) end it "processes the reply from the user as a brand new topic if they have replied from a different address (e.g. auto forward) and allow_unknown_sender_topic_replies is disabled" do email_log, group_post = reply_as_group_user reply_email = email(:email_to_group_email_username_2_as_unknown_sender) reply_email.gsub!("MESSAGE_ID_REPLY_TO", email_log.message_id) expect do Email::Receiver.new(reply_email).process! end.to change { Topic.count }.by( 1, ).and change { Post.count }.by(1) reply_post = Post.last expect(reply_post.topic_id).not_to eq(original_inbound_email_topic.id) end it "processes the reply from the user as a reply if they have replied from a different address (e.g. auto forward) and allow_unknown_sender_topic_replies is enabled" do group.update!(allow_unknown_sender_topic_replies: true) email_log, group_post = reply_as_group_user reply_email = email(:email_to_group_email_username_2_as_unknown_sender) reply_email.gsub!("MESSAGE_ID_REPLY_TO", email_log.message_id) expect do Email::Receiver.new(reply_email).process! end.to not_change { Topic.count }.and change { Post.count }.by(1) reply_post = Post.last expect(reply_post.topic_id).to eq(original_inbound_email_topic.id) end it "creates a new topic with a reference back to the original if replying to a too old topic" do SiteSetting.disallow_reply_by_email_after_days = 2 email_log, group_post = reply_as_group_user group_post.update(created_at: 10.days.ago) group_post.topic.update(created_at: 10.days.ago) reply_email = email(:email_to_group_email_username_2) reply_email.gsub!("MESSAGE_ID_REPLY_TO", email_log.message_id) expect do Email::Receiver.new(reply_email).process! end.to change { Topic.count }.by( 1, ).and change { Post.count }.by(1) reply_post = Post.last new_topic = Topic.last expect(reply_post.topic).to eq(new_topic) expect(reply_post.raw).to include( I18n.t( "emails.incoming.continuing_old_discussion", url: group_post.topic.url, title: group_post.topic.title, count: SiteSetting.disallow_reply_by_email_after_days, ), ) end end context "when message sent to a group has no key and find_related_post_with_key is enabled" do let!(:topic) do process(:email_reply_1) Topic.last end it "creates a reply when the sender and referenced message id are known" do expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1).and not_change { Topic.count } end it "creates a new topic when the sender is not known and the group does not allow unknown senders to reply to topics" do IncomingEmail.where(message_id: "34@foo.bar.mail").update(cc_addresses: "three@foo.com") group.update(allow_unknown_sender_topic_replies: false) expect { process(:email_reply_2) }.to not_change { topic.posts.count }.and change { Topic.count }.by(1) end it "creates a new topic when the referenced message id is not known" do IncomingEmail.where(message_id: "34@foo.bar.mail").update(message_id: "99@foo.bar.mail") expect { process(:email_reply_2) }.to not_change { topic.posts.count }.and change { Topic.count }.by(1) end it "includes the sender on the topic when the message id is known, the sender is not known, and the group allows unknown senders to reply to topics" do IncomingEmail.where(message_id: "34@foo.bar.mail").update(cc_addresses: "three@foo.com") group.update(allow_unknown_sender_topic_replies: true) expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1).and not_change { Topic.count } end context "when the sender is not in the topic allowed users" do before do user = User.find_by_email("two@foo.com") topic.topic_allowed_users.find_by(user: user).destroy end it "adds them to the topic at the same time" do IncomingEmail.where(message_id: "34@foo.bar.mail").update(cc_addresses: "three@foo.com") group.update(allow_unknown_sender_topic_replies: true) expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1).and not_change { Topic.count } end end end end describe "new topic in a category" do fab!(:category) do Fabricate( :category, email_in: "category@bar.com|category@foo.com", email_in_allow_strangers: false, ) end it "raises a StrangersNotAllowedError when 'email_in_allow_strangers' is disabled" do expect { process(:new_user) }.to raise_error(Email::Receiver::StrangersNotAllowedError) end it "raises an InsufficientTrustLevelError when user's trust level isn't enough" do Fabricate(:user, email: "existing@bar.com", trust_level: 3) SiteSetting.email_in_min_trust = 4 expect { process(:existing_user) }.to raise_error( Email::Receiver::InsufficientTrustLevelError, ) end it "works" do handler_calls = 0 handler = proc do |topic| expect(topic.incoming_email_addresses).to contain_exactly( "discourse@bar.com", "category@foo.com", ) handler_calls += 1 end DiscourseEvent.on(:topic_created, &handler) user = Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust) 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) DiscourseEvent.off(:topic_created, &handler) expect(handler_calls).to eq(1) end it "creates visible topic for ham" do SiteSetting.email_in_spam_header = "none" Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust) 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'>···</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 describe "new topic in a category that allows strangers" do fab!(:category) do Fabricate( :category, email_in: "category@bar.com|category@foo.com", email_in_allow_strangers: true, ) end it "lets an email in from a stranger" do expect { process(:new_user) }.to change(Topic, :count) end it "lets an email in from a high-TL user" do Fabricate(:user, email: "tl4@bar.com", trust_level: TrustLevel[4]) expect { process(:tl4_user) }.to change(Topic, :count) end it "fails on email from a low-TL user" do SiteSetting.email_in_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 describe "#reply_by_email_address_regex" do before do SiteSetting.reply_by_email_address = nil SiteSetting.alternative_reply_by_email_addresses = nil end it "it matches nothing if there is not reply_by_email_address" do expect(Email::Receiver.reply_by_email_address_regex).to eq(/$a/) end it "uses 'reply_by_email_address' site setting" do SiteSetting.reply_by_email_address = "foo+%{reply_key}@bar.com" expect(Email::Receiver.reply_by_email_address_regex).to eq(/foo\+?(\h{32})?@bar\.com/) end it "uses 'alternative_reply_by_email_addresses' site setting" do SiteSetting.alternative_reply_by_email_addresses = "alt.foo+%{reply_key}@bar.com" expect(Email::Receiver.reply_by_email_address_regex).to eq(/alt\.foo\+?(\h{32})?@bar\.com/) end it "combines both 'reply_by_email' settings" do SiteSetting.reply_by_email_address = "foo+%{reply_key}@bar.com" SiteSetting.alternative_reply_by_email_addresses = "alt.foo+%{reply_key}@bar.com" expect(Email::Receiver.reply_by_email_address_regex).to eq( /foo\+?(\h{32})?@bar\.com|alt\.foo\+?(\h{32})?@bar\.com/, ) end end describe "check_address" do before { SiteSetting.reply_by_email_address = "foo+%{reply_key}@bar.com" } it "returns nil when the key is invalid" do expect(Email::Receiver.check_address("fake@fake.com")).to be_nil expect( Email::Receiver.check_address("foo+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com"), ).to be_nil end context "with a valid reply" do it "returns the destination when the key is valid" do post_reply_key = Fabricate(:post_reply_key, reply_key: "4f97315cc828096c9cb34c6f1a0d6fe8") dest = Email::Receiver.check_address("foo+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com") expect(dest).to eq(post_reply_key) end end end describe "staged users" do before { SiteSetting.enable_staged_users = true } shared_examples "does not create staged users" do |email_name, expected_exception| it "does not create staged users" do staged_user_count = User.where(staged: true).count User.expects(:create).never User.expects(:create!).never if expected_exception expect { process(email_name) }.to raise_error(expected_exception) else process(email_name) end expect(User.where(staged: true).count).to eq(staged_user_count) end end shared_examples "cleans up staged users" do |email_name, expected_exception| it "cleans up staged users" do staged_user_count = User.where(staged: true).count expect { process(email_name) }.to raise_error(expected_exception) expect(User.where(staged: true).count).to eq(staged_user_count) end end context "when email address is screened" do before { ScreenedEmail.expects(:should_block?).with("screened@mail.com").returns(true) } include_examples "does not create staged users", :screened_email, Email::Receiver::ScreenedEmailError end context "when the mail is auto generated" do include_examples "does not create staged users", :auto_generated_header, Email::Receiver::AutoGeneratedEmailError end context "when email is a bounced email" do include_examples "does not create staged users", :bounced_email, Email::Receiver::BouncedEmailError end context "when the body is blank" do include_examples "does not create staged users", :no_body, Email::Receiver::NoBodyDetectedError end context "when unsubscribe via email is not allowed" do include_examples "does not create staged users", :unsubscribe_new_user, Email::Receiver::UnsubscribeNotAllowed end context "when From email address is not on allowlist" do before do SiteSetting.allowed_email_domains = "example.com|bar.com" Fabricate(:group, incoming_email: "some_group@bar.com") end include_examples "does not create staged users", :blocklist_allowlist_email, Email::Receiver::EmailNotAllowed end context "when From email address is on blocklist" do before do SiteSetting.blocked_email_domains = "email.com|mail.com" Fabricate(:group, incoming_email: "some_group@bar.com") end include_examples "does not create staged users", :blocklist_allowlist_email, Email::Receiver::EmailNotAllowed end context "with blocklist and allowlist for To and Cc" do before { Fabricate(:group, incoming_email: "some_group@bar.com") } it "does not create staged users for email addresses not on allowlist" do SiteSetting.allowed_email_domains = "mail.com|example.com" process(:blocklist_allowlist_email) expect(User.find_by_email("alice@foo.com")).to be_nil expect(User.find_by_email("bob@foo.com")).to be_nil expect(User.find_by_email("carol@example.com")).to be_present end it "does not create staged users for email addresses on blocklist" do SiteSetting.blocked_email_domains = "email.com|foo.com" process(:blocklist_allowlist_email) expect(User.find_by_email("alice@foo.com")).to be_nil expect(User.find_by_email("bob@foo.com")).to be_nil expect(User.find_by_email("carol@example.com")).to be_present end end context "when destinations aren't matching any of the incoming emails" do include_examples "does not create staged users", :bad_destinations, Email::Receiver::BadDestinationAddress end context "when email is sent to category" do context "when email is sent by a new user and category does not allow strangers" do fab!(:category) do Fabricate(:category, email_in: "category@foo.com", email_in_allow_strangers: false) end include_examples "does not create staged users", :new_user, Email::Receiver::StrangersNotAllowedError end context "when email has no date" do fab!(:category) do Fabricate(:category, email_in: "category@foo.com", email_in_allow_strangers: true) end it "includes the translated string in the error" do expect { process(:no_date) }.to raise_error(Email::Receiver::InvalidPost).with_message( I18n.t("system_messages.email_reject_invalid_post_specified.date_invalid"), ) end include_examples "does not create staged users", :no_date, Email::Receiver::InvalidPost end context "with forwarded emails behaviour set to quote" do before { SiteSetting.forwarded_emails_behaviour = "quote" } context "with a category which allows strangers" do fab!(:category) do Fabricate(:category, email_in: "team@bar.com", email_in_allow_strangers: true) end include_examples "creates topic with forwarded message as quote", :category, "team@bar.com" end context "with a category which doesn't allow strangers" do fab!(:category) do Fabricate(:category, email_in: "team@bar.com", email_in_allow_strangers: false) end include_examples "cleans up staged users", :forwarded_email_1, Email::Receiver::StrangersNotAllowedError end end end context "when email is a reply" do let(:reply_key) { "4f97315cc828096c9cb34c6f1a0d6fe8" } fab!(:category) { 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 { SiteSetting.forwarded_emails_behaviour = "create_replies" } context "when a reply contains a forwarded email" do include_examples "does not create staged users", :reply_and_forwarded end context "with forwarded email to category that doesn't allow strangers" do before { category.update!(email_in: "team@bar.com", email_in_allow_strangers: false) } include_examples "cleans up staged users", :forwarded_email_1, Email::Receiver::StrangersNotAllowedError end end end context "when replying without key is allowed" do fab!(:group) { Fabricate(:group, incoming_email: "team@bar.com") } let!(:topic) do SiteSetting.find_related_post_with_key = false process(:email_reply_1) Topic.last end context "when the topic was deleted" do before { topic.update_columns(deleted_at: 1.day.ago) } include_examples "cleans up staged users", :email_reply_staged, Email::Receiver::TopicNotFoundError end context "when the topic was closed" do before { topic.update_columns(closed: true) } include_examples "cleans up staged users", :email_reply_staged, Email::Receiver::TopicClosedError end context "when they aren't allowed to like a post" do before { topic.update_columns(archived: true) } include_examples "cleans up staged users", :email_reply_like, Email::Receiver::InvalidPostAction end end it "does not remove the incoming email record when staged users are deleted" do expect { process(:bad_destinations) }.to change { IncomingEmail.count }.and raise_error( Email::Receiver::BadDestinationAddress, ) expect(IncomingEmail.last.message_id).to eq("9@foo.bar.mail") end end describe "mailman mirror" do fab!(:category) { Fabricate(:mailinglist_mirror_category) } it "uses 'from' email address" do expect { process(:mailman_1) }.to change { Topic.count } user = Topic.last.user expect(user.email).to eq("some@one.com") expect(user.name).to eq("Some One") end it "uses 'reply-to' email address" do expect { process(:mailman_2) }.to change { Topic.count } user = Topic.last.user expect(user.email).to eq("some@one.com") expect(user.name).to eq("Some") end it "uses 'x-mailfrom' email address and name from CC" do expect { process(:mailman_3) }.to change { Topic.count } user = Topic.last.user expect(user.email).to eq("some@one.com") expect(user.name).to eq("Some One") end it "uses 'x-original-from' email address" do expect { process(:mailman_4) }.to change { Topic.count } user = Topic.last.user expect(user.email).to eq("some@one.com") expect(user.name).to eq("Some") end end describe "mailing list mirror" do fab!(:category) { Fabricate(:mailinglist_mirror_category) } before { SiteSetting.block_auto_generated_emails = true } it "should allow creating topic even when email is autogenerated" do expect { process(:mailinglist) }.to change { Topic.count } expect(IncomingEmail.last.is_auto_generated).to eq(false) end it "should allow replying without reply key" do process(:mailinglist) topic = Topic.last expect { process(:mailinglist_reply) }.to change { topic.posts.count } end it "should skip validations for staged users" do Fabricate(:user, email: "alice@foo.com", staged: true) expect { process(:mailinglist_short_message) }.to change { Topic.count } end it "should skip validations for regular users" do Fabricate(:user, email: "alice@foo.com") expect { process(:mailinglist_short_message) }.to change { Topic.count } end context "with 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 describe "#select_body" do let(:email) { <<~EMAIL } MIME-Version: 1.0 Date: Tue, 01 Jan 2019 00:00:00 +0300 Subject: An email with whitespaces From: Foo <foo@discourse.org> To: bar@discourse.org Content-Type: text/plain; charset="UTF-8" This is a line that will be stripped This is another line that will be stripped This is a line that will not be touched. This is another line that will not be touched. * list * sub-list - list - sub-list + list + sub-list [code] 1.upto(10).each do |i| puts i end ``` # comment [/code] This is going to be stripped too. ``` 1.upto(10).each do |i| puts i end [/code] # comment ``` This is going to be stripped too. Bye! EMAIL let(:stripped_text) { <<~MD } This is a line that will be stripped This is another line that will be stripped This is a line that will not be touched. This is another line that will not be touched. * list * sub-list - list - sub-list + list + sub-list [code] 1.upto(10).each do |i| puts i end ``` # comment [/code] This is going to be stripped too. ``` 1.upto(10).each do |i| puts i end [/code] # comment ``` This is going to be stripped too. Bye! MD it "strips lines if strip_incoming_email_lines is enabled" do SiteSetting.strip_incoming_email_lines = true receiver = Email::Receiver.new(email) text, elided, format = receiver.select_body expect(text).to eq(stripped_text) end it "works with empty mail body" do SiteSetting.strip_incoming_email_lines = true email = <<~EMAIL Date: Tue, 01 Jan 2019 00:00:00 +0300 Subject: An email with whitespaces From: Foo <foo@discourse.org> To: bar@discourse.org Content-Type: text/plain; charset="UTF-8" -- my signature EMAIL receiver = Email::Receiver.new(email) text, _elided, _format = receiver.select_body expect(text).to be_blank end end describe "replying to digest" do fab!(:user) { Fabricate(:user) } fab!(:digest_message_id) { "7402d8ae-1c6e-44bc-9948-48e007839bcc@localhost" } fab!(:email_log) do Fabricate( :email_log, user: user, email_type: "digest", to_address: user.email, message_id: digest_message_id, ) end let(:email) { <<~EMAIL } MIME-Version: 1.0 Date: Tue, 01 Jan 2019 00:00:00 +0300 From: someone <#{user.email}> To: Discourse <#{SiteSetting.notification_email}> Message-ID: <CANtGPwC3ZmWSxnnEuJHfosbtc9d0-ZV02b_7KuyircDt4peDC2@mail.gmail.com> In-Reply-To: <#{digest_message_id}> Subject: Re: [Discourse] Summary References: <#{digest_message_id}> Content-Type: text/plain; charset="UTF-8" hello there! I like the digest! EMAIL before { Jobs.run_immediately! } it "returns a ReplyToDigestError" do expect { Email::Receiver.new(email).process! }.to raise_error( Email::Receiver::ReplyToDigestError, ) end end describe "find_related_post" do let(:user) { Fabricate(:user) } let(:group) { Fabricate(:group, users: [user]) } let (:email_1) { <<~EMAIL MIME-Version: 1.0 Date: Wed, 01 Jan 2019 12:00:00 +0200 Message-ID: <7aN1uwcokt2xkfG3iYrpKmiuVhy4w9b5@mail.gmail.com> Subject: Lorem ipsum dolor sit amet From: Dan Ungureanu <dan@discourse.org> To: team-test@discourse.org Content-Type: text/plain; charset="UTF-8" Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas semper, erat tempor sodales commodo, mi diam tempus lorem, in vehicula leo libero quis lacus. Nullam justo nunc, sagittis nec metus placerat, auctor condimentum neque. Sed risus purus, fermentum eget purus porttitor, finibus efficitur orci. Integer tempus mi nec odio elementum pulvinar. Pellentesque sed fringilla nulla, ac mollis quam. Vivamus semper lacinia scelerisque. Cras urna magna, porttitor nec libero quis, congue viverra sapien. Nulla sodales ac tellus a suscipit. EMAIL } let (:post_2) { 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) { <<~EMAIL MIME-Version: 1.0 Date: Wed, 01 Jan 2019 12:00:00 +0200 References: <7aN1uwcokt2xkfG3iYrpKmiuVhy4w9b5@mail.gmail.com> <topic/#{post_2.topic_id}/#{post_2.id}@test.localhost> In-Reply-To: <topic/#{post_2.topic_id}/#{post_2.id}@test.localhost> Message-ID: <w1vdxT8ebJjZQQp7XyDdEJaSscE9qRjr@mail.gmail.com> Subject: Re: Lorem ipsum dolor sit amet From: Dan Ungureanu <dan@discourse.org> To: team-test@discourse.org Content-Type: text/plain; charset="UTF-8" Integer mattis suscipit facilisis. Ut ullamcorper libero at faucibus sodales. Ut suscipit elit ac dui porta consequat. Suspendisse potenti. Nam ut accumsan dui, eget commodo sapien. Etiam ultrices elementum cursus. Vivamus et diam et orci lobortis porttitor. Aliquam scelerisque ex a imperdiet ornare. Donec interdum laoreet posuere. Nulla sagittis, velit id posuere sollicitudin, elit nunc laoreet libero, vitae aliquet tortor eros at est. Donec vitae massa vehicula, aliquet libero non, porttitor ipsum. Phasellus pellentesque sodales lacus eu sagittis. Aliquam ut condimentum nisi. Nulla in placerat felis. Sed pellentesque, massa auctor venenatis gravida, risus lorem iaculis mi, at hendrerit nisi turpis sit amet metus. Nulla egestas ante eget nisi luctus consectetur. EMAIL } def receive(email_string) Email::Receiver.new(email_string, destinations: [group]).process! end it "makes all posts in same topic" do expect { receive(email_1) }.to change { Topic.count }.by(1).and change { Post.where(post_type: Post.types[:regular]).count }.by(1) expect { post_2 }.to not_change { Topic.count }.and change { Post.where(post_type: Post.types[:regular]).count }.by(1) expect { receive(email_3) }.to not_change { Topic.count }.and change { Post.where(post_type: Post.types[:regular]).count }.by(1) end end it "fixes valid addresses in embedded emails" do Fabricate(:group, incoming_email: "group-fwd@example.com") process(:long_embedded_email_headers) incoming_email = IncomingEmail.find_by(message_id: "example1@mail.gmail.com") expect(incoming_email.cc_addresses).to include("a@example.com") expect(incoming_email.cc_addresses).to include("c@example.com") end end