gitlab-mail_room 0.0.10 → 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,12 +3,23 @@ require 'spec_helper'
3
3
  describe MailRoom::Mailbox do
4
4
  let(:sample_message) { MailRoom::Message.new(uid: 123, body: 'a message') }
5
5
 
6
+ context 'with IMAP configuration' do
7
+ subject { build_mailbox }
8
+
9
+ describe '#imap?' do
10
+ it 'configured as an IMAP inbox' do
11
+ expect(subject.imap?).to be true
12
+ expect(subject.microsoft_graph?).to be false
13
+ end
14
+ end
15
+ end
16
+
6
17
  describe "#deliver" do
7
18
  context "with arbitration_method of noop" do
8
19
  it 'arbitrates with a Noop instance' do
9
- mailbox = build_mailbox({:arbitration_method => 'noop'})
20
+ mailbox = build_mailbox({arbitration_method: 'noop'})
10
21
  noop = stub(:deliver?)
11
- MailRoom::Arbitration['noop'].stubs(:new => noop)
22
+ MailRoom::Arbitration['noop'].stubs(new: noop)
12
23
 
13
24
  uid = 123
14
25
 
@@ -20,9 +31,9 @@ describe MailRoom::Mailbox do
20
31
 
21
32
  context "with arbitration_method of redis" do
22
33
  it 'arbitrates with a Redis instance' do
23
- mailbox = build_mailbox({:arbitration_method => 'redis'})
34
+ mailbox = build_mailbox({arbitration_method: 'redis'})
24
35
  redis = stub(:deliver?)
25
- MailRoom::Arbitration['redis'].stubs(:new => redis)
36
+ MailRoom::Arbitration['redis'].stubs(new: redis)
26
37
  uid = 123
27
38
  redis.expects(:deliver?).with(uid)
28
39
 
@@ -32,9 +43,9 @@ describe MailRoom::Mailbox do
32
43
 
33
44
  context "with delivery_method of noop" do
34
45
  it 'delivers with a Noop instance' do
35
- mailbox = build_mailbox({:delivery_method => 'noop'})
46
+ mailbox = build_mailbox({delivery_method: 'noop'})
36
47
  noop = stub(:deliver)
37
- MailRoom::Delivery['noop'].stubs(:new => noop)
48
+ MailRoom::Delivery['noop'].stubs(new: noop)
38
49
 
39
50
  noop.expects(:deliver).with(sample_message.body)
40
51
 
@@ -44,9 +55,9 @@ describe MailRoom::Mailbox do
44
55
 
45
56
  context "with delivery_method of logger" do
46
57
  it 'delivers with a Logger instance' do
47
- mailbox = build_mailbox({:delivery_method => 'logger'})
58
+ mailbox = build_mailbox({delivery_method: 'logger'})
48
59
  logger = stub(:deliver)
49
- MailRoom::Delivery['logger'].stubs(:new => logger)
60
+ MailRoom::Delivery['logger'].stubs(new: logger)
50
61
 
51
62
  logger.expects(:deliver).with(sample_message.body)
52
63
 
@@ -56,9 +67,9 @@ describe MailRoom::Mailbox do
56
67
 
57
68
  context "with delivery_method of postback" do
58
69
  it 'delivers with a Postback instance' do
59
- mailbox = build_mailbox({:delivery_method => 'postback'})
70
+ mailbox = build_mailbox({delivery_method: 'postback'})
60
71
  postback = stub(:deliver)
61
- MailRoom::Delivery['postback'].stubs(:new => postback)
72
+ MailRoom::Delivery['postback'].stubs(new: postback)
62
73
 
63
74
  postback.expects(:deliver).with(sample_message.body)
64
75
 
@@ -68,9 +79,9 @@ describe MailRoom::Mailbox do
68
79
 
69
80
  context "with delivery_method of letter_opener" do
70
81
  it 'delivers with a LetterOpener instance' do
71
- mailbox = build_mailbox({:delivery_method => 'letter_opener'})
82
+ mailbox = build_mailbox({delivery_method: 'letter_opener'})
72
83
  letter_opener = stub(:deliver)
73
- MailRoom::Delivery['letter_opener'].stubs(:new => letter_opener)
84
+ MailRoom::Delivery['letter_opener'].stubs(new: letter_opener)
74
85
 
75
86
  letter_opener.expects(:deliver).with(sample_message.body)
76
87
 
@@ -82,7 +93,7 @@ describe MailRoom::Mailbox do
82
93
  it "doesn't deliver the message" do
83
94
  mailbox = build_mailbox({ name: "magic mailbox", delivery_method: 'noop' })
