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, default_enabled: :yaml)
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
|