diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index e261a689156..48eee92c4ba 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2692,6 +2692,15 @@ en: Can you make sure [your email address](%{base_url}/my/preferences/email) is valid and working? You may also wish to add our email address to your address book / contact list to improve deliverability. + email_bounced: | + The message to %{email} bounced. + + ### Details + + ```text + %{raw} + ``` + too_many_spam_flags: title: "Too Many Spam Flags" subject_template: "New account on hold" diff --git a/lib/email/processor.rb b/lib/email/processor.rb index 27ae09b1b91..a721244c519 100644 --- a/lib/email/processor.rb +++ b/lib/email/processor.rb @@ -18,11 +18,9 @@ module Email @receiver.process! rescue RateLimiter::LimitExceeded @retry_on_rate_limit ? Jobs.enqueue(:process_email, mail: @mail) : raise - rescue Email::Receiver::BouncedEmailError => e - # never reply to bounced emails - log_email_process_failure(@mail, e) - set_incoming_email_rejection_message(@receiver.incoming_email, I18n.t("emails.incoming.errors.bounced_email_error")) rescue => e + return handle_bounce(e) if @receiver.is_bounce? + log_email_process_failure(@mail, e) incoming_email = @receiver.try(:incoming_email) rejection_message = handle_failure(@mail, e) @@ -34,6 +32,12 @@ module Email private + def handle_bounce(e) + # never reply to bounced emails + log_email_process_failure(@mail, e) + set_incoming_email_rejection_message(@receiver.incoming_email, I18n.t("emails.incoming.errors.bounced_email_error")) + end + def handle_failure(mail_string, e) message_template = case e when Email::Receiver::NoSenderDetectedError then return nil diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 084f70962d8..c17b238d644 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -70,8 +70,10 @@ module Email return if IncomingEmail.exists?(message_id: @message_id) ensure_valid_address_lists @from_email, @from_display_name = parse_from_field(@mail) + @from_user = User.find_by_email(@from_email) @incoming_email = create_incoming_email process_internal + raise BouncedEmailError if is_bounce? rescue => e error = e.to_s error = e.class.name if error.blank? @@ -109,14 +111,14 @@ module Email end def process_internal - raise BouncedEmailError if is_bounce? + handle_bounce if is_bounce? raise NoSenderDetectedError if @from_email.blank? raise FromReplyByAddressError if is_from_reply_by_email_address? raise ScreenedEmailError if ScreenedEmail.should_block?(@from_email) hidden_reason_id = is_spam? ? Post.hidden_reasons[:email_spam_header_found] : nil - user = find_user(@from_email) + user = @from_user if user.present? log_and_validate_user(user) @@ -132,7 +134,7 @@ module Email if is_auto_generated? && !sent_to_mailinglist_mirror? @incoming_email.update_columns(is_auto_generated: true) - if SiteSetting.block_auto_generated_emails? + if SiteSetting.block_auto_generated_emails? && !is_bounce? raise AutoGeneratedEmailError end end @@ -157,17 +159,16 @@ module Email hidden_reason_id: hidden_reason_id, post: post, topic: post.topic, - skip_validations: user.staged?) + skip_validations: user.staged?, + bounce: is_bounce?) else first_exception = nil destinations.each do |destination| begin - process_destination(destination, user, body, elided, hidden_reason_id) + return process_destination(destination, user, body, elided, hidden_reason_id) rescue => e first_exception ||= e - else - return end end @@ -195,16 +196,21 @@ module Email end def is_bounce? - return false unless @mail.bounced? || verp + @mail.bounced? || verp + end + def handle_bounce @incoming_email.update_columns(is_bounce: true) if verp && (bounce_key = verp[/\+verp-(\h{32})@/, 1]) && (email_log = EmailLog.find_by(bounce_key: bounce_key)) email_log.update_columns(bounced: true) - email = email_log.user.try(:email).presence + user = email_log.user + email = user&.email.presence + topic = email_log.topic end email ||= @from_email + user ||= @from_user if @mail.error_status.present? && Array.wrap(@mail.error_status).any? { |s| s.start_with?("4.") } Email::Receiver.update_bounce_score(email, SiteSetting.soft_bounce_score) @@ -212,7 +218,11 @@ module Email Email::Receiver.update_bounce_score(email, SiteSetting.hard_bounce_score) end - true + return if SiteSetting.enable_whispers? && + user&.staged? && + (topic.blank? || topic.archetype == Archetype.private_message) + + raise BouncedEmailError end def is_from_reply_by_email_address? @@ -444,6 +454,12 @@ module Email end def parse_from_field(mail) + if is_bounce? + Array.wrap(mail.final_recipient).each do |from| + return extract_from_address_and_name(from) + end + end + return unless mail[:from] if mail[:from].errors.blank? @@ -470,6 +486,11 @@ module Email end def extract_from_address_and_name(value) + if value[";"] + from_display_name, from_address = value.split(";") + return [from_address&.strip&.downcase, from_display_name&.strip] + end + if value[/<[^>]+>/] from_address = value[/<([^>]+)>/, 1] from_display_name = value[/^([^<]+)/, 1] @@ -492,10 +513,6 @@ module Email end end - def find_user(email) - User.find_by_email(email) - end - def find_or_create_user(email, display_name, raise_on_failed_create: false) user = nil @@ -582,6 +599,8 @@ module Email has_been_forwarded? && process_forwarded_email(destination, user) + return if is_bounce? && destination[:type] != :reply + case destination[:type] when :group group = destination[:obj] @@ -616,7 +635,8 @@ module Email hidden_reason_id: hidden_reason_id, post: post, topic: post&.topic, - skip_validations: user.staged?) + skip_validations: user.staged?, + bounce: is_bounce?) end end @@ -852,6 +872,8 @@ module Email def create_reply(options = {}) raise TopicNotFoundError if options[:topic].nil? || options[:topic].trashed? + raise BouncedEmailError if options[:bounce] && options[:topic].archetype != Archetype.private_message + options[:post] = nil if options[:post]&.trashed? enable_email_pm_setting(options[:user]) if options[:topic].archetype == Archetype.private_message @@ -985,6 +1007,13 @@ module Email end user = options.delete(:user) + + if options[:bounce] + options[:raw] = I18n.t("system_messages.email_bounced", email: user.email, raw: options[:raw]) + user = Discourse.system_user + options[:post_type] = Post.types[:whisper] + end + result = NewPostManager.new(user, options).perform errors = result.errors.full_messages diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 705791aacb7..ba8e82673c5 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -103,12 +103,40 @@ describe Email::Receiver do ) end - it "raises a BouncerEmailError when email is a bounced email" do - expect { process(:bounced_email) }.to raise_error(Email::Receiver::BouncedEmailError) - expect(IncomingEmail.last.is_bounce).to eq(true) + 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) + expect { process(:bounced_email_multiple_status_codes) }.to raise_error(Email::Receiver::BouncedEmailError) + expect(IncomingEmail.last.is_bounce).to eq(true) + end + + it "creates a whisper post in PM if user is staged" do + SiteSetting.enable_staged_users = true + SiteSetting.enable_whispers = true + + email = "linux-admin@b-s-c.co.jp" + user = Fabricate(:staged, email: email) + private_message = Fabricate(:topic, archetype: 'private_message', category_id: nil, user: user) + private_message.allowed_users = [user] + private_message.save! + post = create_post(topic: private_message, user: user) + + post_reply_key = begin + Fabricate(:post_reply_key, + reply_key: "4f97315cc828096c9cb34c6f1a0d6fe8", + user: user, + post: post + ) + end + + 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, raw: "Your email bounced").strip) + expect(IncomingEmail.last.is_bounce).to eq(true) + end end it "logs a blank error" do diff --git a/spec/fixtures/emails/bounced_email.eml b/spec/fixtures/emails/bounced_email.eml index 5365881539d..67ffec2c54d 100644 --- a/spec/fixtures/emails/bounced_email.eml +++ b/spec/fixtures/emails/bounced_email.eml @@ -2,7 +2,7 @@ Delivered-To: someguy@discourse.org Date: Thu, 7 Apr 2016 19:04:30 +0900 (JST) From: MAILER-DAEMON@b-s-c.co.jp (Mail Delivery System) Subject: Undelivered Mail Returned to Sender -To: someguy@discourse.org +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com MIME-Version: 1.0 Content-Type: multipart/report; report-type=delivery-status; boundary="18F5D18A0075.1460023470/some@daemon.com"