diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 76480ea1c12d7399a77b635e8d391f8c06cec1d7..f1f574c642483842c95dbbae41a283e22aa429f0 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -293,6 +293,7 @@ export default {
           :key="`${currentKey}-${field.name}`"
           v-bind="field"
           :is-validated="isValidated"
+          :data-qa-selector="`${field.name}_div`"
         />
       </div>
     </div>
diff --git a/app/assets/javascripts/integrations/edit/components/sections/configuration.vue b/app/assets/javascripts/integrations/edit/components/sections/configuration.vue
index 9e1ad24ae9fc62f4e88b94d8f2b516e9d192723e..b8fd8995744104b4526c8866c36895b882c7fa0a 100644
--- a/app/assets/javascripts/integrations/edit/components/sections/configuration.vue
+++ b/app/assets/javascripts/integrations/edit/components/sections/configuration.vue
@@ -33,6 +33,7 @@ export default {
       :key="`${currentKey}-${field.name}`"
       v-bind="field"
       :is-validated="isValidated"
+      :data-qa-selector="`${field.name}_div`"
     />
   </div>
 </template>
diff --git a/qa/qa/page/project/settings/integrations.rb b/qa/qa/page/project/settings/integrations.rb
index 420dcb6391878220329e96938827302d66c06388..0d5515aacdf8bdcf4c577a39548a827c5bbea562 100644
--- a/qa/qa/page/project/settings/integrations.rb
+++ b/qa/qa/page/project/settings/integrations.rb
@@ -8,12 +8,17 @@ class Integrations < QA::Page::Base
           view 'app/assets/javascripts/integrations/index/components/integrations_table.vue' do
             element :prometheus_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern
             element :jira_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern
+            element :pipelines_email_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern
           end
 
           def click_on_prometheus_integration
             click_element :prometheus_link
           end
 
+          def click_pipelines_email_link
+            click_element :pipelines_email_link
+          end
+
           def click_jira_link
             click_element :jira_link
           end
diff --git a/qa/qa/page/project/settings/services/pipeline_status_emails.rb b/qa/qa/page/project/settings/services/pipeline_status_emails.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2f78577e3d5fc415229296976b0bd474ee394fc5
--- /dev/null
+++ b/qa/qa/page/project/settings/services/pipeline_status_emails.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module QA
+  module Page
+    module Project
+      module Settings
+        module Services
+          class PipelineStatusEmails < QA::Page::Base
+            view 'app/assets/javascripts/integrations/edit/components/integration_form.vue' do
+              element :recipients_div, %q(:data-qa-selector="`${field.name}_div`") # rubocop:disable QA/ElementWithPattern
+              element :notify_only_broken_pipelines_div, %q(:data-qa-selector="`${field.name}_div`") # rubocop:disable QA/ElementWithPattern
+              element :save_changes_button
+            end
+
+            def set_recipients(emails)
+              within_element :recipients_div do
+                fill_in 'Recipients', with: emails.join(',')
+              end
+            end
+
+            def toggle_notify_broken_pipelines
+              within_element :notify_only_broken_pipelines_div do
+                uncheck 'Notify only broken pipelines', allow_label_click: true
+              end
+            end
+
+            def click_save_button
+              click_element(:save_changes_button)
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb
index f41e59856226b3ddc6312b699b3df7161bbacd93..b815186cd493e2cbb7ad7a589e226ac65a7a68b3 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb
@@ -46,19 +46,22 @@ def mailhog_json
           total = mailhog_data.dig('total')
           subjects = mailhog_data.dig('items')
             .map(&method(:mailhog_item_subject))
-            .join("\n")
 
           Runtime::Logger.debug(%Q[Total number of emails: #{total}])
-          Runtime::Logger.debug(%Q[Subjects:\n#{subjects}])
+          Runtime::Logger.debug(%Q[Subjects:\n#{subjects.join("\n")}])
 
           # Expect at least two invitation messages: group and project
-          mailhog_data if total >= 2
+          mailhog_data if mailhog_project_message_count(subjects) >= 1
         end
       end
 
       def mailhog_item_subject(item)
         item.dig('Content', 'Headers', 'Subject', 0)
       end
+
+      def mailhog_project_message_count(subjects)
+        subjects.count { |subject| subject.include?('project was granted') }
+      end
     end
   end
 end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_status_emails_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_status_emails_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f4794b3a904a39827ba0d27088e7860f560ad9af
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_status_emails_spec.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+module QA
+  RSpec.shared_examples 'notifies on a pipeline' do |exit_code|
+    before do
+      push_commit(exit_code: exit_code)
+    end
+
+    it 'sends an email' do
+      meta = exit_code_meta(exit_code)
+
+      project.visit!
+      Flow::Pipeline.wait_for_latest_pipeline(status: meta[:status])
+
+      messages = mail_hog_messages(mail_hog)
+      subjects = messages.map(&:subject)
+      targets = messages.map(&:to)
+
+      aggregate_failures do
+        expect(subjects).to include(meta[:email_subject])
+        expect(subjects).to include(/#{Regexp.escape(project.name)}/)
+        expect(targets).to include(*emails)
+      end
+    end
+  end
+
+  RSpec.describe 'Verify', :orchestrated, :runner, :requires_admin, :smtp do
+    describe 'Pipeline status emails' do
+      let(:executor) { "qa-runner-#{Time.now.to_i}" }
+      let(:emails) { %w[foo@bar.com baz@buzz.com] }
+
+      let(:project) do
+        Resource::Project.fabricate_via_api! do |project|
+          project.name = 'pipeline-status-project'
+        end
+      end
+
+      let!(:runner) do
+        Resource::Runner.fabricate! do |runner|
+          runner.project = project
+          runner.name = executor
+          runner.tags = [executor]
+        end
+      end
+
+      let(:mail_hog) { Vendor::MailHog::API.new }
+
+      before(:all) do
+        Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true)
+      end
+
+      before do
+        setup_pipeline_emails(emails)
+      end
+
+      describe 'when pipeline passes', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366240' do
+        include_examples 'notifies on a pipeline', 0
+      end
+
+      describe 'when pipeline fails', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366241' do
+        include_examples 'notifies on a pipeline', 1
+      end
+
+      def push_commit(exit_code: 0)
+        Resource::Repository::Commit.fabricate_via_api! do |commit|
+          commit.project = project
+          commit.commit_message = 'Add .gitlab-ci.yml'
+          commit.add_files(
+            [
+              {
+                file_path: '.gitlab-ci.yml',
+                content: gitlab_ci_yaml(exit_code: exit_code)
+              }
+            ]
+          )
+        end
+      end
+
+      def setup_pipeline_emails(emails)
+        page.visit Runtime::Scenario.gitlab_address
+        Flow::Login.sign_in_unless_signed_in
+
+        project.visit!
+
+        Page::Project::Menu.perform(&:go_to_integrations_settings)
+        QA::Page::Project::Settings::Integrations.perform(&:click_pipelines_email_link)
+
+        QA::Page::Project::Settings::Services::PipelineStatusEmails.perform do |pipeline_status_emails|
+          pipeline_status_emails.toggle_notify_broken_pipelines # notify on pass and fail
+          pipeline_status_emails.set_recipients(emails)
+          pipeline_status_emails.click_save_button
+        end
+      end
+
+      def gitlab_ci_yaml(exit_code: 0, tag: executor)
+        <<~YAML
+          test-pipeline-email:
+            tags:
+              - #{tag}
+            script: sleep 5; exit #{exit_code};
+        YAML
+      end
+
+      private
+
+      def exit_code_meta(exit_code)
+        {
+          0 => { status: 'passed', email_subject: /Successful pipeline/ },
+          1 => { status: 'failed', email_subject: /Failed pipeline/ }
+        }[exit_code]
+      end
+
+      def mail_hog_messages(mail_hog_api)
+        Support::Retrier.retry_until(sleep_interval: 1) do
+          Runtime::Logger.debug('Fetching email...')
+
+          messages = mail_hog_api.fetch_messages
+          logs = messages.map { |m| "#{m.to}: #{m.subject}" }
+
+          Runtime::Logger.debug("MailHog Logs: #{logs.join("\n")}")
+
+          # for failing pipelines we have three messages
+          # one for the owner
+          # and one for each recipient
+          messages if mail_hog_pipeline_count(messages) >= 2
+        end
+      end
+
+      def mail_hog_pipeline_count(messages)
+        messages.count { |message| message.subject.include?('pipeline') }
+      end
+    end
+  end
+end
diff --git a/qa/qa/vendor/mail_hog/api.rb b/qa/qa/vendor/mail_hog/api.rb
new file mode 100644
index 0000000000000000000000000000000000000000..85eb06316247c8a4ece6cc0ca1a41e70fe6837fe
--- /dev/null
+++ b/qa/qa/vendor/mail_hog/api.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module QA
+  module Vendor
+    module MailHog
+      # Represents a Set of messages from a MailHog response
+      class Messages
+        include Enumerable
+
+        attr_reader :data
+
+        def initialize(data)
+          @data = data
+        end
+
+        def total
+          data.dig('total')
+        end
+
+        def each
+          data.dig('items')&.each do |item|
+            yield MessageItem.new(item)
+          end
+        end
+      end
+
+      # Represents an email item from a MailHog response
+      class MessageItem
+        attr_reader :data
+
+        def initialize(data)
+          @data = data
+        end
+
+        def to
+          data.dig('Content', 'Headers', 'To', 0)
+        end
+
+        def subject
+          data.dig('Content', 'Headers', 'Subject', 0)
+        end
+      end
+
+      class API
+        include Support::API
+
+        attr_reader :hostname
+
+        def initialize(hostname: QA::Runtime::Env.mailhog_hostname || 'localhost')
+          @hostname = hostname
+        end
+
+        def base_url
+          "http://#{hostname}:8025"
+        end
+
+        def api_messages_url(version: 2)
+          "#{base_url}/api/v#{version}/messages"
+        end
+
+        def delete_messages
+          delete(api_messages_url(version: 1))
+        end
+
+        def fetch_messages
+          Messages.new(JSON.parse(fetch_messages_json))
+        end
+
+        def fetch_messages_json
+          get(api_messages_url).body
+        end
+      end
+    end
+  end
+end