84
95
  noop = stub(:deliver)
85
- MailRoom::Delivery['noop'].stubs(:new => noop)
96
+ MailRoom::Delivery['noop'].stubs(new: noop)
86
97
  noop.expects(:deliver).never
87
98
 
88
99
  mailbox.deliver(MailRoom::Message.new(uid: 1234, body: nil))
@@ -91,9 +102,9 @@ describe MailRoom::Mailbox do
91
102
 
92
103
  context "with ssl options hash" do
93
104
  it 'replaces verify mode with constant' do
94
- mailbox = build_mailbox({:ssl => {:verify_mode => :none}})
105
+ mailbox = build_mailbox({ssl: {verify_mode: :none}})
95
106
 
96
- expect(mailbox.ssl_options).to eq({:verify_mode => OpenSSL::SSL::VERIFY_NONE})
107
+ expect(mailbox.ssl_options).to eq({verify_mode: OpenSSL::SSL::VERIFY_NONE})
97
108
  end
98
109
  end
99
110
 
@@ -121,8 +132,45 @@ describe MailRoom::Mailbox do
121
132
  describe "#validate!" do
122
133
  context "with missing configuration" do
123
134
  it 'raises an error' do
124
- expect { build_mailbox({:name => nil}) }.to raise_error(MailRoom::ConfigurationError)
125
- expect { build_mailbox({:host => nil}) }.to raise_error(MailRoom::ConfigurationError)
135
+ expect { build_mailbox({name: nil}) }.to raise_error(MailRoom::ConfigurationError)
136
+ expect { build_mailbox({host: nil}) }.to raise_error(MailRoom::ConfigurationError)
137
+ end
138
+ end
139
+
140
+ context "with Microsoft Graph configuration" do
141
+ let(:options) do
142
+ {
143
+ arbitration_method: 'redis',
144
+ }.merge(REQUIRED_MICROSOFT_GRAPH_DEFAULTS)
145
+ end
146
+
147
+ subject { build_mailbox(options) }
148
+
149
+ def delete_inbox_option(key)
150
+ options[:inbox_options] = options[:inbox_options].dup.delete(key)
151
+ end
152
+
153
+ it 'allows password omission' do
154
+ expect { subject }.not_to raise_error
155
+ end
156
+
157
+ it 'configured as a Microsoft Graph inbox' do
158
+ expect(subject.imap?).to be false
159
+ expect(subject.microsoft_graph?).to be true
160
+ end
161
+
162
+ it 'raises an error when the inbox options are not present' do
163
+ options.delete(:inbox_options)
164
+
165
+ expect { subject }.to raise_error(MailRoom::ConfigurationError)
166
+ end
167
+
168
+ %i[tenant_id client_id client_secret].each do |item|
169
+ it "raises an error when the #{item} is not present" do
170
+ delete_inbox_option(item)
171
+
172
+ expect { subject }.to raise_error(MailRoom::ConfigurationError)
173
+ end
126
174
  end
127
175
  end
128
176
  end
@@ -1,61 +1,77 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe MailRoom::MailboxWatcher do
4
- let(:mailbox) {build_mailbox}
5
-
6
- describe '#running?' do
7
- it 'is false by default' do
8
- watcher = MailRoom::MailboxWatcher.new(mailbox)
9
- expect(watcher.running?).to eq(false)
4
+ context 'with IMAP configured' do
5
+ let(:mailbox) {build_mailbox}
6
+
7
+ describe '#running?' do
8
+ it 'is false by default' do
9
+ watcher = MailRoom::MailboxWatcher.new(mailbox)
10
+ expect(watcher.running?).to eq(false)
11
+ end
10
12
  end
11
- end
12
13
 
13
- describe '#run' do
14
- let(:imap) {stub(:login => true, :select => true)}
15
- let(:watcher) {MailRoom::MailboxWatcher.new(mailbox)}
14
+ describe '#run' do
15
+ let(:imap) {stub(login: true, select: true)}
16
+ let(:watcher) {MailRoom::MailboxWatcher.new(mailbox)}
16
17
 
17
- before :each do
18
- Net::IMAP.stubs(:new).returns(imap) # prevent connection
19
- end
18
+ before :each do
19
+ Net::IMAP.stubs(:new).returns(imap) # prevent connection
20
+ end
20
21
 
21
- it 'loops over wait while running' do
22
- connection = MailRoom::IMAP::Connection.new(mailbox)
22
+ it 'loops over wait while running' do
23
+ connection = MailRoom::IMAP::Connection.new(mailbox)
23
24
 
