gitlab-mail_room 0.0.3 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailRoom
4
+ class HealthCheck
5
+ attr_reader :address, :port, :running
6
+
7
+ def initialize(attributes = {})
8
+ @address = attributes[:address]
9
+ @port = attributes[:port]
10
+
11
+ validate!
12
+ end
13
+
14
+ def run
15
+ @server = create_server
16
+
17
+ @thread = Thread.new do
18
+ @server.start
19
+ end
20
+
21
+ @thread.abort_on_exception = true
22
+ @running = true
23
+ end
24
+
25
+ def quit
26
+ @running = false
27
+ @server&.shutdown
28
+ @thread&.join(60)
29
+ end
30
+
31
+ private
32
+
33
+ def validate!
34
+ raise 'No health check address specified' unless address
35
+ raise "Health check port #{@port.to_i} is invalid" unless port.to_i.positive?
36
+ end
37
+
38
+ def create_server
39
+ require 'webrick'
40
+
41
+ server = ::WEBrick::HTTPServer.new(Port: port, BindAddress: address, AccessLog: [])
42
+
43
+ server.mount_proc '/liveness' do |_req, res|
44
+ handle_liveness(res)
45
+ end
46
+
47
+ server
48
+ end
49
+
50
+ def handle_liveness(res)
51
+ if @running
52
+ res.status = 200
53
+ res.body = "OK\n"
54
+ else
55
+ res.status = 500
56
+ res.body = "Not running\n"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailRoom
4
+ module IMAP
5
+ autoload :Connection, 'mail_room/imap/connection'
6
+ autoload :Message, 'mail_room/imap/message'
7
+ end
8
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MailRoom
4
+ module IMAP
5
+ class Connection < MailRoom::Connection
6
+ def initialize(mailbox)
7
+ super
8
+
9
+ # log in and set the mailbox
10
+ reset
11
+ setup
12
+ end
13
+
14
+ # is the connection logged in?
15
+ # @return [Boolean]
16
+ def logged_in?
17
+ @logged_in
18
+ end
19
+
20
+ # is the connection blocked idling?
21
+ # @return [Boolean]
22
+ def idling?
23
+ @idling
24
+ end
25
+
26
+ # is the imap connection closed?
27
+ # @return [Boolean]
28
+ def disconnected?
29
+ imap.disconnected?
30
+ end
31
+
32
+ # is the connection ready to idle?
33
+ # @return [Boolean]
34
+ def ready_to_idle?
35
+ logged_in? && !idling?
36
+ end
37
+
38
+ def quit
39
+ stop_idling
40
+ reset
41
+ end
42
+
43
+ def wait
44
+ # in case we missed any between idles
45
+ process_mailbox
46
+
47
+ idle
48
+
49
+ process_mailbox
50
+ rescue Net::IMAP::Error, IOError => e
51
+ @mailbox.logger.warn({ context: @mailbox.context, action: 'Disconnected. Resetting...', error: e.message })
52
+ reset
53
+ setup
54
+ end
55
+
56
+ private
57
+
58
+ def reset
59
+ @imap = nil
60
+ @logged_in = false
61
+ @idling = false
62
+ end
63
+
64
+ def setup
65
+ @mailbox.logger.info({ context: @mailbox.context, action: 'Starting TLS session' })
66
+ start_tls
67
+
68
+ @mailbox.logger.info({ context: @mailbox.context, action: 'Logging into mailbox' })
69
+ log_in
70
+
71
+ @mailbox.logger.info({ context: @mailbox.context, action: 'Setting mailbox' })
72
+ set_mailbox
73
+ end
74
+
75
+ # build a net/imap connection to google imap
76
+ def imap
77
+ @imap ||= Net::IMAP.new(@mailbox.host, port: @mailbox.port, ssl: @mailbox.ssl_options)
78
+ end
79
+
80
+ # start a TLS session
81
+ def start_tls
82
+ imap.starttls if @mailbox.start_tls
83
+ end
84
+
85
+ # send the imap login command to google
86
+ def log_in
87
+ imap.login(@mailbox.email, @mailbox.password)
88
+ @logged_in = true
89
+ end
90
+
91
+ # select the mailbox name we want to use
92
+ def set_mailbox
93
+ imap.select(@mailbox.name) if logged_in?
94
+ end
95
+
96
+ # is the response for a new message?
97
+ # @param response [Net::IMAP::TaggedResponse] the imap response from idle
98
+ # @return [Boolean]
99
+ def message_exists?(response)
100
+ response.respond_to?(:name) && response.name == 'EXISTS'
101
+ end
102
+
103
+ # @private
104
+ def idle_handler
105
+ ->(response) { imap.idle_done if message_exists?(response) }
106
+ end
107
+
108
+ # maintain an imap idle connection
109
+ def idle
110
+ return unless ready_to_idle?
111
+
112
+ @mailbox.logger.info({ context: @mailbox.context, action: 'Idling' })
113
+ @idling = true
114
+
115
+ imap.idle(@mailbox.idle_timeout, &idle_handler)
116
+ ensure
117
+ @idling = false
118
+ end
119
+
120
+ # trigger the idle to finish and wait for the thread to finish
121
+ def stop_idling
122
+ return unless idling?
123
+
124
+ imap.idle_done
125
+
126
+ # idling_thread.join
127
+ # self.idling_thread = nil
128
+ end
129
+
130
+ def process_mailbox
131
+ return unless @new_message_handler
132
+
133
+ @mailbox.logger.info({ context: @mailbox.context, action: 'Processing started' })
134
+
135
+ msgs = new_messages
136
+ any_deletions = msgs.
137
+ # deliver each new message, collect success
138
+ map(&@new_message_handler).
139
+ # include messages with success
140
+ zip(msgs).
141
+ # filter failed deliveries, collect message
142
+ select(&:first).map(&:last).
143
+ # scrub delivered messages
144
+ map { |message| scrub(message) }
145
+ .any?
146
+
147
+ imap.expunge if @mailbox.expunge_deleted && any_deletions
148
+ end
149
+
150
+ def scrub(message)
151
+ if @mailbox.delete_after_delivery
152
+ imap.store(message.seqno, '+FLAGS', [Net::IMAP::DELETED])
153
+ true
154
+ end
155
+ end
156
+
157
+ # @private
158
+ # fetch all messages for the new message ids
159
+ def new_messages
160
+ # Both of these calls may results in
161
+ # imap raising an EOFError, we handle
162
+ # this exception in the watcher
163
+ messages_for_ids(new_message_ids)
164
+ end
165
+
166
+ # TODO: label messages?
167
+ # @imap.store(id, "+X-GM-LABELS", [label])
168
+
169
+ # @private
170
+ # search for all new (unseen) message ids
171
+ # @return [Array<Integer>] message ids
172
+ def new_message_ids
173
+ # uid_search still leaves messages UNSEEN
174
+ all_unread = imap.uid_search(@mailbox.search_command)
175
+
176
+ all_unread = all_unread.slice(0, @mailbox.limit_max_unread) if @mailbox.limit_max_unread.to_i > 0
177
+
178
+ to_deliver = all_unread.select { |uid| @mailbox.deliver?(uid) }
179
+ @mailbox.logger.info({ context: @mailbox.context, action: 'Getting new messages',
180
+ unread: { count: all_unread.count, ids: all_unread }, to_be_delivered: { count: to_deliver.count, ids: to_deliver } })
181
+ to_deliver
182
+ end
183
+
184
+ # @private
185
+ # fetch the email for all given ids in RFC822 format
186
+ # @param ids [Array<Integer>] list of message ids
187
+ # @return [Array<MailRoom::IMAP::Message>] the net/imap messages for the given ids
188
+ def messages_for_ids(uids)
189
+ return [] if uids.empty?
190
+
191
+ # uid_fetch marks as SEEN, will not be re-fetched for UNSEEN
192
+ imap_messages = imap.uid_fetch(uids, 'RFC822')
193
+
194
+ imap_messages.each_with_object([]) do |msg, messages|
195
+ messages << ::MailRoom::IMAP::Message.new(uid: msg.attr['UID'], body: msg.attr['RFC822'], seqno: msg.seqno)
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal:true
2
+
3
+ module MailRoom
4
+ module IMAP
5
+ class Message < MailRoom::Message
6
+ attr_reader :seqno
7
+
8
+ def initialize(uid:, body:, seqno:)
9
+ super(uid: uid, body: body)
10
+
11
+ @seqno = seqno
12
+ end
13
+
14
+ def ==(other)
15
+ super && seqno == other.seqno
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,6 @@
1
1
  require "mail_room/delivery"
