gitlab-mail_room 0.0.6 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ require "mail_room/connection"
2
+
1
3
  module MailRoom
2
4
  # TODO: split up between processing and idling?
3
5
 
@@ -49,14 +51,25 @@ module MailRoom
49
51
  @connection = nil
50
52
  end
51
53
 
54
+ @mailbox.logger.info({ context: @mailbox.context, action: "Terminating watching thread..." })
55
+
52
56
  if self.watching_thread
53
- self.watching_thread.join
57
+ thr = self.watching_thread.join(60)
58
+ @mailbox.logger.info({ context: @mailbox.context, action: "Timeout waiting for watching thread" }) unless thr
54
59
  end
60
+
61
+ @mailbox.logger.info({ context: @mailbox.context, action: "Done with thread cleanup" })
55
62
  end
56
63
 
57
64
  private
65
+
58
66
  def connection
59
- @connection ||= 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
60
73
  end
61
74
  end
62
75
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailRoom
4
+ class Message
5
+ attr_reader :uid, :body
6
+
7
+ def initialize(uid:, body:)
8
+ @uid = uid
9
+ @body = body
10
+ end
11
+
12
+ def ==(other)
13
+ self.class == other.class && uid == other.uid && body == other.body
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailRoom
4
+ module MicrosoftGraph
5
+ autoload :Connection, 'mail_room/microsoft_graph/connection'
6
+ end
7
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'oauth2'
5
+
6
+ module MailRoom
7
+ module MicrosoftGraph
8
+ class Connection < MailRoom::Connection
9
+ SCOPE = 'https://graph.microsoft.com/.default'
10
+ NEXT_PAGE_KEY = '@odata.nextLink'
11
+ DEFAULT_POLL_INTERVAL_S = 60
12
+
13
+ TooManyRequestsError = Class.new(RuntimeError)
14
+
15
+ attr_accessor :token, :throttled_count
16
+
17
+ def initialize(mailbox)
18
+ super
19
+
20
+ reset
21
+ setup
22
+ end
23
+
24
+ def wait
25
+ process_mailbox
26
+
27
+ @throttled_count = 0
28
+ wait_for_new_messages
29
+ rescue TooManyRequestsError => e
30
+ @throttled_count += 1
31
+
32
+ @mailbox.logger.warn({ context: @mailbox.context, action: 'Too many requests, backing off...', backoff_s: backoff_secs, error: e.message, error_backtrace: e.backtrace })
33
+
34
+ backoff
35
+ rescue OAuth2::Error, IOError => e
36
+ @mailbox.logger.warn({ context: @mailbox.context, action: 'Disconnected. Resetting...', error: e.message, error_backtrace: e.backtrace })
37
+
38
+ reset
39
+ setup
40
+ end
41
+
42
+ private
43
+
44
+ def wait_for_new_messages
45
+ sleep poll_interval
46
+ end
47
+
48
+ def backoff
49
+ sleep backoff_secs
50
+ end
51
+
52
+ def backoff_secs
53
+ [60 * 10, 2**throttled_count].min
54
+ end
55
+
56
+ def reset
57
+ @token = nil
58
+ @throttled_count = 0
59
+ end
60
+
61
+ def setup
62
+ @mailbox.logger.info({ context: @mailbox.context, action: 'Retrieving OAuth2 token...' })
63
+
64
+ @token = client.client_credentials.get_token({ scope: SCOPE })
65
+ end
66
+
67
+ def client
68
+ @client ||= OAuth2::Client.new(client_id, client_secret,
69
+ site: 'https://login.microsoftonline.com',
70
+ authorize_url: "/#{tenant_id}/oauth2/v2.0/authorize",
71
+ token_url: "/#{tenant_id}/oauth2/v2.0/token",
72
+ auth_scheme: :basic_auth)
73
+ end
74
+
75
+ def inbox_options
76
+ mailbox.inbox_options
77
+ end
78
+
79
+ def tenant_id
80
+ inbox_options[:tenant_id]
81
+ end
82
+
83
+ def client_id
84
+ inbox_options[:client_id]
85
+ end
86
+
87
+ def client_secret
88
+ inbox_options[:client_secret]
89
+ end
90
+
91
+ def poll_interval
92
+ @poll_interval ||= begin
93
+ interval = inbox_options[:poll_interval].to_i
94
+
95
+ if interval.positive?
96
+ interval
97
+ else
98
+ DEFAULT_POLL_INTERVAL_S
99
+ end
100
+ end
101
+ end
102
+
103
+ def process_mailbox
104
+ return unless @new_message_handler
105
+
106
+ @mailbox.logger.info({ context: @mailbox.context, action: 'Processing started' })
107
+
108
+ new_messages.each do |msg|
109
+ success = @new_message_handler.call(msg)
110
+ handle_delivered(msg) if success
111
+ end
112
+ end
113
+
114
+ def handle_delivered(msg)
115
+ mark_as_read(msg)
116
+ delete_message(msg) if @mailbox.delete_after_delivery
117
+ end
118
+
119
+ def delete_message(msg)
120
+ token.delete(msg_url(msg.uid))
121
+ end
122
+
123
+ def mark_as_read(msg)
124
+ token.patch(msg_url(msg.uid),
125
+ headers: { 'Content-Type' => 'application/json' },
126
+ body: { isRead: true }.to_json)
127
+ end
128
+
129
+ def new_messages
130
+ messages_for_ids(new_message_ids)
131
+ end
132
+
133
+ # Yields a page of message IDs at a time
134
+ def new_message_ids
135
+ url = unread_messages_url
136
+
137
+ Enumerator.new do |block|
138
+ loop do
139
+ messages, next_page_url = unread_messages(url: url)
140
+ messages.each { |msg| block.yield msg }
141
+
142
+ break unless next_page_url
143
+
144
+ url = next_page_url
145
+ end
146
+ end
147
+ end
148
+
149
+ def unread_messages(url:)
150
+ body = get(url)
151
+
152
+ return [[], nil] unless body
153
+
154
+ all_unread = body['value'].map { |msg| msg['id'] }
155
+ to_deliver = all_unread.select { |uid| @mailbox.deliver?(uid) }
156
+ @mailbox.logger.info({ context: @mailbox.context, action: 'Getting new messages',
157
+ unread: { count: all_unread.count, ids: all_unread },
158
+ to_be_delivered: { count: to_deliver.count, ids: to_deliver } })
159
+ [to_deliver, body[NEXT_PAGE_KEY]]
160
+ rescue TypeError, JSON::ParserError => e
161
+ log_exception('Error parsing JSON response', e)
162
+ [[], nil]
163
+ end
164
+
165
+ # Returns the JSON response
166
+ def get(url)
167
+ response = token.get(url, { raise_errors: false })
168
+
169
+ case response.status
170
+ when 429
171
+ raise TooManyRequestsError
172
+ when 400..599
173
+ raise OAuth2::Error, response
174
+ end
175
+
176
+ return unless response.body
177
+
178
+ body = JSON.parse(response.body)
179
+
180
+ raise TypeError, 'Response did not contain value hash' unless body.is_a?(Hash) && body.key?('value')
181
+
182
+ body
183
+ end
184
+
185
+ def messages_for_ids(message_ids)
186
+ message_ids.each_with_object([]) do |id, arr|
187
+ response = token.get(rfc822_msg_url(id))
188
+
189
+ arr << ::MailRoom::Message.new(uid: id, body: response.body)
190
+ end
191
+ end
192
+
193
+ def base_url
194
+ "https://graph.microsoft.com/v1.0/users/#{mailbox.email}/mailFolders/#{mailbox.name}/messages"
195
+ end
196
+
197
+ def unread_messages_url
198
+ "#{base_url}?$filter=isRead eq false"
199
+ end
200
+
201
+ def msg_url(id)
202
+ # Attempting to use the base_url fails with "The OData request is not supported"
203
+ "https://graph.microsoft.com/v1.0/users/#{mailbox.email}/messages/#{id}"
204
+ end
205
+
206
+ def rfc822_msg_url(id)
207
+ # Attempting to use the base_url fails with "The OData request is not supported"
208
+ "#{msg_url(id)}/$value"
209
+ end
210
+
211
+ def log_exception(message, exception)
212
+ @mailbox.logger.warn({ context: @mailbox.context, message: message, exception: exception.to_s })
213
+ end
214
+ end
215
+ end
216
+ end
@@ -1,4 +1,4 @@
1
1
  module MailRoom
