From 5ab913f7b5f7e5133f6a7740dcdc3c09e2a57dac Mon Sep 17 00:00:00 2001 From: Pranav Date: Fri, 1 Aug 2025 02:13:46 -0700 Subject: [PATCH] chore: Add a condition to handle bounced email (#11873) Add bounced emails to the conversation thread. Fix Gmail bounce detection by checking the X-Failed-Recipients header. Currently, bounced emails are rejected as auto-replies, which causes support agents to miss important delivery failure context. This PR ensures bounced messages are correctly added to the thread, preserving visibility for the support team. --- .../incoming_email_validity_helper.rb | 13 +- app/presenters/mail_presenter.rb | 4 + spec/fixtures/files/bounced_gmail.eml | 120 ++++++++++++++++++ spec/mailboxes/imap/imap_mailbox_spec.rb | 8 ++ 4 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 spec/fixtures/files/bounced_gmail.eml diff --git a/app/mailboxes/incoming_email_validity_helper.rb b/app/mailboxes/incoming_email_validity_helper.rb index 252ef5257..9483c9768 100644 --- a/app/mailboxes/incoming_email_validity_helper.rb +++ b/app/mailboxes/incoming_email_validity_helper.rb @@ -4,16 +4,17 @@ module IncomingEmailValidityHelper def incoming_email_from_valid_email? return false unless valid_external_email_for_active_account? + # Return if email doesn't have a valid sender + # This can happen in cases like bounce emails for invalid contact email address + return false unless Devise.email_regexp.match?(@processed_mail.original_sender) + + # Process bounced emails, as regular emails + return true if @processed_mail.bounced? + # we skip processing auto reply emails like delivery status notifications # out of office replies, etc. return false if auto_reply_email? - # return if email doesn't have a valid sender - # This can happen in cases like bounce emails for invalid contact email address - # TODO: Handle the bounce separately and mark the contact as invalid in case of reply bounces - # The returned value could be "\"\"" for some email clients - return false unless Devise.email_regexp.match?(@processed_mail.original_sender) - true end diff --git a/app/presenters/mail_presenter.rb b/app/presenters/mail_presenter.rb index 890e97a78..e57831c96 100644 --- a/app/presenters/mail_presenter.rb +++ b/app/presenters/mail_presenter.rb @@ -157,6 +157,10 @@ class MailPresenter < SimpleDelegator auto_submitted? || x_auto_reply? end + def bounced? + @mail.bounced? || @mail['X-Failed-Recipients'].try(:value).present? + end + def notification_email_from_chatwoot? # notification emails are send via mailer sender email address. so it should match original_sender == Mail::Address.new(ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot ')).address diff --git a/spec/fixtures/files/bounced_gmail.eml b/spec/fixtures/files/bounced_gmail.eml new file mode 100644 index 000000000..2c45bcd4d --- /dev/null +++ b/spec/fixtures/files/bounced_gmail.eml @@ -0,0 +1,120 @@ +Delivered-To: robert.smith@gmail.com +Return-Path: <> +Subject: Delivery Status Notification (Failure) +From: Mail Delivery Subsystem +To: robert.smith@gmail.com +Content-Type: multipart/report; boundary="00000000000093475906390e1e9b"; report-type=delivery-status +Auto-Submitted: auto-replied +Message-ID: <686707c9.050a0220.302e7d.0cb2.GMR@mx.google.com> +Date: Thu, 03 Jul 2025 15:44:25 -0700 (PDT) +X-Failed-Recipients: alex.jones@fictionalcorp.com + +--00000000000093475906390e1e9b +Content-Type: multipart/related; boundary="000000000000936d8406390e1ec7" + +--000000000000936d8406390e1ec7 +Content-Type: multipart/alternative; boundary="000000000000936d9006390e1ec8" + +--000000000000936d9006390e1ec8 +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + + +** Address not found ** + +Your message wasn't delivered to alex.jones@fictionalcorp.com because the address co= +uldn't be found or is unable to receive email. + +Learn more here: https://support.google.com/mail/?p=3DNoSuchUser + +The response was: + +550 5.1.1 The email account that you tried to reach does not exist. Please = +try double-checking the recipient's email address for typos or unnecessary = +spaces. For more information, go to https://support.google.com/mail/?p=3DNo= +SuchUser d2e1a72fcca58-74ce2b0525csor332154b3a.0 - gsmtp + +--000000000000936d9006390e1ec8 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + + + + + + + + + + + +
+ + +
++=3D"Error + + + + +

+Address not found +

+Your message wasn't delivered to alex.jones@fictionalcorp.com because the address couldn't be found = +or is unable to receive email. +
+LEARN MORE +
+
+
+The response was:
+

+550 5.1.1 The email account that you tried to reach does not exist. Please = +try double-checking the recipient's email address for typos or unnecessary = +spaces. For more information, go to https://support.google.com/mail/?p=3DNo= +SuchUser d2e1a72fcca58-74ce2b0525csor332154b3a.0 - gsmtp +

+
+ + + +--000000000000936d9006390e1ec8-- +--000000000000936d8406390e1ec7 +Content-Type: image/png; name="icon.png" +Content-Disposition: attachment; filename="icon.png" +Content-Transfer-Encoding: base64 +Content-ID: + +--000000000000936d8406390e1ec7-- +--00000000000093475906390e1e9b +Content-Type: message/delivery-status + +--00000000000093475906390e1e9b +Content-Type: message/rfc822 + +Date: Thu, 03 Jul 2025 15:44:23 -0700 +From: Robert Smith +Reply-To: robert.smith@gmail.com +To: alex.jones@fictionalcorp.com +Message-ID: +In-Reply-To: +Subject: Just checking in +Mime-Version: 1.0 +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: 7bit + +

Hey, just checking in. Let me know if you got my earlier message.

+ +--00000000000093475906390e1e9b-- diff --git a/spec/mailboxes/imap/imap_mailbox_spec.rb b/spec/mailboxes/imap/imap_mailbox_spec.rb index cc72be18b..fc94c98be 100644 --- a/spec/mailboxes/imap/imap_mailbox_spec.rb +++ b/spec/mailboxes/imap/imap_mailbox_spec.rb @@ -115,6 +115,14 @@ RSpec.describe Imap::ImapMailbox do end end + context 'when the email is bounced' do + let!(:bounced_mail) { create_inbound_email_from_fixture('bounced_gmail.eml') } + + it 'processes the bounced email' do + expect { class_instance.process(bounced_mail.mail, channel) }.to change(Message, :count) + end + end + context 'when a reply for existing email conversation' do let(:prev_conversation) { create(:conversation, account: account, inbox: channel.inbox, assignee: agent) } let(:reply_mail) do