2
2
  require "mail_room/arbitration"
3
+ require "mail_room/imap"
3
4
 
4
5
  module MailRoom
5
6
  # Mailbox Configuration fields
@@ -10,6 +11,7 @@ module MailRoom
10
11
  :port,
11
12
  :ssl,
12
13
  :start_tls,
14
+ :limit_max_unread, #to avoid 'Error in IMAP command UID FETCH: Too long argument'
13
15
  :idle_timeout,
14
16
  :search_command,
15
17
  :name,
@@ -50,6 +52,7 @@ module MailRoom
50
52
  :port => 993,
51
53
  :ssl => true,
52
54
  :start_tls => false,
55
+ :limit_max_unread => 0,
53
56
  :idle_timeout => IMAP_IDLE_TIMEOUT,
54
57
  :delete_after_delivery => false,
55
58
  :expunge_deleted => false,
@@ -100,13 +103,13 @@ module MailRoom
100
103
  arbitrator.deliver?(uid)
101
104
  end
102
105
 
103
- # deliver the imap email message
104
- # @param message [Net::IMAP::FetchData]
106
+ # deliver the email message
107
+ # @param message [MailRoom::Message]
105
108
  def deliver(message)
106
- body = message.attr['RFC822']
109
+ body = message.body
107
110
  return true unless body
