1# frozen_string_literal: true
2
3class Notify < ApplicationMailer
4  include ActionDispatch::Routing::PolymorphicRoutes
5  include GitlabRoutingHelper
6  include EmailsHelper
7  include ReminderEmailsHelper
8  include IssuablesHelper
9
10  include Emails::Issues
11  include Emails::MergeRequests
12  include Emails::Notes
13  include Emails::PagesDomains
14  include Emails::Projects
15  include Emails::Profile
16  include Emails::Pipelines
17  include Emails::Members
18  include Emails::AutoDevops
19  include Emails::RemoteMirrors
20  include Emails::Releases
21  include Emails::Groups
22  include Emails::Reviews
23  include Emails::ServiceDesk
24  include Emails::InProductMarketing
25  include Emails::AdminNotification
26
27  helper TimeboxesHelper
28  helper MergeRequestsHelper
29  helper DiffHelper
30  helper BlobHelper
31  helper EmailsHelper
32  helper ReminderEmailsHelper
33  helper MembersHelper
34  helper AvatarsHelper
35  helper GitlabRoutingHelper
36  helper IssuablesHelper
37  helper InProductMarketingHelper
38
39  def test_email(recipient_email, subject, body)
40    mail(to: recipient_email,
41         subject: subject,
42         body: body.html_safe,
43         content_type: 'text/html'
44        )
45  end
46
47  # Splits "gitlab.corp.company.com" up into "gitlab.corp.company.com",
48  # "corp.company.com" and "company.com".
49  # Respects set tld length so "company.co.uk" won't match "somethingelse.uk"
50  def self.allowed_email_domains
51    domain_parts = Gitlab.config.gitlab.host.split(".")
52    allowed_domains = []
53    begin
54      allowed_domains << domain_parts.join(".")
55      domain_parts.shift
56    end while domain_parts.length > ActionDispatch::Http::URL.tld_length
57
58    allowed_domains
59  end
60
61  def can_send_from_user_email?(sender)
62    sender_domain = sender.email.split("@").last
63    self.class.allowed_email_domains.include?(sender_domain)
64  end
65
66  private
67
68  # Return an email address that displays the name of the sender.
69  # Only the displayed name changes; the actual email address is always the same.
70  def sender(sender_id, send_from_user_email: false, sender_name: nil)
71    return unless sender = User.find(sender_id)
72
73    address = default_sender_address
74    address.display_name = sender_name.presence || "#{sender.name} (#{sender.to_reference})"
75
76    if send_from_user_email && can_send_from_user_email?(sender)
77      address.address = sender.email
78    end
79
80    address.format
81  end
82
83  # Formats arguments into a String suitable for use as an email subject
84  #
85  # extra - Extra Strings to be inserted into the subject
86  #
87  # Examples
88  #
89  #   >> subject('Lorem ipsum')
90  #   => "Lorem ipsum"
91  #
92  #   # Automatically inserts Project name when @project is set
93  #   >> @project = Project.last
94  #   => #<Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...>
95  #   >> subject('Lorem ipsum')
96  #   => "Ruby on Rails | Lorem ipsum "
97  #
98  #   # Accepts multiple arguments
99  #   >> subject('Lorem ipsum', 'Dolor sit amet')
100  #   => "Lorem ipsum | Dolor sit amet"
101  def subject(*extra)
102    subject = []
103
104    subject << @project.name if @project
105    subject << @group.name if @group
106    subject.concat(extra) if extra.present?
107    subject << Gitlab.config.gitlab.email_subject_suffix if Gitlab.config.gitlab.email_subject_suffix.present?
108
109    subject.join(' | ')
110  end
111
112  # Return a string suitable for inclusion in the 'Message-Id' mail header.
113  #
114  # The message-id is generated from the unique URL to a model object.
115  def message_id(model)
116    model_name = model.class.model_name.singular_route_key
117    "<#{model_name}_#{model.id}@#{Gitlab.config.gitlab.host}>"
118  end
119
120  def mail_thread(model, headers = {})
121    add_project_headers
122    add_unsubscription_headers_and_links
123    add_model_headers(model)
124
125    headers['X-GitLab-Reply-Key'] = reply_key
126
127    @reason = headers['X-GitLab-NotificationReason']
128
129    if Gitlab::IncomingEmail.enabled? && @sent_notification
130      headers['Reply-To'] = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)).tap do |address|
131        address.display_name = reply_display_name(model)
132      end
133
134      fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>"
135      headers['References'] ||= []
136      headers['References'].unshift(fallback_reply_message_id)
137
138      @reply_by_email = true
139    end
140
141    mail(headers)
142  end
143
144  # `model` is used on EE code
145  def reply_display_name(_model)
146    @project.full_name
147  end
148
149  # Send an email that starts a new conversation thread,
150  # with headers suitable for grouping by thread in email clients.
151  #
152  # See: mail_answer_thread
153  def mail_new_thread(model, headers = {})
154    headers['Message-ID'] = message_id(model)
155
156    mail_thread(model, headers)
157  end
158
159  # Send an email that responds to an existing conversation thread,
160  # with headers suitable for grouping by thread in email clients.
161  #
162  # For grouping emails by thread, email clients heuristics require the answers to:
163  #
164  #  * have a subject that begin by 'Re: '
165  #  * have a 'In-Reply-To' or 'References' header that references the original 'Message-ID'
166  #
167  def mail_answer_thread(model, headers = {})
168    headers['Message-ID'] = "<#{SecureRandom.hex}@#{Gitlab.config.gitlab.host}>"
169    headers['In-Reply-To'] = message_id(model)
170    headers['References'] = [message_id(model)]
171
172    headers[:subject] = "Re: #{headers[:subject]}" if headers[:subject]
173
174    mail_thread(model, headers)
175  end
176
177  def mail_answer_note_thread(model, note, headers = {})
178    headers['Message-ID'] = message_id(note)
179    headers['In-Reply-To'] = message_id(note.references.last)
180    headers['References'] = note.references.map { |ref| message_id(ref) }
181
182    headers['X-GitLab-Discussion-ID'] = note.discussion.id if note.part_of_discussion? || note.can_be_discussion_note?
183
184    headers[:subject] = "Re: #{headers[:subject]}" if headers[:subject]
185
186    mail_thread(model, headers)
187  end
188
189  def reply_key
190    @reply_key ||= SentNotification.reply_key
191  end
192
193  # This method applies threading headers to the email to identify
194  # the instance we are discussing.
195  #
196  # All model instances must have `#id`, and may implement `#iid`.
197  def add_model_headers(object)
198    # Use replacement so we don't strip the module.
199    prefix = "X-GitLab-#{object.class.name.gsub(/::/, '-')}"
200
201    headers["#{prefix}-ID"] = object.id
202    headers["#{prefix}-IID"] = object.iid if object.respond_to?(:iid)
203  end
204
205  def add_project_headers
206    return unless @project
207
208    headers['X-GitLab-Project'] = @project.name
209    headers['X-GitLab-Project-Id'] = @project.id
210    headers['X-GitLab-Project-Path'] = @project.full_path
211    headers['List-Id'] = "#{@project.full_path} <#{create_list_id_string(@project)}>"
212  end
213
214  def add_unsubscription_headers_and_links
215    return unless !@labels_url && @sent_notification && @sent_notification.unsubscribable?
216
217    list_unsubscribe_methods = [unsubscribe_sent_notification_url(@sent_notification, force: true)]
218    if Gitlab::IncomingEmail.enabled? && Gitlab::IncomingEmail.supports_wildcard?
219      list_unsubscribe_methods << "mailto:#{Gitlab::IncomingEmail.unsubscribe_address(reply_key)}"
220    end
221
222    headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',')
223    @unsubscribe_url = unsubscribe_sent_notification_url(@sent_notification)
224  end
225end
226
227Notify.prepend_mod_with('Notify')
228