FEATURE: If PM email bounced for staged user then alert in whisper reply (#6648)

FEATURE: If PM email bounced for staged user then alert in whisper reply (#6648)

From cedd2118c497f59fdfaf34097fa9ab4f6e9ef928 Mon Sep 17 00:00:00 2001
From: Vinoth Kannan <vinothkannan@vinkas.com>
Date: Tue, 27 Nov 2018 00:29:37 +0530
Subject: [PATCH] FEATURE: If PM email bounced for staged user then alert in
 whisper reply (#6648)


diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index e261a68..48eee92 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 27ae09b..a721244 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 084f709..c17b238 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.ne

GitHub