24
- MailRoom::IMAP::Connection.stubs(:new).returns(connection)
25
+ MailRoom::IMAP::Connection.stubs(:new).returns(connection)
25
26
 
26
- watcher.expects(:running?).twice.returns(true, false)
27
- connection.expects(:wait).once
28
- connection.expects(:on_new_message).once
27
+ watcher.expects(:running?).twice.returns(true, false)
28
+ connection.expects(:wait).once
29
+ connection.expects(:on_new_message).once
29
30
 
30
- watcher.run
31
- watcher.watching_thread.join # wait for finishing run
31
+ watcher.run
32
+ watcher.watching_thread.join # wait for finishing run
33
+ end
32
34
  end
33
- end
34
35
 
35
- describe '#quit' do
36
- let(:imap) {stub(:login => true, :select => true)}
37
- let(:watcher) {MailRoom::MailboxWatcher.new(mailbox)}
36
+ describe '#quit' do
37
+ let(:imap) {stub(login: true, select: true)}
38
+ let(:watcher) {MailRoom::MailboxWatcher.new(mailbox)}
38
39
 
39
- before :each do
40
- Net::IMAP.stubs(:new).returns(imap) # prevent connection
41
- end
40
+ before :each do
41
+ Net::IMAP.stubs(:new).returns(imap) # prevent connection
42
+ end
43
+
44
+ it 'closes and waits for the connection' do
45
+ connection = MailRoom::IMAP::Connection.new(mailbox)
46
+ connection.stubs(:wait)
47
+ connection.stubs(:quit)
42
48
 
43
- it 'closes and waits for the connection' do
44
- connection = MailRoom::IMAP::Connection.new(mailbox)
45
- connection.stubs(:wait)
46
- connection.stubs(:quit)
49
+ MailRoom::IMAP::Connection.stubs(:new).returns(connection)
47
50
 
48
- MailRoom::IMAP::Connection.stubs(:new).returns(connection)
51
+ watcher.run
52
+
53
+ expect(watcher.running?).to eq(true)
54
+
55
+ connection.expects(:quit)
56
+
57
+ watcher.quit
58
+
59
+ expect(watcher.running?).to eq(false)
60
+ end
61
+ end
62
+ end
49
63
 
50
- watcher.run
64
+ context 'with Microsoft Graph configured' do
65
+ let(:mailbox) { build_mailbox(REQUIRED_MICROSOFT_GRAPH_DEFAULTS) }
51
66
 
52
- expect(watcher.running?).to eq(true)
67
+ subject { described_class.new(mailbox) }
53
68
 
54
- connection.expects(:quit)
69
+ it 'initializes a Microsoft Graph connection' do
70
+ connection = stub(on_new_message: nil)
55
71
 
56
- watcher.quit
72
+ MailRoom::MicrosoftGraph::Connection.stubs(:new).returns(connection)
57
73
 
58
- expect(watcher.running?).to eq(false)
74
+ expect(subject.send(:connection)).to eq(connection)
59
75
  end
60
76
  end
61
77
  end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'spec_helper'
