1# frozen_string_literal: true 2 3# handles service desk issue creation emails with these formats: 4# incoming+gitlab-org-gitlab-ce-20-issue-@incoming.gitlab.com 5# incoming+gitlab-org/gitlab-ce@incoming.gitlab.com (legacy) 6module Gitlab 7 module Email 8 module Handler 9 class ServiceDeskHandler < BaseHandler 10 include ReplyProcessing 11 include Gitlab::Utils::StrongMemoize 12 13 HANDLER_REGEX = /\A#{HANDLER_ACTION_BASE_REGEX}-issue-\z/.freeze 14 HANDLER_REGEX_LEGACY = /\A(?<project_path>[^\+]*)\z/.freeze 15 PROJECT_KEY_PATTERN = /\A(?<slug>.+)-(?<key>[a-z0-9_]+)\z/.freeze 16 17 def initialize(mail, mail_key, service_desk_key: nil) 18 if service_desk_key 19 mail_key ||= service_desk_key 20 @service_desk_key = service_desk_key 21 end 22 23 super(mail, mail_key) 24 25 match_project_slug || match_legacy_project_slug 26 end 27 28 def can_handle? 29 Gitlab::ServiceDesk.supported? && (project_id || can_handle_legacy_format? || service_desk_key) 30 end 31 32 def execute 33 raise ProjectNotFound if project.nil? 34 35 create_issue_or_note 36 37 if from_address 38 add_email_participant 39 send_thank_you_email unless reply_email? 40 end 41 end 42 43 def match_project_slug 44 return if mail_key&.include?('/') 45 return unless matched = HANDLER_REGEX.match(mail_key.to_s) 46 47 @project_slug = matched[:project_slug] 48 @project_id = matched[:project_id]&.to_i 49 end 50 51 def match_legacy_project_slug 52 return unless matched = HANDLER_REGEX_LEGACY.match(mail_key.to_s) 53 54 @project_path = matched[:project_path] 55 end 56 57 def metrics_event 58 :receive_email_service_desk 59 end 60 61 def project 62 strong_memoize(:project) do 63 project_record = super 64 project_record ||= project_from_key if service_desk_key 65 project_record&.service_desk_enabled? ? project_record : nil 66 end 67 end 68 69 private 70 71 attr_reader :project_id, :project_path, :service_desk_key 72 73 def project_from_key 74 return unless match = service_desk_key.match(PROJECT_KEY_PATTERN) 75 76 Project.with_service_desk_key(match[:key]).find do |project| 77 valid_project_key?(project, match[:slug]) 78 end 79 end 80 81 def valid_project_key?(project, slug) 82 project.present? && slug == project.full_path_slug 83 end 84 85 def create_issue_or_note 86 if reply_email? 87 create_note_from_reply_email 88 else 89 create_issue! 90 end 91 end 92 93 def create_issue! 94 @issue = ::Issues::CreateService.new( 95 project: project, 96 current_user: User.support_bot, 97 params: { 98 title: mail.subject, 99 description: message_including_template, 100 confidential: true, 101 external_author: from_address 102 }, 103 spam_params: nil 104 ).execute 105 106 raise InvalidIssueError unless @issue.persisted? 107 108 begin 109 ::Issue::Email.create!(issue: @issue, email_message_id: mail.message_id) 110 rescue StandardError => e 111 Gitlab::ErrorTracking.log_exception(e) 112 end 113 114 if service_desk_setting&.issue_template_missing? 115 create_template_not_found_note 116 end 117 end 118 119 def issue_from_reply_to 120 strong_memoize(:issue_from_reply_to) do 121 next unless mail.in_reply_to 122 123 Issue::Email.find_by_email_message_id(mail.in_reply_to)&.issue 124 end 125 end 126 127 def reply_email? 128 issue_from_reply_to.present? 129 end 130 131 def create_note_from_reply_email 132 @issue = issue_from_reply_to 133 134 create_note(message_including_reply) 135 end 136 137 def send_thank_you_email 138 Notify.service_desk_thank_you_email(@issue.id).deliver_later 139 Gitlab::Metrics::BackgroundTransaction.current&.add_event(:service_desk_thank_you_email) 140 end 141 142 def message_including_template 143 description = message_including_reply_or_only_quotes 144 template_content = service_desk_setting&.issue_template_content 145 146 if template_content.present? 147 description += " \n" + template_content 148 end 149 150 description 151 end 152 153 def service_desk_setting 154 strong_memoize(:service_desk_setting) do 155 project.service_desk_setting 156 end 157 end 158 159 def create_template_not_found_note 160 issue_template_key = service_desk_setting&.issue_template_key 161 162 warning_note = <<-MD.strip_heredoc 163 WARNING: The template file #{issue_template_key}.md used for service desk issues is empty or could not be found. 164 Please check service desk settings and update the file to be used. 165 MD 166 167 create_note(warning_note) 168 end 169 170 def create_note(note) 171 ::Notes::CreateService.new( 172 project, 173 User.support_bot, 174 noteable: @issue, 175 note: note 176 ).execute 177 end 178 179 def from_address 180 (mail.reply_to || []).first || mail.from.first || mail.sender 181 end 182 183 def can_handle_legacy_format? 184 project_path && project_path.include?('/') && !mail_key.include?('+') 185 end 186 187 def author 188 User.support_bot 189 end 190 191 def add_email_participant 192 return if reply_email? && !Feature.enabled?(:issue_email_participants, @issue.project) 193 194 @issue.issue_email_participants.create(email: from_address) 195 end 196 end 197 end 198 end 199end 200