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