gitlab-mail_room 0.0.15 → 0.0.18

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec4ee38465229ec567497afa371c9001ba6b55f3f7b427815d2aeeaaa3916569
4
- data.tar.gz: f98d83bd07c436f6d833c8ed9710a7cfe33d43a0f56891495eb495c75001ac6f
3
+ metadata.gz: b35546ba9ef63ad546144c97515d7203a56182abb48be0b4975061e7c8598555
4
+ data.tar.gz: c6d96b85320aeafd0b41004e6b606aad1a3642de8ebee52734becc92de387185
5
5
  SHA512:
6
- metadata.gz: e7146b00f46e4ba842fad2234caf4f9145263f25aa4b21daaf16b17d5308d9cb7fc028c4cdaeede5e542bc05476bbbf95dff3f9fc5aae35fefca458004cfcd6e
7
- data.tar.gz: c89588a3d43583ce20f1513b8488293f8b2f56b13bd15331276288f4331f2bef59abef77d790836cdcfe298d8af3fc9c4475586c44bb174fb485a6e1f4ac0834
6
+ metadata.gz: 9732c479849c02606adfda10314f3cfd5d83a8c1283d4054b5464480a3eae20f1414e0db0147a8b8cf383fe0fd4d073fcc0f522377792a4fed0d64daa621ad6f
7
+ data.tar.gz: a7d5f829af0796d81deeb8b4b570c932723f0bcfb0d80f322e9bed02dddd809a286cac2beff486c199b992d3d93d1646f268e15b983f8e3e0eae6a8c0c36fb3c
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.7.2
1
+ 2.7.5
data/README.md CHANGED
@@ -138,6 +138,17 @@ You will also need to install `faraday` or `letter_opener` if you use the `postb
138
138
  :delivery_options:
139
139
  :redis_url: redis://localhost:6379
140
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"
141
152
  ```
142
153
 
143
154
  **Note:** If using `delete_after_delivery`, you also probably want to use
@@ -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
@@ -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
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'securerandom'
5
+
6
+ module MailRoom
7
+ # Responsible for validating and generating JWT token
8
+ class JWT
9
+ DEFAULT_ISSUER = 'mailroom'
10
+ DEFAULT_ALGORITHM = 'HS256'
11
+
12
+ attr_reader :header, :secret_path, :issuer, :algorithm
13
+
14
+ def initialize(header:, secret_path:, issuer:, algorithm:)
15
+ @header = header
16
+ @secret_path = secret_path
17
+ @issuer = issuer || DEFAULT_ISSUER
18
+ @algorithm = algorithm || DEFAULT_ALGORITHM
19
+ end
20
+
21
+ def valid?
22
+ [@header, @secret_path, @issuer, @algorithm].none?(&:nil?)
23
+ end
24
+
25
+ def token
26
+ return nil unless valid?
27
+
28
+ secret = Base64.strict_decode64(File.read(@secret_path).chomp)
29
+ payload = {
30
+ nonce: SecureRandom.hex(12),
31
+ iat: Time.now.to_i, # https://github.com/jwt/ruby-jwt#issued-at-claim
32
+ iss: @issuer
33
+ }
34
+ ::JWT.encode payload, secret, @algorithm
35
+ end
36
+ end
37
+ end
@@ -1,4 +1,4 @@
1
1
  module MailRoom
2
2
  # Current version of gitlab-mail_room gem
3
- VERSION = "0.0.15"
3
+ VERSION = "0.0.18"
4
4
  end
data/mail_room.gemspec CHANGED
@@ -19,6 +19,7 @@ Gem::Specification.new do |gem|
19
19
 
20
20
  gem.add_dependency "net-imap", ">= 0.2.1"
21
21
  gem.add_dependency "oauth2", "~> 1.4.4"
22
+ gem.add_dependency "jwt", ">= 2.0"
22
23
 
23
24
  # Pinning io-wait to 0.1.0, which is the last version to support Ruby < 3
24
25
  gem.add_dependency "io-wait", "~> 0.1.0"
@@ -34,7 +35,7 @@ Gem::Specification.new do |gem|
34
35
  gem.add_development_dependency "faraday"
35
36
  gem.add_development_dependency "mail"
36
37
  gem.add_development_dependency "letter_opener"
37
- gem.add_development_dependency "redis", "~> 3.3.1"
38
+ gem.add_development_dependency "redis", "~> 4"
38
39
  gem.add_development_dependency "redis-namespace"
39
40
  gem.add_development_dependency "pg"