108
111
 
109
- logger.info({context: context, uid: message.attr['UID'], action: "sending to deliverer", deliverer: delivery.class.name, byte_size: message.attr['RFC822.SIZE']})
112
+ logger.info({context: context, uid: message.uid, action: "sending to deliverer", deliverer: delivery.class.name, byte_size: body.bytesize})
110
113
  delivery.deliver(body)
111
114
  end
112
115
 
@@ -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,19 @@ 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
58
65
  def connection
59
- @connection ||= Connection.new(@mailbox)
66
+ @connection ||= ::MailRoom::IMAP::Connection.new(@mailbox)
60
67
  end
61
68
  end
62
69
  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
@@ -1,4 +1,4 @@
1
1
  module MailRoom
2
- # Current version of MailRoom gem
3
- VERSION = "0.0.3"
2
+ # Current version of gitlab-mail_room gem
3
+ VERSION = "0.0.9"
4
4
  end
data/mail_room.gemspec CHANGED
@@ -18,10 +18,10 @@ Gem::Specification.new do |gem|
18
18
  gem.require_paths = ["lib"]
19
19
 
20
20
  gem.add_development_dependency "rake"
21
- gem.add_development_dependency "rspec"
22
- gem.add_development_dependency "mocha"
23
- gem.add_development_dependency "bourne"
21
+ gem.add_development_dependency "rspec", "~> 3.9"
22
+ gem.add_development_dependency "mocha", "~> 1.11"
24
23
  gem.add_development_dependency "simplecov"
24
+ gem.add_development_dependency "webrick", "~> 1.6"
25
25
 
26
26
  # for testing delivery methods
27
27
  gem.add_development_dependency "faraday"
@@ -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"
@@ -5,7 +5,8 @@ describe MailRoom::Arbitration::Redis do
5
5
  let(:mailbox) {
6
6
  build_mailbox(
7
7
  arbitration_options: {
8
- namespace: "mail_room"
8
+ namespace: "mail_room",
9
+ redis_url: ENV['REDIS_URL']
9
10
  }
10
11
  )
11
12
  }
@@ -78,7 +79,7 @@ describe MailRoom::Arbitration::Redis do
78
79
 
79
80
  context 'redis client connection params' do
80
81
  context 'when only url is present' do
