summaryrefslogtreecommitdiff
path: root/lib/gitlab/email/handler/service_desk_handler.rb
blob: 71b1d4ed8f90fa3b0b176e52e83e0dd6cda9021d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# frozen_string_literal: true

# handles service desk issue creation emails with these formats:
#   incoming+gitlab-org-gitlab-ce-20-issue-@incoming.gitlab.com
#   incoming+gitlab-org/gitlab-ce@incoming.gitlab.com (legacy)
module Gitlab
  module Email
    module Handler
      class ServiceDeskHandler < BaseHandler
        include ReplyProcessing
        include Gitlab::Utils::StrongMemoize

        HANDLER_REGEX        = /\A#{HANDLER_ACTION_BASE_REGEX}-issue-\z/.freeze
        HANDLER_REGEX_LEGACY = /\A(?<project_path>[^\+]*)\z/.freeze
        PROJECT_KEY_PATTERN  = /\A(?<slug>.+)-(?<key>[a-z0-9_]+)\z/.freeze

        def initialize(mail, mail_key, service_desk_key: nil)
          if service_desk_key
            mail_key ||= service_desk_key
            @service_desk_key = service_desk_key
          end

          super(mail, mail_key)

          match_project_slug || match_legacy_project_slug
        end

        def can_handle?
          Gitlab::ServiceDesk.supported? && (project_id || can_handle_legacy_format? || service_desk_key)
        end

        def execute
          raise ProjectNotFound if project.nil?

          create_issue_or_note

          if from_address
            add_email_participant
            send_thank_you_email unless reply_email?
          end
        end

        def match_project_slug
          return if mail_key&.include?('/')
          return unless matched = HANDLER_REGEX.match(mail_key.to_s)

          @project_slug = matched[:project_slug]
          @project_id   = matched[:project_id]&.to_i
        end

        def match_legacy_project_slug
          return unless matched = HANDLER_REGEX_LEGACY.match(mail_key.to_s)

          @project_path = matched[:project_path]
        end

        def metrics_event
          :receive_email_service_desk
        end

        def project
          strong_memoize(:project) do
            project_record = super
            project_record ||= project_from_key if service_desk_key
            project_record&.service_desk_enabled? ? project_record : nil
          end
        end

        private

        attr_reader :project_id, :project_path, :service_desk_key

        def project_from_key
          return unless match = service_desk_key.match(PROJECT_KEY_PATTERN)

          Project.with_service_desk_key(match[:key]).find do |project|
            valid_project_key?(project, match[:slug])
          end
        end

        def valid_project_key?(project, slug)
          project.present? && slug == project.full_path_slug
        end

        def create_issue_or_note
          if reply_email?
            create_note_from_reply_email
          else
            create_issue!
          end
        end

        def create_issue!
          @issue = ::Issues::CreateService.new(
            project: project,
            current_user: User.support_bot,
            params: {
              title: mail.subject,
              description: message_including_template,
              confidential: true,
              external_author: from_address
            },
            spam_params: nil
          ).execute

          raise InvalidIssueError unless @issue.persisted?

          begin
            ::Issue::Email.create!(issue: @issue, email_message_id: mail.message_id)
          rescue StandardError => e
            Gitlab::ErrorTracking.log_exception(e)
          end

          if service_desk_setting&.issue_template_missing?
            create_template_not_found_note
          end
        end

        def issue_from_reply_to
          strong_memoize(:issue_from_reply_to) do
            next unless mail.in_reply_to

            Issue::Email.find_by_email_message_id(mail.in_reply_to)&.issue
          end
        end

        def reply_email?
          issue_from_reply_to.present?
        end

        def create_note_from_reply_email
          @issue = issue_from_reply_to

          create_note(message_including_reply)
        end

        def send_thank_you_email
          Notify.service_desk_thank_you_email(@issue.id).deliver_later
          Gitlab::Metrics::BackgroundTransaction.current&.add_event(:service_desk_thank_you_email)
        end

        def message_including_template
          description = message_including_reply_or_only_quotes
          template_content = service_desk_setting&.issue_template_content

          if template_content.present?
            description += "  \n" + template_content
          end

          description
        end

        def service_desk_setting
          strong_memoize(:service_desk_setting) do
            project.service_desk_setting
          end
        end

        def create_template_not_found_note
          issue_template_key = service_desk_setting&.issue_template_key

          warning_note = <<-MD.strip_heredoc
            WARNING: The template file #{issue_template_key}.md used for service desk issues is empty or could not be found.
            Please check service desk settings and update the file to be used.
          MD

          create_note(warning_note)
        end

        def create_note(note)
          ::Notes::CreateService.new(
            project,
            User.support_bot,
            noteable: @issue,
            note: note
          ).execute
        end

        def from_address
          (mail.reply_to || []).first || mail.from.first || mail.sender
        end

        def can_handle_legacy_format?
          project_path && project_path.include?('/') && !mail_key.include?('+')
        end

        def author
          User.support_bot
        end

        def add_email_participant
          return if reply_email? && !Feature.enabled?(:issue_email_participants, @issue.project)

          @issue.issue_email_participants.create(email: from_address)
        end
      end
    end
  end
end