40
41
  gem.add_development_dependency "charlock_holmes"
@@ -0,0 +1 @@
1
+ aGVsbG93b3JsZA==
@@ -15,6 +15,7 @@ describe MailRoom::Arbitration::Redis do
15
15
 
16
16
  # Private, but we don't care.
17
17
  let(:redis) { subject.send(:client) }
18
+ let(:raw_client) { redis._client }
18
19
 
19
20
  describe '#deliver?' do
20
21
  context "when called the first time" do
@@ -95,7 +96,7 @@ describe MailRoom::Arbitration::Redis do
95
96
  it 'client has same specified url' do
96
97
  subject.deliver?(123)
97
98
 
98
- expect(redis.client.options[:url]).to eq redis_url
99
+ expect(raw_client.options[:url]).to eq redis_url
99
100
  end
100
101
 
101
102
  it 'client is a instance of Redis class' do
@@ -137,10 +138,10 @@ describe MailRoom::Arbitration::Redis do
137
138
  before { ::Redis::Client::Connector::Sentinel.any_instance.stubs(:resolve).returns(sentinels) }
138
139
 
139
140
  it 'client has same specified sentinel params' do
140
- expect(redis.client.instance_variable_get(:@connector)).to be_a Redis::Client::Connector::Sentinel
141
- expect(redis.client.options[:host]).to eq('sentinel-master')
142
- expect(redis.client.options[:password]).to eq('mypassword')
143
- expect(redis.client.options[:sentinels]).to eq(sentinels)
141
+ expect(raw_client.instance_variable_get(:@connector)).to be_a Redis::Client::Connector::Sentinel
142
+ expect(raw_client.options[:host]).to eq('sentinel-master')
143
+ expect(raw_client.options[:password]).to eq('mypassword')
144
+ expect(raw_client.options[:sentinels]).to eq(sentinels)
144
145
  end
145
146
  end
146
147
  end
@@ -65,11 +65,10 @@ describe MailRoom::Delivery::Postback do
65
65
  }
66
66
  })}
67
67
 
68
-
69
68
  let(:delivery_options) {
70
69
  MailRoom::Delivery::Postback::Options.new(mailbox)
71
70
  }
72
-
71
+
73
72
  it 'posts the message with faraday' do
74
73
  connection = stub
75
74
  request = stub
@@ -82,10 +81,59 @@ describe MailRoom::Delivery::Postback do
82
81
  connection.expects(:basic_auth).with('user1', 'password123abc')
83
82
 
84
83
  MailRoom::Delivery::Postback.new(delivery_options).deliver('a message')
85
-
84
+
86
85
  expect(request.headers['Content-Type']).to eq('text/plain')
87
86
  end
88
87
  end
88
+
89
+ context 'with jwt token in the delivery options' do
90
+ let(:mailbox) {build_mailbox({
91
+ delivery_options: {
92
+ url: 'http://localhost/inbox',
93
+ jwt_auth_header: "Mailroom-Api-Request",
94
+ jwt_issuer: "mailroom",
95
+ jwt_algorithm: "HS256",
96
+ jwt_secret_path: "secret_path"
97
+ }
98
+ })}
99
+
100
+ let(:delivery_options) {
101
+ MailRoom::Delivery::Postback::Options.new(mailbox)
102
+ }
103
+
104
+ it 'posts the message with faraday' do
105
+ connection = stub
106
+ request = stub
107
+ Faraday.stubs(:new).returns(connection)
108
+
109
+ connection.expects(:post).yields(request).twice
110
+ request.stubs(:url)
111
+ request.stubs(:body=)
112
+ request.stubs(:headers).returns({})
113
+
114
+ jwt = stub
115
+ MailRoom::JWT.expects(:new).with(
116
+ header: 'Mailroom-Api-Request',
117
+ issuer: 'mailroom',
118
+ algorithm: 'HS256',
119
+ secret_path: 'secret_path'
120
+ ).returns(jwt)
121
+ jwt.stubs(:valid?).returns(true)
122
+ jwt.stubs(:header).returns('Mailroom-Api-Request')
123
+ jwt.stubs(:token).returns('a_jwt_token')
124
+
125
+ delivery = MailRoom::Delivery::Postback.new(delivery_options)
126
+
127
+ delivery.deliver('a message')
128
+ expect(request.headers['Mailroom-Api-Request']).to eql('a_jwt_token')
129
+
130
+ # A different jwt token for the second time
131
+ jwt.stubs(:token).returns('another_jwt_token')
132
+
133
+ delivery.deliver('another message')
134
+ expect(request.headers['Mailroom-Api-Request']).to eql('another_jwt_token')
135
+ end
136
+ end
89
137
  end
