gitlab-mail_room 0.0.10 → 0.0.19

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -20,7 +20,14 @@ The fork is useful as we can post quick fixes to our own fork and release fixes
20
20
 
21
21
  ## README
22
22
 
23
- mail_room is a configuration based process that will idle on IMAP connections and execute a delivery method when a new message is received. Examples of delivery methods include:
23
+ mail_room is a configuration based process that will listen for incoming
24
+ e-mail and execute a delivery method when a new message is
25
+ received. mail_room supports the following methods for receiving e-mail:
26
+
27
+ * IMAP
28
+ * [Microsoft Graph API](https://docs.microsoft.com/en-us/graph/api/resources/mail-api-overview?view=graph-rest-1.0)
29
+
30
+ Examples of delivery methods include:
24
31
 
25
32
  * POST to a delivery URL (Postback)
26
33
  * Queue a job to Sidekiq or Que for later processing (Sidekiq or Que)
@@ -117,6 +124,31 @@ You will also need to install `faraday` or `letter_opener` if you use the `postb
117
124
  :host: 127.0.0.1
118
125
  :port: 26379
119
126
  :worker: EmailReceiverWorker
127
+ -
128
+ :email: "user7@outlook365.com"
129
+ :password: "password"
130
+ :name: "inbox"
131
+ :inbox_method: microsoft_graph
132
+ :inbox_options:
133
+ :tenant_id: 12345
134
+ :client_id: ABCDE
135
+ :client_secret: YOUR-SECRET-HERE
136
+ :poll_interval: 60
137
+ :delivery_method: sidekiq
138
+ :delivery_options:
139
+ :redis_url: redis://localhost:6379
140
+ :worker: EmailReceiverWorker
141
+ -
142
+ :email: "user8@gmail.com"
143
+ :password: "password"
144
+ :name: "inbox"
145
+ :delivery_method: postback
146
+ :delivery_options:
147
+ :delivery_url: "http://localhost:3000/inbox"
148
+ :jwt_auth_header: "Mailroom-Api-Request"
149
+ :jwt_issuer: "mailroom"
150
+ :jwt_algorithm: "HS256"
151
+ :jwt_secret_path: "/etc/secrets/mailroom/.mailroom_secret"
120
152
  ```
121
153
 
122
154
  **Note:** If using `delete_after_delivery`, you also probably want to use
@@ -134,6 +166,72 @@ the server is running. Otherwise, it returns a 500 status code.
134
166
 
135
167
  This feature is not included in upstream `mail_room` and is specific to GitLab.
136
168
 
169
+ ## inbox_method
170
+
171
+ By default, IMAP mode is assumed for reading a mailbox.
172
+
173
+ ### IMAP Server Configuration ##
174
+
175
+ You can set per-mailbox configuration for the IMAP server's `host` (default: 'imap.gmail.com'), `port` (default: 993), `ssl` (default: true), and `start_tls` (default: false).
176
+
177
+ If you want to set additional options for IMAP SSL you can pass a YAML hash to match [SSLContext#set_params](http://docs.ruby-lang.org/en/2.2.0/OpenSSL/SSL/SSLContext.html#method-i-set_params). If you set `verify_mode` to `:none` it'll replace with the appropriate constant.
178
+
179
+ If you're seeing the error `Please log in via your web browser: https://support.google.com/mail/accounts/answer/78754 (Failure)`, you need to configure your Gmail account to allow less secure apps to access it: https://support.google.com/accounts/answer/6010255.
180
+
181
+ ### Microsoft Graph configuration
182
+
183
+ To use the Microsoft Graph API instead of IMAP to read e-mail, you will
184
+ need to create an application in the Azure Active Directory. See the
185
+ [Microsoft instructions](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) for more details:
186
+
187
+ 1. Sign in to the [Azure portal](https://portal.azure.com).
188
+ 1. Search for and select `Azure Active Directory`.
189
+ 1. Under `Manage`, select `App registrations` > `New registration`.
190
+ 1. Enter a `Name` for your application, such as `MailRoom`. Users of your app might see this name, and you can change it later.
191
+ 1. If `Supported account types` is listed, select the appropriate option.
192
+ 1. Leave `Redirect URI` blank. This is not needed.
193
+ 1. Select `Register`.
194
+ 1. Under `Manage`, select `Certificates & secrets`.
195
+ 1. Under `Client secrets`, select `New client secret`, and enter a name.
196
+ 1. Under `Expires`, select `Never`, unless you plan on updating the credentials every time it expires.
197
+ 1. Select `Add`. Record the secret value in a safe location for use in a later step.
198
+ 1. Under `Manage`, select `API Permissions` > `Add a permission`. Select `Microsoft Graph`.
199
+ 1. Select `Application permissions`.
200
+ 1. Under the `Mail` node, select `Mail.ReadWrite`, and then select Add permissions.
201
+ 1. If `User.Read` is listed in the permission list, you can delete this.
202
+ 1. Click `Grant admin consent` for these permissions.
203
+
204
+ #### Restrict mailbox access
205
+
206
+ Note that for MailRoom to work as a service account, this application
207
+ must have the `Mail.ReadWrite` to read/write mail in *all*
208
+ mailboxes. However, while this appears to be security risk,
209
+ we can configure an application access policy to limit the
210
+ mailbox access for this account. [Follow these instructions](https://docs.microsoft.com/en-us/graph/auth-limit-mailbox-access)
211
+ to setup PowerShell and configure this policy.
212
+
213
+ #### MailRoom config for Microsoft Graph
214
+
215
+ In the MailRoom configuration, set `inbox_method` to `microsoft_graph`.
216
+ You will also need:
217
+
218
+ * The client and tenant ID from the `Overview` section in the Azure app page
219
+ * The client secret created earlier
220
+
221
+ Fill in `inbox_options` with these values:
222
+
223
+ ```yaml
224
+ :inbox_method: microsoft_graph
225
+ :inbox_options:
226
+ :tenant_id: 12345
227
+ :client_id: ABCDE
228
+ :client_secret: YOUR-SECRET-HERE
229
+ :poll_interval: 60
230
+ ```
231
+
232
+ By default, MailRoom will poll for new messages every 60 seconds. `poll_interval` configures the number of
233
+ seconds to poll. Setting the value to 0 or under will default to 60 seconds.
234
+
137
235
  ## delivery_method ##
138
236
 
139
237
  ### postback ###
@@ -284,14 +382,6 @@ If you'd prefer not to wait that long, you can pass `idle_timeout` in seconds fo
284
382
 
285
383
  This setting allows configuration of the IMAP search command sent to the server. This still defaults 'UNSEEN'. You may find that 'NEW' works better for you.
286
384
 
287
- ## IMAP Server Configuration ##
288
-
289
- You can set per-mailbox configuration for the IMAP server's `host` (default: 'imap.gmail.com'), `port` (default: 993), `ssl` (default: true), and `start_tls` (default: false).
290
-
291
- If you want to set additional options for IMAP SSL you can pass a YAML hash to match [SSLContext#set_params](http://docs.ruby-lang.org/en/2.2.0/OpenSSL/SSL/SSLContext.html#method-i-set_params). If you set `verify_mode` to `:none` it'll replace with the appropriate constant.
292
-
293
- If you're seeing the error `Please log in via your web browser: https://support.google.com/mail/accounts/answer/78754 (Failure)`, you need to configure your Gmail account to allow less secure apps to access it: https://support.google.com/accounts/answer/6010255.
294
-
295
385
  ## Running in Production ##
296
386
 
297
387
  I suggest running with either upstart or init.d. Check out this wiki page for some example scripts for both: https://github.com/tpitale/mail_room/wiki/Init-Scripts-for-Running-mail_room
data/Rakefile CHANGED
@@ -3,4 +3,4 @@ require 'rspec/core/rake_task'
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task :default => :spec
6
+ task default: :spec
@@ -31,7 +31,7 @@ module MailRoom
31
31
  # Any subsequent failure in the instance which gets the lock will be dealt
32
32
  # with by the expiration, at which time another instance can pick up the
33
33
  # message and try again.
34
- client.set(key, 1, {:nx => true, :ex => expiration})
34
+ client.set(key, 1, nx: true, ex: expiration)
35
35
  end
36
36
 
37
37
  private
@@ -6,18 +6,23 @@ module MailRoom
6
6
 
7
7
  def initialize(mailbox)
8
8
  @mailbox = mailbox
9
+ @stopped = false
9
10
  end
10
11
 
11
12
  def on_new_message(&block)
12
13
  @new_message_handler = block
13
14
  end
14
15
 
16
+ def stopped?
17
+ @stopped
18
+ end
19
+
15
20
  def wait
16
21
  raise NotImplementedError
17
22
  end
18
23
 
19
24
  def quit
20
- raise NotImplementedError
25
+ @stopped = true
21
26
  end
22
27
  end
23
28
  end
@@ -24,7 +24,7 @@ module MailRoom
24
24
  # Trigger `LetterOpener` to deliver our message
25
25
  # @param message [String] the email message as a string, RFC822 format
26
26
  def deliver(message)
27
- method = ::LetterOpener::DeliveryMethod.new(:location => @delivery_options.location)
27
+ method = ::LetterOpener::DeliveryMethod.new(location: @delivery_options.location)
28
28
  method.deliver!(Mail.read_from_string(message))
29
29
 
30
30
  true
@@ -1,11 +1,12 @@
1
1
  require 'faraday'
2
+ require "mail_room/jwt"
2
3
 
3
4
  module MailRoom
4
5
  module Delivery
5
6
  # Postback Delivery method
6
7
  # @author Tony Pitale
7
8
  class Postback
8
- Options = Struct.new(:url, :token, :username, :password, :logger, :content_type) do
9
+ Options = Struct.new(:url, :token, :username, :password, :logger, :content_type, :jwt) do
9
10
  def initialize(mailbox)
10
11
  url =
11
12
  mailbox.delivery_url ||
@@ -17,6 +18,8 @@ module MailRoom
17
18
  mailbox.delivery_options[:delivery_token] ||
18
19
  mailbox.delivery_options[:token]
19
20
 
21
+ jwt = initialize_jwt(mailbox.delivery_options)
22
+
20
23
  username = mailbox.delivery_options[:username]
21
24
  password = mailbox.delivery_options[:password]
22
25
 
@@ -24,16 +27,31 @@ module MailRoom
24
27
 
25
28
  content_type = mailbox.delivery_options[:content_type]
26
29
 
27
- super(url, token, username, password, logger, content_type)
30
+ super(url, token, username, password, logger, content_type, jwt)
28
31
  end
29
32
 
30
33
  def token_auth?
31
34
  !self[:token].nil?
32
35
  end
33
36
 
37
+ def jwt_auth?
38
+ self[:jwt].valid?
39
+ end
40
+
34
41
  def basic_auth?
35
42
  !self[:username].nil? && !self[:password].nil?
36
43
  end
44
+
45
+ private
46
+
47
+ def initialize_jwt(delivery_options)
48
+ ::MailRoom::JWT.new(
49
+ header: delivery_options[:jwt_auth_header],
50
+ secret_path: delivery_options[:jwt_secret_path],
51
+ algorithm: delivery_options[:jwt_algorithm],
52
+ issuer: delivery_options[:jwt_issuer]
53
+ )
54
+ end
37
55
  end
38
56
 
39
57
  # Build a new delivery, hold the delivery options
@@ -60,13 +78,27 @@ module MailRoom
60
78
  connection.post do |request|
61
79
  request.url @delivery_options.url
62
80
  request.body = message
63
- # request.options[:timeout] = 3
64
- request.headers['Content-Type'] = @delivery_options.content_type unless @delivery_options.content_type.nil?
81
+ config_request_content_type(request)
82
+ config_request_jwt_auth(request)
65
83
  end
66
84
 
67
85
  @delivery_options.logger.info({ delivery_method: 'Postback', action: 'message pushed', url: @delivery_options.url })
68
86
  true
69
87
  end
88
+
89
+ private
90
+
91
+ def config_request_content_type(request)
92
+ return if @delivery_options.content_type.nil?
93
+
94
+ request.headers['Content-Type'] = @delivery_options.content_type
95
+ end
96
+
97
+ def config_request_jwt_auth(request)
98
+ return unless @delivery_options.jwt_auth?
99
+
100
+ request.headers[@delivery_options.jwt.header] = @delivery_options.jwt.token
101
+ end
70
102
  end
71
103
  end
72
104
  end
@@ -8,16 +8,17 @@ module MailRoom
8
8
  # Sidekiq Delivery method
9
9
  # @author Douwe Maan
10
10
  class Sidekiq
11
- Options = Struct.new(:redis_url, :namespace, :sentinels, :queue, :worker, :logger) do
11
+ Options = Struct.new(:redis_url, :namespace, :sentinels, :queue, :worker, :logger, :redis_db) do
12
12
  def initialize(mailbox)
13
13
  redis_url = mailbox.delivery_options[:redis_url] || "redis://localhost:6379"
14
+ redis_db = mailbox.delivery_options[:redis_db] || 0
14
15
  namespace = mailbox.delivery_options[:namespace]
15
16
  sentinels = mailbox.delivery_options[:sentinels]
16
17
  queue = mailbox.delivery_options[:queue] || "default"
17
18
  worker = mailbox.delivery_options[:worker]
18
19
  logger = mailbox.logger
19
20
 
20
- super(redis_url, namespace, sentinels, queue, worker, logger)
21
+ super(redis_url, namespace, sentinels, queue, worker, logger, redis_db)
21
22
  end
22
23
  end
23
24
 
@@ -45,7 +46,7 @@ module MailRoom
45
46
  def client
46
47
  @client ||= begin
47
48
  sentinels = options.sentinels
48
- redis_options = { url: options.redis_url }
49
+ redis_options = { url: options.redis_url, db: options.redis_db }
49
50
  redis_options[:sentinels] = sentinels if sentinels
50
51
 
51
52
  redis = ::Redis.new(redis_options)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'securerandom'
5
+ require 'jwt'
6
+ require 'base64'
7
+
8
+ module MailRoom
9
+ # Responsible for validating and generating JWT token
10
+ class JWT
11
+ DEFAULT_ISSUER = 'mailroom'
12
+ DEFAULT_ALGORITHM = 'HS256'
13
+
14
+ attr_reader :header, :secret_path, :issuer, :algorithm
15
+
16
+ def initialize(header:, secret_path:, issuer:, algorithm:)
17
+ @header = header
18
+ @secret_path = secret_path
19
+ @issuer = issuer || DEFAULT_ISSUER
20
+ @algorithm = algorithm || DEFAULT_ALGORITHM
21
+ end
22
+
23
+ def valid?
24
+ [@header, @secret_path, @issuer, @algorithm].none?(&:nil?)
25
+ end
26
+
27
+ def token
28
+ return nil unless valid?
29
+
30
+ secret = Base64.strict_decode64(File.read(@secret_path).chomp)
31
+ payload = {
32
+ nonce: SecureRandom.hex(12),
33
+ iat: Time.now.to_i, # https://github.com/jwt/ruby-jwt#issued-at-claim
34
+ iss: @issuer
35
+ }
36
+ ::JWT.encode payload, secret, @algorithm
37
+ end
38
+ end
39
+ end
@@ -1,11 +1,14 @@
1
1
  require "mail_room/delivery"
2
2
  require "mail_room/arbitration"
3
3
  require "mail_room/imap"
4
+ require "mail_room/microsoft_graph"
4
5
 
5
6
  module MailRoom
6
7
  # Mailbox Configuration fields
7
8
  MAILBOX_FIELDS = [
8
9
  :email,
10
+ :inbox_method,
11
+ :inbox_options,
9
12
  :password,
10
13
  :host,
11
14
  :port,
@@ -42,24 +45,26 @@ module MailRoom
42
45
  # 29 minutes, as suggested by the spec: https://tools.ietf.org/html/rfc2177
43
46
  IMAP_IDLE_TIMEOUT = 29 * 60 # 29 minutes in in seconds
44
47
 
45
- REQUIRED_CONFIGURATION = [:name, :email, :password, :host, :port]
48
+ IMAP_CONFIGURATION = [:name, :email, :password, :host, :port].freeze
49
+ MICROSOFT_GRAPH_CONFIGURATION = [:name, :email].freeze
50
+ MICROSOFT_GRAPH_INBOX_OPTIONS = [:tenant_id, :client_id, :client_secret].freeze
46
51
 
47
52
  # Default attributes for the mailbox configuration
48
53
  DEFAULTS = {
49
- :search_command => 'UNSEEN',
50
- :delivery_method => 'postback',
51
- :host => 'imap.gmail.com',
52
- :port => 993,
53
- :ssl => true,
54
- :start_tls => false,
55
- :limit_max_unread => 0,
56
- :idle_timeout => IMAP_IDLE_TIMEOUT,
57
- :delete_after_delivery => false,
58
- :expunge_deleted => false,
59
- :delivery_options => {},
60
- :arbitration_method => 'noop',
61
- :arbitration_options => {},
62
- :logger => {}
54
+ search_command: 'UNSEEN',
55
+ delivery_method: 'postback',
56
+ host: 'imap.gmail.com',
57
+ port: 993,
58
+ ssl: true,
59
+ start_tls: false,
60
+ limit_max_unread: 0,
61
+ idle_timeout: IMAP_IDLE_TIMEOUT,
62
+ delete_after_delivery: false,
63
+ expunge_deleted: false,
64
+ delivery_options: {},
65
+ arbitration_method: 'noop',
66
+ arbitration_options: {},
67
+ logger: {}
63
68
  }
64
69
 
65
70
  # Store the configuration and require the appropriate delivery method
@@ -122,14 +127,32 @@ module MailRoom
122
127
  { email: self.email, name: self.name }
123
128
  end
124
129
 
130
+ def imap?
131
+ !microsoft_graph?
132
+ end
133
+
134
+ def microsoft_graph?
135
+ self[:inbox_method].to_s == 'microsoft_graph'
136
+ end
137
+
125
138
  def validate!
139
+ if microsoft_graph?
140
+ validate_microsoft_graph!
141
+ else
142
+ validate_imap!
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def validate_imap!
126
149
  if self[:idle_timeout] > IMAP_IDLE_TIMEOUT
127
150
  raise IdleTimeoutTooLarge,
128
151
  "Please use an idle timeout smaller than #{29*60} to prevent " \
129
152
  "IMAP server disconnects"
130
153
  end
131
154
 
132
- REQUIRED_CONFIGURATION.each do |k|
155
+ IMAP_CONFIGURATION.each do |k|
133
156
  if self[k].nil?
134
157
  raise ConfigurationError,
135
158
  "Field :#{k} is required in Mailbox: #{inspect}"
@@ -137,7 +160,23 @@ module MailRoom
137
160
  end
138
161
  end
139
162
 
140
- private
163
+ def validate_microsoft_graph!
164
+ raise ConfigurationError, "Missing inbox_options in Mailbox: #{inspect}" unless self.inbox_options.is_a?(Hash)
165
+
166
+ MICROSOFT_GRAPH_CONFIGURATION.each do |k|
167
+ if self[k].nil?
168
+ raise ConfigurationError,
169
+ "Field :#{k} is required in Mailbox: #{inspect}"
170
+ end
171
+ end
172
+
173
+ MICROSOFT_GRAPH_INBOX_OPTIONS.each do |k|
174
+ if self[:inbox_options][k].nil?
175
+ raise ConfigurationError,
176
+ "inbox_options field :#{k} is required in Mailbox: #{inspect}"
177
+ end
178
+ end
179
+ end
141
180
 
142
181
  def parsed_arbitration_options
143
182
  arbitration_klass::Options.new(self)
@@ -62,8 +62,14 @@ module MailRoom
62
62
  end
63
63
 
64
64
  private
65
+
65
66
  def connection
66
- @connection ||= ::MailRoom::IMAP::Connection.new(@mailbox)
67
+ @connection ||=
68
+ if @mailbox.microsoft_graph?
69
+ ::MailRoom::MicrosoftGraph::Connection.new(@mailbox)
70
+ else
71
+ ::MailRoom::IMAP::Connection.new(@mailbox)
72
+ end
67
73
  end
68
74
  end
69
75
  end