2
2
  # Current version of gitlab-mail_room gem
3
- VERSION = "0.0.6"
3
+ VERSION = "0.0.11"
4
4
  end
data/mail_room.gemspec CHANGED
@@ -17,10 +17,14 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
19
 
20
+ gem.add_dependency "net-imap", ">= 0.2.1"
21
+ gem.add_dependency "oauth2", "~> 1.4.4"
22
+
20
23
  gem.add_development_dependency "rake"
21
24
  gem.add_development_dependency "rspec", "~> 3.9"
22
25
  gem.add_development_dependency "mocha", "~> 1.11"
23
26
  gem.add_development_dependency "simplecov"
27
+ gem.add_development_dependency "webrick", "~> 1.6"
24
28
 
25
29
  # for testing delivery methods
26
30
  gem.add_development_dependency "faraday"
@@ -30,4 +34,5 @@ Gem::Specification.new do |gem|
30
34
  gem.add_development_dependency "redis-namespace"
31
35
  gem.add_development_dependency "pg"
32
36
  gem.add_development_dependency "charlock_holmes"
37
+ gem.add_development_dependency "webmock"
33
38
  end
@@ -1,4 +1,7 @@
1
1
  ---
2
+ :health_check:
3
+ :address: "127.0.0.1"
4
+ :port: 8080
2
5
  :mailboxes:
3
6
  -
4
7
  :email: "user1@gmail.com"
data/spec/lib/cli_spec.rb CHANGED
@@ -5,14 +5,14 @@ describe MailRoom::CLI do
5
5
  let!(:configuration) {MailRoom::Configuration.new({:config_path => config_path})}
6
6
  let(:coordinator) {stub(:run => true, :quit => true)}
7
7
  let(:configuration_args) { anything }
8
- let(:coordinator_args) { anything }
8
+ let(:coordinator_args) { [anything, anything] }
9
9
 
10
10
  describe '.new' do
11
11
  let(:args) {["-c", "a path"]}
12
12
 
13
13
  before :each do
14
14
  MailRoom::Configuration.expects(:new).with(configuration_args).returns(configuration)
15
- MailRoom::Coordinator.stubs(:new).with(coordinator_args).returns(coordinator)
15
+ MailRoom::Coordinator.stubs(:new).with(*coordinator_args).returns(coordinator)
16
16
  end
17
17
 
18
18
  context 'with configuration args' do
@@ -27,7 +27,7 @@ describe MailRoom::CLI do
27
27
 
28
28
  context 'with coordinator args' do
29
29
  let(:coordinator_args) do
30
- configuration.mailboxes
30
+ [configuration.mailboxes, anything]
31
31
  end
32
32
 
33
33
  it 'creates a new coordinator with configuration' do
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  describe MailRoom::Configuration do
4
4
  let(:config_path) {File.expand_path('../fixtures/test_config.yml', File.dirname(__FILE__))}
5
5
 
6
- describe 'set_mailboxes' do
6
+ describe '#initalize' do
7
7
  context 'with config_path' do
8
8
  let(:configuration) { MailRoom::Configuration.new(:config_path => config_path) }
9
9
 