90
138
  end
91
139
  end
@@ -4,6 +4,7 @@ require 'mail_room/delivery/sidekiq'
4
4
  describe MailRoom::Delivery::Sidekiq do
5
5
  subject { described_class.new(options) }
6
6
  let(:redis) { subject.send(:client) }
7
+ let(:raw_client) { redis._client }
7
8
  let(:options) { MailRoom::Delivery::Sidekiq::Options.new(mailbox) }
8
9
 
9
10
  describe '#options' do
@@ -20,7 +21,7 @@ describe MailRoom::Delivery::Sidekiq do
20
21
 
21
22
  context 'with simple redis url' do
22
23
  it 'client has same specified redis_url' do
23
- expect(redis.client.options[:url]).to eq(redis_url)
24
+ expect(raw_client.options[:url]).to eq(redis_url)
24
25
  end
25
26
 
26
27
  it 'client is a instance of RedisNamespace class' do
@@ -39,7 +40,7 @@ describe MailRoom::Delivery::Sidekiq do
39
40
  end
40
41
 
41
42
  it 'client has correct redis_url' do
42
- expect(redis.client.options[:url]).to eq(redis_url)
43
+ expect(raw_client.options[:url]).to eq(redis_url)
43
44
  end
44
45
 
45
46
 
@@ -87,10 +88,10 @@ describe MailRoom::Delivery::Sidekiq do
87
88
  before { ::Redis::Client::Connector::Sentinel.any_instance.stubs(:resolve).returns(sentinels) }
88
89
 
89
90
  it 'client has same specified sentinel params' do
90
- expect(redis.client.instance_variable_get(:@connector)).to be_a Redis::Client::Connector::Sentinel
91
- expect(redis.client.options[:host]).to eq('sentinel-master')
92
- expect(redis.client.options[:password]).to eq('mypassword')
93
- expect(redis.client.options[:sentinels]).to eq(sentinels)
91
+ expect(raw_client.instance_variable_get(:@connector)).to be_a Redis::Client::Connector::Sentinel
92
+ expect(raw_client.options[:host]).to eq('sentinel-master')
93
+ expect(raw_client.options[:password]).to eq('mypassword')
94
+ expect(raw_client.options[:sentinels]).to eq(sentinels)
94
95
  end
95
96
  end
96
97
 
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ require 'mail_room/jwt'
4
+
5
+ describe MailRoom::JWT do
6
+ let(:secret_path) { File.expand_path('../fixtures/jwt_secret', File.dirname(__FILE__)) }
7
+ let(:secret) { Base64.strict_decode64(File.read(secret_path).chomp) }
8
+
9
+ let(:standard_config) do
10
+ {
11
+ secret_path: secret_path,
12
+ issuer: 'mailroom',
13
+ header: 'Mailroom-Api-Request',
14
+ algorithm: 'HS256'
15
+ }
16
+ end
17
+
18
+ describe '#token' do
19
+ let(:jwt) { described_class.new(**standard_config) }
20
+
21
+ it 'generates a valid jwt token' do
22
+ token = jwt.token
23
+ expect(token).not_to be_empty
24
+
25
+ payload = nil
26
+ expect do
27
+ payload = JWT.decode(token, secret, true, iss: 'mailroom', verify_iat: true, verify_iss: true, algorithm: 'HS256')
28
+ end.not_to raise_error
29
+ expect(payload).to be_an(Array)
30
+ expect(payload).to match(
31
+ [
32
+ a_hash_including(
33
+ 'iss' => 'mailroom',
34
+ 'nonce' => be_a(String),
35
+ 'iat' => be_a(Integer)
36
+ ),
37
+ { 'alg' => 'HS256' }
38
+ ]
39
+ )
40
+ end
41
+
42
+ it 'generates a different token for each invocation' do
43
+ expect(jwt.token).not_to eql(jwt.token)
44
+ end
45
+ end
46
+
47
+ describe '#valid?' do
48
+ it 'returns true if all essential components are present' do
49
+ jwt = described_class.new(**standard_config)
50
+ expect(jwt.valid?).to eql(true)
51
+ end
52
+
53
+ it 'returns true if header and secret path are present' do
54
+ jwt = described_class.new(
55
+ secret_path: secret_path,
56
+ header: 'Mailroom-Api-Request',
57
+ issuer: nil,
58
+ algorithm: nil
59
+ )
60
+ expect(jwt.valid?).to eql(true)
61
+ expect(jwt.issuer).to eql(described_class::DEFAULT_ISSUER)
62
+ expect(jwt.algorithm).to eql(described_class::DEFAULT_ALGORITHM)
63
+ end
64
+
65
+ it 'returns false if either header or secret_path are missing' do
66
+ expect(described_class.new(
67
+ secret_path: nil,
68
+ header: 'Mailroom-Api-Request',
69
+ issuer: nil,
70
+ algorithm: nil
71
+ ).valid?).to eql(false)
72
+ expect(described_class.new(
73
+ secret_path: secret_path,
74
+ header: nil,
75
+ issuer: nil,
76
+ algorithm: nil
77
+ ).valid?).to eql(false)
78
+ end
79
+ end
80
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-mail_room
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.15
4
+ version: 0.0.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Pitale
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-14 00:00:00.000000000 Z
11
+ date: 2021-12-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-imap
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 1.4.4
41
+ - !ruby/object:Gem::Dependency
42
+ name: jwt
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: io-wait
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -184,14 +198,14 @@ dependencies:
184
198
  requirements:
185
199
  - - "~>"
186
200
  - !ruby/object:Gem::Version
187
- version: 3.3.1
201
+ version: '4'
188
202
  type: :development
189
203
  prerelease: false
190
204
  version_requirements: !ruby/object:Gem::Requirement
191
205
  requirements:
192
206
  - - "~>"
193
207
  - !ruby/object:Gem::Version
194
- version: 3.3.1
208
+ version: '4'
195
209
  - !ruby/object:Gem::Dependency
196
210
  name: redis-namespace
197
211
  requirement: !ruby/object:Gem::Requirement
@@ -291,6 +305,7 @@ files:
291
305
  - lib/mail_room/imap.rb
292
306
  - lib/mail_room/imap/connection.rb
293
307
  - lib/mail_room/imap/message.rb
308
+ - lib/mail_room/jwt.rb
294
309
  - lib/mail_room/logger/structured.rb
295
310
  - lib/mail_room/mailbox.rb
296
311
  - lib/mail_room/mailbox_watcher.rb
@@ -300,6 +315,7 @@ files:
300
315
  - lib/mail_room/version.rb
301
316
  - logfile.log
302
317
  - mail_room.gemspec
318
+ - spec/fixtures/jwt_secret
303
319
  - spec/fixtures/test_config.yml
304
320
  - spec/lib/arbitration/redis_spec.rb
305
321
  - spec/lib/cli_spec.rb
@@ -314,6 +330,7 @@ files:
314
330
  - spec/lib/health_check_spec.rb
315
331
  - spec/lib/imap/connection_spec.rb
316
332
  - spec/lib/imap/message_spec.rb
333
+ - spec/lib/jwt_spec.rb
317
334
  - spec/lib/logger/structured_spec.rb
318
335
  - spec/lib/mailbox_spec.rb
319
336
  - spec/lib/mailbox_watcher_spec.rb
@@ -323,7 +340,7 @@ files:
323
340
  homepage: http://github.com/tpitale/mail_room
324
341
  licenses: []
325
342
  metadata: {}
326
- post_install_message:
343
+ post_install_message:
327
344
  rdoc_options: []
328
345
  require_paths:
329
346
  - lib
@@ -338,12 +355,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
338
355
  - !ruby/object:Gem::Version
339
356
  version: '0'
340
357
  requirements: []
341
- rubygems_version: 3.1.4
342
- signing_key:
358
+ rubygems_version: 3.1.6
359
+ signing_key:
343
360
  specification_version: 4
344
361
  summary: mail_room will proxy email (gmail) from IMAP to a callback URL, logger, or
345
362
  letter_opener
346
363
  test_files:
364
+ - spec/fixtures/jwt_secret
347
365
  - spec/fixtures/test_config.yml
348
366
  - spec/lib/arbitration/redis_spec.rb
349
367
  - spec/lib/cli_spec.rb
@@ -358,6 +376,7 @@ test_files:
358
376
  - spec/lib/health_check_spec.rb
359
377
  - spec/lib/imap/connection_spec.rb
360
378
  - spec/lib/imap/message_spec.rb
379
+ - spec/lib/jwt_spec.rb
361
380
  - spec/lib/logger/structured_spec.rb
362
381
  - spec/lib/mailbox_spec.rb
363
382
  - spec/lib/mailbox_watcher_spec.rb