81
- let(:redis_url) { "redis://localhost:6379" }
82
+ let(:redis_url) { ENV.fetch('REDIS_URL', 'redis://localhost:6379') }
82
83
  let(:mailbox) {
83
84
  build_mailbox(
84
85
  arbitration_options: {
data/spec/lib/cli_spec.rb CHANGED
@@ -4,23 +4,35 @@ describe MailRoom::CLI do
4
4
  let(:config_path) {File.expand_path('../fixtures/test_config.yml', File.dirname(__FILE__))}
5
5
  let!(:configuration) {MailRoom::Configuration.new({:config_path => config_path})}
6
6
  let(:coordinator) {stub(:run => true, :quit => true)}
7
+ let(:configuration_args) { anything }
8
+ let(:coordinator_args) { [anything, anything] }
7
9
 
8
10
  describe '.new' do
9
11
  let(:args) {["-c", "a path"]}
10
12
 
11
13
  before :each do
12
- MailRoom::Configuration.stubs(:new).returns(configuration)
13
- MailRoom::Coordinator.stubs(:new).returns(coordinator)
14
+ MailRoom::Configuration.expects(:new).with(configuration_args).returns(configuration)
15
+ MailRoom::Coordinator.stubs(:new).with(*coordinator_args).returns(coordinator)
14
16
  end
15
17
 
16
- it 'parses arguments into configuration' do
17
- expect(MailRoom::CLI.new(args).configuration).to eq(configuration)
18
- expect(MailRoom::Configuration).to have_received(:new).with({:config_path => 'a path'})
18
+ context 'with configuration args' do
19
+ let(:configuration_args) do
20
+ {:config_path => 'a path'}
21
+ end
22
+
23
+ it 'parses arguments into configuration' do
24
+ expect(MailRoom::CLI.new(args).configuration).to eq configuration
25
+ end
19
26
  end
20
27
 
21
- it 'creates a new coordinator with configuration' do
22
- expect(MailRoom::CLI.new(args).coordinator).to eq(coordinator)
23
- expect(MailRoom::Coordinator).to have_received(:new).with(configuration.mailboxes)
28
+ context 'with coordinator args' do
29
+ let(:coordinator_args) do
30
+ [configuration.mailboxes, anything]
31
+ end
32
+
33
+ it 'creates a new coordinator with configuration' do
34
+ expect(MailRoom::CLI.new(args).coordinator).to eq(coordinator)
35
+ end
24
36
  end
25
37
  end
26
38
 
@@ -30,30 +42,33 @@ describe MailRoom::CLI do
30
42
  before :each do
31
43
  cli.configuration = configuration
32
44
  cli.coordinator = coordinator
45
+ cli.stubs(:exit)
33
46
  end
34
47
 
35
48
  it 'starts running the coordinator' do
36
- cli.start
49
+ coordinator.expects(:run)
37
50
 
38
- expect(coordinator).to have_received(:run)
51
+ cli.start
39
52
  end
40
53
 
41
54
  context 'on error' do
42
- let(:error_message) { "oh noes!" }
43
- let(:coordinator) { OpenStruct.new(run: true, quit: true) }
55
+ let(:error) { RuntimeError.new("oh noes!") }
56
+ let(:coordinator) { stub(run: true, quit: true) }
57
+ let(:crash_handler) { stub(handle: nil) }
44
58
 
45
59
  before do
46
60
  cli.instance_variable_set(:@options, {exit_error_format: error_format})
47
- coordinator.stubs(:run).raises(RuntimeError, error_message)
61
+ coordinator.stubs(:run).raises(error)
62
+ MailRoom::CrashHandler.stubs(:new).returns(crash_handler)
48
63
  end
49
64
 
50
65
  context 'json format provided' do
51
66
  let(:error_format) { 'json' }
52
67
 
53
68
  it 'passes onto CrashHandler' do
54
- cli.start
69
+ crash_handler.expects(:handle).with(error, error_format)
55
70
 
56
- expect(MailRoom::CrashHandler).to have_received(:new).with a_hash_including({format: error_format})
71
+ cli.start
57
72
  end
58
73
  end
59
74
  end