5
+ require 'json'
6
+ require 'webmock/rspec'
7
+
8
+ describe MailRoom::MicrosoftGraph::Connection do
9
+ let(:tenant_id) { options[:inbox_options][:tenant_id] }
10
+ let(:options) do
11
+ {
12
+ delete_after_delivery: true,
13
+ expunge_deleted: true
14
+ }.merge(REQUIRED_MICROSOFT_GRAPH_DEFAULTS)
15
+ end
16
+ let(:mailbox) { build_mailbox(options) }
17
+ let(:base_url) { 'https://graph.microsoft.com/v1.0/users/user@example.com/mailFolders/inbox/messages' }
18
+ let(:message_base_url) { 'https://graph.microsoft.com/v1.0/users/user@example.com/messages' }
19
+
20
+ before do
21
+ WebMock.enable!
22
+ end
23
+
24
+ context '#wait' do
25
+ let(:connection) { described_class.new(mailbox) }
26
+ let(:uid) { 1 }
27
+ let(:access_token) { SecureRandom.hex }
28
+ let(:refresh_token) { SecureRandom.hex }
29
+ let(:expires_in) { Time.now + 3600 }
30
+ let(:unread_messages_body) { '' }
31
+ let(:status) { 200 }
32
+ let!(:stub_token) do
33
+ stub_request(:post, "https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token").to_return(
34
+ body: { 'access_token' => access_token, 'refresh_token' => refresh_token, 'expires_in' => expires_in }.to_json,
35
+ headers: { 'Content-Type' => 'application/json' }
36
+ )
37
+ end
38
+ let!(:stub_unread_messages_request) do
39
+ stub_request(:get, "#{base_url}?$filter=isRead%20eq%20false").to_return(
40
+ status: status,
41
+ body: unread_messages_body.to_json,
42
+ headers: { 'Content-Type' => 'application/json' }
43
+ )
44
+ end
45
+
46
+ before do
47
+ connection.stubs(:wait_for_new_messages)
48
+ end
49
+
50
+ describe 'poll interval' do
51
+ it 'defaults to 60 seconds' do
52
+ expect(connection.send(:poll_interval)).to eq(60)
53
+ end
54
+
55
+ context 'interval set to 10' do
56
+ let(:options) do
57
+ {
58
+ inbox_method: :microsoft_graph,
59
+ inbox_options: {
60
+ tenant_id: '98776',
61
+ client_id: '12345',
62
+ client_secret: 'MY-SECRET',
63
+ poll_interval: '10'
64
+ }
65
+ }
66
+ end
67
+
68
+ it 'sets the poll interval to 10' do
69
+ expect(connection.send(:poll_interval)).to eq(10)
70
+ end
71
+ end
72
+ end
73
+
74
+ context 'with a single message' do
75
+ let(:message_id) { SecureRandom.hex }
76
+ let(:unread_messages_body) { { value: ['id' => message_id] } }
77
+ let(:message_url) { "#{message_base_url}/#{message_id}" }
78
+ let(:message_body) { 'hello world' }
79
+
80
+ it 'requests message ID' do
81
+ stub_get = stub_request(:get, "#{message_url}/$value").to_return(
82
+ status: 200,
83
+ body: message_body
84
+ )
85
+ stub_patch = stub_request(:patch, message_url).with(body: { "isRead": true }.to_json)
86
+ stub_delete = stub_request(:delete, message_url)
87
+ message_count = 0
88
+
89
+ connection.on_new_message do |message|
90
+ message_count += 1
91
+ expect(message.uid).to eq(message_id)
92
+ expect(message.body).to eq(message_body)
93
+ end
94
+
95
+ connection.wait
96
+
97
+ assert_requested(stub_token)
98
+ assert_requested(stub_unread_messages_request)
99
+ assert_requested(stub_get)
100
+ assert_requested(stub_patch)
101
+ assert_requested(stub_delete)
102
+ expect(message_count).to eq(1)
103
+ end
104
+ end
105
+
106
+ context 'with multiple pages of messages' do
107
+ let(:message_ids) { [SecureRandom.hex, SecureRandom.hex] }
108
+ let(:next_page_url) { 'https://graph.microsoft.com/v1.0/nextPage' }
109
+ let(:unread_messages_body) { { value: ['id' => message_ids.first], '@odata.nextLink' => next_page_url } }
110
+ let(:message_body) { 'hello world' }
111
+
112
+ it 'requests message ID' do
113
+ stub_request(:get, next_page_url).to_return(
114
+ status: 200,
115
+ body: { value: ['id' => message_ids[1]] }.to_json
116
+ )
117
+
118
+ stubs = []
119
+ message_ids.each do |message_id|
120
+ rfc822_msg_url = "#{message_base_url}/#{message_id}/$value"
121
+ stubs << stub_request(:get, rfc822_msg_url).to_return(
122
+ status: 200,
123
+ body: message_body
124
+ )
125
+
126
+ msg_url = "#{message_base_url}/#{message_id}"
127
+ stubs << stub_request(:patch, msg_url).with(body: { "isRead": true }.to_json)
128
+ stubs << stub_request(:delete, msg_url)
129
+ end
130
+
131
+ message_count = 0
132
+
133
+ connection.on_new_message do |message|
134
+ expect(message.uid).to eq(message_ids[message_count])
135
+ expect(message.body).to eq(message_body)
136
+ message_count += 1
137
+ end
138
+
139
+ connection.wait
140
+
141
+ stubs.each { |stub| assert_requested(stub) }
142
+ expect(message_count).to eq(2)
143
+ end
144
+ end
145
+
146
+ shared_examples 'request backoff' do
147
+ it 'backs off' do
148
+ connection.expects(:backoff)
149
+
150
+ connection.on_new_message {}
151
+ connection.wait
152
+
153
+ expect(connection.throttled_count).to eq(1)
154
+ end
155
+ end
156
+
157
+ context 'too many requests' do
158
+ let(:status) { 429 }
159
+
160
+ it_behaves_like 'request backoff'
161
+ end
162
+
163
+ context 'too much bandwidth' do
164
+ let(:status) { 509 }
165
+
166
+ it_behaves_like 'request backoff'
167
+ end
168
+
169
+ context 'invalid JSON response' do
170
+ let(:body) { 'this is something' }
171
+
172
+ it 'ignores the message and logs a warning' do
173
+ mailbox.logger.expects(:warn)
174
+
175
+ connection.on_new_message {}
176
+ connection.wait
177
+ end
178
+ end
179
+
180
+ context '500 error' do
181
+ let(:status) { 500 }
182
+
183
+ it 'terminates due to error' do
184
+ connection.on_new_message {}
185
+
186
+ expect { connection.wait }.to raise_error(OAuth2::Error)
187
+ end
188
+ end
189
+ end
190
+ end
data/spec/spec_helper.rb CHANGED
@@ -22,11 +22,21 @@ RSpec.configure do |config|
22
22
  end