@@ -12,6 +12,10 @@ describe MailRoom::Configuration do
12
12
 
13
13
  expect(configuration.mailboxes).to eq(['mailbox1', 'mailbox2'])
14
14
  end
15
+
16
+ it 'parses health check' do
17
+ expect(configuration.health_check).to be_a(MailRoom::HealthCheck)
18
+ end
15
19
  end
16
20
 
17
21
  context 'without config_path' do
@@ -23,6 +27,10 @@ describe MailRoom::Configuration do
23
27
 
24
28
  expect(configuration.mailboxes).to eq([])
25
29
  end
30
+
31
+ it 'sets the health check to nil' do
32
+ expect(configuration.health_check).to be_nil
33
+ end
26
34
  end
27
35
  end
28
36
  end
@@ -15,6 +15,13 @@ describe MailRoom::Coordinator do
15
15
  coordinator = MailRoom::Coordinator.new([])
16
16
  expect(coordinator.watchers).to eq([])
17
17
  end
18
+
19
+ it 'sets the health check' do
20
+ health_check = MailRoom::HealthCheck.new({ address: '127.0.0.1', port: 8080})
21
+ coordinator = MailRoom::Coordinator.new([], health_check)
22
+
23
+ expect(coordinator.health_check).to eq(health_check)
24
+ end
18
25
  end
19
26
 
20
27
  describe '#run' do
@@ -22,15 +29,22 @@ describe MailRoom::Coordinator do
22
29
  watcher = stub
23
30
  watcher.stubs(:run)
24
31
  watcher.stubs(:quit)
32
+
33
+ health_check = stub
34
+ health_check.stubs(:run)
35
+ health_check.stubs(:quit)
36
+
25
37
  MailRoom::MailboxWatcher.stubs(:new).returns(watcher)
26
- coordinator = MailRoom::Coordinator.new(['mailbox1'])
38
+ coordinator = MailRoom::Coordinator.new(['mailbox1'], health_check)
27
39
  coordinator.stubs(:sleep_while_running)
28
40
  watcher.expects(:run)
29
41
  watcher.expects(:quit)
42
+ health_check.expects(:run)
43
+ health_check.expects(:quit)
30
44
 
31
45
  coordinator.run
32
46
  end
33
-
47
+
34
48
  it 'should go to sleep after running watchers' do
35
49
  coordinator = MailRoom::Coordinator.new([])
36
50
  coordinator.stubs(:running=)
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe MailRoom::HealthCheck do
6
+ let(:address) { '127.0.0.1' }
7
+ let(:port) { 8000 }
8
+ let(:params) { { address: address, port: port } }
9
+ subject { described_class.new(params) }
10
+
11
+ describe '#initialize' do
12
+ context 'with valid parameters' do
13
+ it 'validates successfully' do
14
+ expect(subject).to be_a(described_class)
15
+ end
16
+ end
17
+
18
+ context 'with invalid address' do
19
+ let(:address) { nil }
20
+
21
+ it 'raises an error' do
22
+ expect { subject }.to raise_error('No health check address specified')
23
+ end
24
+ end
25
+
26
+ context 'with invalid port' do
27
+ let(:port) { nil }
28
+
29
+ it 'raises an error' do
30
+ expect { subject }.to raise_error('Health check port 0 is invalid')
31
+ end
32
+ end
33
+ end
34
+
35
+ describe '#run' do
36
+ it 'sets running to true' do
37
+ server = stub(start: true)
38
+ subject.stubs(:create_server).returns(server)
39
+
40
+ subject.run
41
+
42
+ expect(subject.running).to be true
43
+ end
44
+ end
45
+
46
+ describe '#quit' do
47
+ it 'sets running to false' do
48
+ server = stub(start: true, shutdown: true)
49
+ subject.stubs(:create_server).returns(server)
50
+
51
+ subject.run
52
+ subject.quit
53
+
54
+ expect(subject.running).to be false
55
+ end
56
+ end
57
+ end