diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7fdba85021991aad8763852f6567cf3565e2620b
--- /dev/null
+++ b/app/mailers/emails/pipelines.rb
@@ -0,0 +1,38 @@
+module Emails
+  module Pipelines
+    def pipeline_succeeded_email(params, to)
+      pipeline_mail(params, to, 'succeeded') # TODO: missing template
+    end
+
+    def pipeline_failed_email(params, to)
+      pipeline_mail(params, to, 'failed') # TODO: missing template
+    end
+
+    private
+
+    def pipeline_mail(params, to, status)
+      @params = params
+      add_headers
+
+      mail(to: to, subject: pipeline_subject('failed'))
+    end
+
+    def add_headers
+      @project = @params.project # `add_project_headers` needs this
+      add_project_headers
+      add_pipeline_headers(@params.pipeline)
+    end
+
+    def add_pipeline_headers(pipeline)
+      headers['X-GitLab-Pipeline-Id'] = pipeline.id
+      headers['X-GitLab-Pipeline-Ref'] = pipeline.ref
+      headers['X-GitLab-Pipeline-Status'] = pipeline.status
+    end
+
+    def pipeline_subject(status)
+      subject(
+        "Pipeline #{status} for #{@params.project.name}",
+        @params.pipeline.short_sha)
+    end
+  end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0cc709f68e46c0319f737f3faa7c590f20064671..fef8149b325ae9779701e468a59ea12f89a6e7ad 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -7,6 +7,7 @@ class Notify < BaseMailer
   include Emails::Projects
   include Emails::Profile
   include Emails::Builds
+  include Emails::Pipelines
   include Emails::Members
 
   add_template_helper MergeRequestsHelper
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1f852445c1c3b711de9c048c24bd34942a2e3fcd
--- /dev/null
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -0,0 +1,93 @@
+class PipelinesEmailService < Service
+  prop_accessor :recipients
+  boolean_accessor :add_pusher
+  boolean_accessor :notify_only_broken_pipelines
+  validates :recipients,
+    presence: true,
+    if: ->(s) { s.activated? && !s.add_pusher? }
+
+  def initialize_properties
+    self.properties ||= { notify_only_broken_builds: true }
+  end
+
+  def title
+    'Pipelines emails'
+  end
+
+  def description
+    'Email the pipelines status to a list of recipients.'
+  end
+
+  def to_param
+    'pipelines_email'
+  end
+
+  def supported_events
+    %w[pipeline]
+  end
+
+  def execute(data, force = false)
+    return unless supported_events.include?(data[:object_kind])
+    return unless force || should_build_be_notified?(data)
+
+    all_recipients = retrieve_recipients(data)
+
+    return unless all_recipients.any?
+
+    PipelineEmailWorker.perform_async(data, all_recipients)
+  end
+
+  def can_test?
+    project.pipelines.count > 0
+  end
+
+  def disabled_title
+    'Please setup a pipeline on your repository.'
+  end
+
+  def test_data(project, user)
+    data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last)
+    data[:user] = user.hook_attrs
+    data
+  end
+
+  def fields
+    [
+      { type: 'textarea',
+        name: 'recipients',
+        placeholder: 'Emails separated by comma' },
+      { type: 'checkbox',
+        name: 'add_pusher',
+        label: 'Add pusher to recipients list' },
+      { type: 'checkbox',
+        name: 'notify_only_broken_pipelines' },
+    ]
+  end
+
+  def test(data)
+    result = execute(data, true)
+
+    { success: true, result: result }
+  rescue StandardError => error
+    { success: false, result: error }
+  end
+
+  def should_build_be_notified?(data)
+    case data[:object_attributes][:status]
+    when 'success'
+      !notify_only_broken_pipelines?
+    else
+      false
+    end
+  end
+
+  def retrieve_recipients(data)
+    all_recipients = recipients.to_s.split(',').reject(&:blank?)
+
+    if add_pusher? && data[:user].try(:[], :email)
+      all_recipients << data[:user][:email]
+    else
+      all_recipients
+    end
+  end
+end
diff --git a/app/workers/pipeline_email_worker.rb b/app/workers/pipeline_email_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2160207c9014880497940b038d936589a23cf919
--- /dev/null
+++ b/app/workers/pipeline_email_worker.rb
@@ -0,0 +1,39 @@
+class PipelineEmailWorker
+  include Sidekiq::Worker
+
+  ParamsStruct = Struct.new(:pipeline, :project, :email_template)
+  class Params < ParamsStruct
+    def initialize(pipeline_id)
+      self.pipeline = Ci::Pipeline.find(pipeline_id)
+      self.project = pipeline.project
+      self.email_template = case pipeline.status
+                            when 'success'
+                              :pipeline_succeeded_email
+                            when 'failed'
+                              :pipeline_failed_email
+                            end
+    end
+  end
+
+  def perform(data, recipients)
+    params = Params.new(data['object_attributes']['id'])
+
+    return unless params.email_template
+
+    recipients.each do |to|
+      deliver(params, to) do
+        Notify.public_send(params.email_template, params, to).deliver_now
+      end
+    end
+  end
+
+  private
+
+  def deliver(params, to)
+    yield
+  # These are input errors and won't be corrected even if Sidekiq retries
+  rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
+    project_name = params.project.path_with_namespace
+    logger.info("Failed to send email for #{project_name} to #{to}: #{e}")
+  end
+end