summaryrefslogtreecommitdiff
path: root/lib/gitlab/email/receiver.rb
blob: 4da112bc5a0b6b6855c83b0904d4bcd5ef987ee4 (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
# frozen_string_literal: true

require_dependency 'gitlab/email/handler'

# Inspired in great part by Discourse's Email::Receiver
module Gitlab
  module Email
    class Receiver
      include Gitlab::Utils::StrongMemoize

      RECEIVED_HEADER_REGEX = /for\s+\<(.+)\>/.freeze

      def initialize(raw)
        @raw = raw
      end

      def execute
        raise EmptyEmailError if @raw.blank?

        ignore_auto_reply!

        raise UnknownIncomingEmail unless handler

        handler.execute.tap do
          Gitlab::Metrics::BackgroundTransaction.current&.add_event(handler.metrics_event, handler.metrics_params)
        end
      rescue StandardError => e
        Gitlab::Metrics::BackgroundTransaction.current&.add_event('email_receiver_error', error: e.class.name)
        raise e
      end

      def mail_metadata
        {
          mail_uid: mail.message_id,
          from_address: mail.from,
          to_address: mail.to,
          mail_key: mail_key,
          references: Array(mail.references),
          delivered_to: delivered_to.map(&:value),
          envelope_to: envelope_to.map(&:value),
          x_envelope_to: x_envelope_to.map(&:value),
          # reduced down to what looks like an email in the received headers
          received_recipients: recipients_from_received_headers,
          meta: {
            client_id: "email/#{mail.from.first}",
            project: handler&.project&.full_path
          }
        }
      end

      def mail
        strong_memoize(:mail) { build_mail }
      end

      private

      def handler
        strong_memoize(:handler) { find_handler }
      end

      def find_handler
        Handler.for(mail, mail_key)
      end

      def build_mail
        Mail::Message.new(@raw)
      rescue Encoding::UndefinedConversionError,
             Encoding::InvalidByteSequenceError => e
        raise EmailUnparsableError, e
      end

      def mail_key
        strong_memoize(:mail_key) do
          key_from_to_header || key_from_additional_headers
        end
      end

      def key_from_to_header
        mail.to.find do |address|
          key = email_class.key_from_address(address)
          break key if key
        end
      end

      def key_from_additional_headers
        find_key_from_references ||
          find_key_from_delivered_to_header ||
          find_key_from_envelope_to_header ||
          find_key_from_x_envelope_to_header ||
          find_first_key_from_received_headers
      end

      def ensure_references_array(references)
        case references
        when Array
          references
        when String
          # Handle emails from clients which append with commas,
          # example clients are Microsoft exchange and iOS app
          Gitlab::IncomingEmail.scan_fallback_references(references)
        when nil
          []
        end
      end

      def find_key_from_references
        ensure_references_array(mail.references).find do |mail_id|
          key = email_class.key_from_fallback_message_id(mail_id)
          break key if key
        end
      end

      def delivered_to
        Array(mail[:delivered_to])
      end

      def envelope_to
        Array(mail[:envelope_to])
      end

      def x_envelope_to
        Array(mail[:x_envelope_to])
      end

      def received
        Array(mail[:received])
      end

      def find_key_from_delivered_to_header
        delivered_to.find do |header|
          key = email_class.key_from_address(header.value)
          break key if key
        end
      end

      def find_key_from_envelope_to_header
        envelope_to.find do |header|
          key = email_class.key_from_address(header.value)
          break key if key
        end
      end

      def find_key_from_x_envelope_to_header
        x_envelope_to.find do |header|
          key = email_class.key_from_address(header.value)
          break key if key
        end
      end

      def find_first_key_from_received_headers
        return unless ::Feature.enabled?(:use_received_header_for_incoming_emails)

        recipients_from_received_headers.find do |email|
          key = email_class.key_from_address(email)
          break key if key
        end
      end

      def recipients_from_received_headers
        strong_memoize :emails_from_received_headers do
          received.map { |header| header.value[RECEIVED_HEADER_REGEX, 1] }.compact
        end
      end

      def ignore_auto_reply!
        if auto_submitted? || auto_replied?
          raise AutoGeneratedEmailError
        end
      end

      def auto_submitted?
        # Mail::Header#[] is case-insensitive
        auto_submitted = mail.header['Auto-Submitted']&.value

        # Mail::Field#value would strip leading and trailing whitespace
        # See also https://tools.ietf.org/html/rfc3834
        auto_submitted && auto_submitted != 'no'
      end

      def auto_replied?
        autoreply = mail.header['X-Autoreply']&.value

        autoreply && autoreply == 'yes'
      end

      def email_class
        Gitlab::IncomingEmail
      end
    end
  end
end