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