23
23
 
24
24
  REQUIRED_MAILBOX_DEFAULTS = {
25
- :name => "inbox",
26
- :email => "user@example.com",
27
- :password => "password123"
25
+ name: "inbox",
26
+ email: "user@example.com",
27
+ password: "password123"
28
28
  }
29
29
 
30
+ REQUIRED_MICROSOFT_GRAPH_DEFAULTS = {
31
+ password: nil,
32
+ inbox_method: :microsoft_graph,
33
+ inbox_options: {
34
+ tenant_id: '98776',
35
+ client_id: '12345',
36
+ client_secret: 'MY-SECRET',
37
+ }.freeze
38
+ }.freeze
39
+
30
40
  def build_mailbox(options = {})
31
41
  MailRoom::Mailbox.new(REQUIRED_MAILBOX_DEFAULTS.merge(options))
32
42
  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.10
4
+ version: 0.0.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Pitale
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-17 00:00:00.000000000 Z
11
+ date: 2021-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-imap
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.2.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: oauth2
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.4.4
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.4.4
41
+ - !ruby/object:Gem::Dependency
42
+ name: io-wait
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.1.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.1.0
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: rake
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +80,20 @@ dependencies:
52
80
  - - "~>"
53
81
  - !ruby/object:Gem::Version
54
82
  version: '3.9'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.11'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.11'
55
97
  - !ruby/object:Gem::Dependency
56
98
  name: mocha
57
99
  requirement: !ruby/object:Gem::Requirement
@@ -192,6 +234,20 @@ dependencies:
192
234
  - - ">="
193
235
  - !ruby/object:Gem::Version
194
236
  version: '0'
237
+ - !ruby/object:Gem::Dependency
238
+ name: webmock
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - ">="
242
+ - !ruby/object:Gem::Version
243
+ version: '0'
244
+ type: :development
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ version: '0'
195
251
  description: mail_room will proxy email (gmail) from IMAP to a delivery method
196
252
  email:
197
253
  - tpitale@gmail.com
@@ -203,6 +259,8 @@ files:
203
259
  - ".gitignore"
204
260
  - ".gitlab-ci.yml"
205
261
  - ".gitlab/issue_templates/Release.md"
262
+ - ".rubocop.yml"
263
+ - ".rubocop_todo.yml"
206
264
  - ".ruby-version"
207
265
  - ".travis.yml"
208
266
  - CHANGELOG.md
@@ -236,6 +294,8 @@ files:
236
294
  - lib/mail_room/mailbox.rb
237
295
  - lib/mail_room/mailbox_watcher.rb
238
296
  - lib/mail_room/message.rb
297
+ - lib/mail_room/microsoft_graph.rb
298
+ - lib/mail_room/microsoft_graph/connection.rb
239
299
  - lib/mail_room/version.rb
240
300
  - logfile.log
241
301
  - mail_room.gemspec
@@ -257,6 +317,7 @@ files:
257
317
  - spec/lib/mailbox_spec.rb
258
318
  - spec/lib/mailbox_watcher_spec.rb
259
319
  - spec/lib/message_spec.rb
320
+ - spec/lib/microsoft_graph/connection_spec.rb
260
321
  - spec/spec_helper.rb
261
322
  homepage: http://github.com/tpitale/mail_room
262
323
  licenses: []
@@ -300,4 +361,5 @@ test_files:
300
361
  - spec/lib/mailbox_spec.rb
301
362
  - spec/lib/mailbox_watcher_spec.rb
302
363
  - spec/lib/message_spec.rb
364
+ - spec/lib/microsoft_graph/connection_spec.rb
303
365
  - spec/spec_helper.rb