gitlab-mail_room 0.0.4 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +14 -5
  3. data/.gitlab/issue_templates/Release.md +7 -0
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +3 -2
  6. data/README.md +16 -1
  7. data/lib/mail_room.rb +2 -0
  8. data/lib/mail_room/cli.rb +2 -2
  9. data/lib/mail_room/configuration.rb +11 -1
  10. data/lib/mail_room/connection.rb +7 -179
  11. data/lib/mail_room/coordinator.rb +8 -4
  12. data/lib/mail_room/crash_handler.rb +8 -12
  13. data/lib/mail_room/health_check.rb +60 -0
  14. data/lib/mail_room/imap.rb +8 -0
  15. data/lib/mail_room/imap/connection.rb +200 -0
  16. data/lib/mail_room/imap/message.rb +19 -0
  17. data/lib/mail_room/logger/structured.rb +15 -1
  18. data/lib/mail_room/mailbox.rb +7 -4
  19. data/lib/mail_room/mailbox_watcher.rb +9 -2
  20. data/lib/mail_room/message.rb +16 -0
  21. data/lib/mail_room/version.rb +2 -2
  22. data/mail_room.gemspec +5 -3
  23. data/spec/fixtures/test_config.yml +3 -0
  24. data/spec/lib/arbitration/redis_spec.rb +3 -2
  25. data/spec/lib/cli_spec.rb +30 -15
  26. data/spec/lib/configuration_spec.rb +9 -2
  27. data/spec/lib/coordinator_spec.rb +27 -11
  28. data/spec/lib/crash_handler_spec.rb +10 -9
  29. data/spec/lib/delivery/letter_opener_spec.rb +9 -5
  30. data/spec/lib/delivery/logger_spec.rb +7 -9
  31. data/spec/lib/delivery/postback_spec.rb +11 -27
  32. data/spec/lib/delivery/que_spec.rb +5 -8
  33. data/spec/lib/health_check_spec.rb +57 -0
  34. data/spec/lib/{connection_spec.rb → imap/connection_spec.rb} +13 -17
  35. data/spec/lib/imap/message_spec.rb +36 -0
  36. data/spec/lib/logger/structured_spec.rb +34 -2
  37. data/spec/lib/mailbox_spec.rb +14 -17
  38. data/spec/lib/mailbox_watcher_spec.rb +9 -12
  39. data/spec/lib/message_spec.rb +35 -0
  40. data/spec/spec_helper.rb +1 -1
  41. metadata +45 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3aef2538505e8ae118f1a7ad8fe25d1ced7dff264f74427b0e8a4ebe4b07bec1
4
- data.tar.gz: f765a548cbb934e04a482852501b6c6d66055528649db07828a01315667f8f32
3
+ metadata.gz: ba503479ca9b110e2cf338a688df900d2ae393118c79a47b9e0e8b4737b5bc3c
4
+ data.tar.gz: be44255c80b29ccbad7d7c38d1ba18e5d7e9262041b36645deff6feb5266d70a
5
5
  SHA512:
6
- metadata.gz: 041ee7bec2b17ce6af2de5db39a36403b8c51ea9ba58e0b4bfeb0cf5ca6298cae8ee8e902e8391c0f9200e3dfbf03bbf5831a4e938862ddf813b1229996c6f00
7
- data.tar.gz: 0f67f7e7f883b743f279f1af998f8172d47807592394318b7b1bcac7421ebc9d8400c3bd9c58648208a6dd3253cb3e652faf4b84c53e90e993360e1595bf05f5
6
+ metadata.gz: 8e0e0d8e8dbb37643139a655b5fbe613630ea1b6e99de6e2fd51098fb4da75d381762af72f96fd0c1d4922764f8688a75be2bd660ef6a4b21492b0a2df791a15
7
+ data.tar.gz: 4f02c046002569bfb9d15d730ea2853036cce2e88af27e88a5a586f1526a70869dc1533b5abf601d05ce777a3f3e2620872b27769a49383bb9fc1ef761e47794
data/.gitlab-ci.yml CHANGED
@@ -1,11 +1,16 @@
1
1
  # Cache gems in between builds
2
2
 
3
+ services:
4
+ - redis:latest
5
+
3
6
  .test-template: &test
4
7
  cache:
5
8
  paths:
6
9
  - vendor/ruby
10
+ variables:
11
+ REDIS_URL: redis://redis:6379
7
12
  script:
8
- - bundle exec rspec spec
13
+ - bundle exec rspec spec
9
14
  before_script:
10
15
  - apt update && apt install -y libicu-dev
11
16
  - ruby -v # Print out ruby version for debugging
@@ -14,10 +19,6 @@
14
19
  - gem install bundler --no-document # Bundler is not installed with the image
15
20
  - bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby
16
21
 
17
- rspec-2.4:
18
- image: "ruby:2.4"
19
- <<: *test
20
-
21
22
  rspec-2.5:
22
23
  image: "ruby:2.5"
23
24
  <<: *test
@@ -25,3 +26,11 @@ rspec-2.5:
25
26
  rspec-2.6:
26
27
  image: "ruby:2.6"
27
28
  <<: *test
29
+
30
+ rspec-2.7:
31
+ image: "ruby:2.7"
32
+ <<: *test
33
+
34
+ rspec-3.0:
35
+ image: "ruby:3.0"
36
+ <<: *test
@@ -0,0 +1,7 @@
1
+ # GitLab mail_room release checklist
2
+
3
+ - [ ] create tag in https://gitlab.com/gitlab-org/gitlab-mail_room/
4
+ - [ ] publish gem from this tag to rubygems.org
5
+ - [ ] update https://gitlab.com/gitlab-org/gitlab/-/blob/master/Gemfile to use the new gem version
6
+ - [ ] update gitlab-org/build/CNG to build container images from the new gem (example: https://gitlab.com/gitlab-org/build/CNG/-/merge_requests/451/diffs)
7
+ - [ ] to deploy the new version to gitlab.com, update gitlab-com/gl-infra/k8s-workloads/gitlab-com to pin the new mailroom container image version and assign it the [release managers](https://about.gitlab.com/community/release-managers/) (example: https://gitlab.com/gitlab-com/gl-infra/k8s-workloads/gitlab-com/-/merge_requests/236/diffs)
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.6.0
1
+ 2.7.2
data/.travis.yml CHANGED
@@ -1,9 +1,10 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.3.0
4
- - 2.4
5
3
  - 2.5
6
4
  - 2.6
5
+ - 2.7
6
+ - 3.0
7
+ - truffleruby
7
8
  services:
8
9
  - redis-server
9
10
  script: bundle exec rspec spec
data/README.md CHANGED
@@ -55,6 +55,9 @@ You will also need to install `faraday` or `letter_opener` if you use the `postb
55
55
 
56
56
  ```yaml
57
57
  ---
58
+ :health_check:
59
+ :address: "127.0.0.1"
60
+ :port: 8080
58
61
  :mailboxes:
59
62
  -
60
63
  :email: "user1@gmail.com"
@@ -119,6 +122,18 @@ You will also need to install `faraday` or `letter_opener` if you use the `postb
119
122
  **Note:** If using `delete_after_delivery`, you also probably want to use
120
123
  `expunge_deleted` unless you really know what you're doing.
121
124
 
125
+ ## health_check ##
126
+
127
+ Requires `webrick` gem to be installed.
128
+
129
+ This option enables an HTTP server that listens to a bind address
130
+ defined by `address` and `port`. The following endpoints are supported:
131
+
132
+ * `/liveness`: This returns a 200 status code with `OK` as the body if
133
+ the server is running. Otherwise, it returns a 500 status code.
134
+
135
+ This feature is not included in upstream `mail_room` and is specific to GitLab.
136
+
122
137
  ## delivery_method ##
123
138
 
124
139
  ### postback ###
@@ -263,7 +278,7 @@ it's probably because the content-type is set to Faraday's default, which is `H
263
278
  ## idle_timeout ##
264
279
 
265
280
  By default, the IDLE command will wait for 29 minutes (in order to keep the server connection happy).
266
- If you'd prefer not to wait that long, you can pass `imap_timeout` in seconds for your mailbox configuration.
281
+ If you'd prefer not to wait that long, you can pass `idle_timeout` in seconds for your mailbox configuration.
267
282
 
268
283
  ## Search Command ##
269
284
 
data/lib/mail_room.rb CHANGED
@@ -7,8 +7,10 @@ end
7
7
 
8
8
  require "mail_room/version"
9
9
  require "mail_room/configuration"
10
+ require "mail_room/health_check"
10
11
  require "mail_room/mailbox"
11
12
  require "mail_room/mailbox_watcher"
13
+ require "mail_room/message"
12
14
  require "mail_room/connection"
13
15
  require "mail_room/coordinator"
14
16
  require "mail_room/cli"
data/lib/mail_room/cli.rb CHANGED
@@ -42,7 +42,7 @@ module MailRoom
42
42
  end.parse!(args)
43
43
 
44
44
  self.configuration = Configuration.new(options)
45
- self.coordinator = Coordinator.new(configuration.mailboxes)
45
+ self.coordinator = Coordinator.new(configuration.mailboxes, configuration.health_check)
46
46
  end
47
47
 
48
48
  # Start the coordinator running, sets up signal traps
@@ -57,7 +57,7 @@ module MailRoom
57
57
 
58
58
  coordinator.run
59
59
  rescue Exception => e # not just Errors, but includes lower-level Exceptions
60
- CrashHandler.new(error: e, format: @options[:exit_error_format]).handle
60
+ CrashHandler.new.handle(e, @options[:exit_error_format])
61
61
  exit
62
62
  end
63
63
  end
@@ -4,7 +4,7 @@ module MailRoom
4
4
  # Wraps configuration for a set of individual mailboxes with global config
5
5
  # @author Tony Pitale
6
6
  class Configuration
7
- attr_accessor :mailboxes, :log_path, :quiet
7
+ attr_accessor :mailboxes, :log_path, :quiet, :health_check
8
8
 
9
9
  # Initialize a new configuration of mailboxes
10
10
  def initialize(options={})
@@ -18,6 +18,7 @@ module MailRoom
18
18
  config_file = YAML.load(erb.result)
19
19
 
20
20
  set_mailboxes(config_file[:mailboxes])
21
+ set_health_check(config_file[:health_check])
21
22
  rescue => e
22
23
  raise e unless quiet
23
24
  end
@@ -32,5 +33,14 @@ module MailRoom
32
33
  self.mailboxes << Mailbox.new(attributes)
33
34
  end
34
35
  end
36
+
37
+ # Builds the health checker from YAML configuration
38
+ #
39
+ # @param health_check_config nil or a Hash containing :address and :port
40
+ def set_health_check(health_check_config)
41
+ return unless health_check_config
42
+
43
+ self.health_check = HealthCheck.new(health_check_config)
44
+ end
35
45
  end
36
46
  end
@@ -1,195 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MailRoom
2
4
  class Connection
5
+ attr_reader :mailbox, :new_message_handler
6
+
3
7
  def initialize(mailbox)
4
8
  @mailbox = mailbox
5
-
6
- # log in and set the mailbox
7
- reset
8
- setup
9
9
  end
10
10
 
11
11
  def on_new_message(&block)
12
12
  @new_message_handler = block
13
13
  end
14
14
 
15
- # is the connection logged in?
16
- # @return [Boolean]
17
- def logged_in?
18
- @logged_in
19
- end
20
-
21
- # is the connection blocked idling?
22
- # @return [Boolean]
23
- def idling?
24
- @idling
25
- end
26
-
27
- # is the imap connection closed?
28
- # @return [Boolean]
29
- def disconnected?
30
- @imap.disconnected?
31
- end
32
-
33
- # is the connection ready to idle?
34
- # @return [Boolean]
35
- def ready_to_idle?
36
- logged_in? && !idling?
37
- end
38
-
39
- def quit
40
- stop_idling
41
- reset
42
- end
43
-
44
15
  def wait
45
- begin
46
- # in case we missed any between idles
47
- process_mailbox
48
-
49
- idle
50
-
51
- process_mailbox
52
- rescue Net::IMAP::Error, IOError
53
- @mailbox.logger.warn({ context: @mailbox.context, action: "Disconnected. Resetting..." })
54
- reset
55
- setup
56
- end
57
- end
58
-
59
- private
60
-
61
- def reset
62
- @imap = nil
63
- @logged_in = false
64
- @idling = false
65
- end
66
-
67
- def setup
68
- @mailbox.logger.info({ context: @mailbox.context, action: "Starting TLS session" })
69
- start_tls
70
-
71
- @mailbox.logger.info({ context: @mailbox.context, action: "Logging into mailbox" })
72
- log_in
73
-
74
- @mailbox.logger.info({ context: @mailbox.context, action: "Setting mailbox" })
75
- set_mailbox
76
- end
77
-
78
- # build a net/imap connection to google imap
79
- def imap
80
- @imap ||= Net::IMAP.new(@mailbox.host, :port => @mailbox.port, :ssl => @mailbox.ssl_options)
81
- end
82
-
83
- # start a TLS session
84
- def start_tls
85
- imap.starttls if @mailbox.start_tls
86
- end
87
-
88
- # send the imap login command to google
89
- def log_in
90
- imap.login(@mailbox.email, @mailbox.password)
91
- @logged_in = true
16
+ raise NotImplementedError
92
17
  end
93
18
 
94
- # select the mailbox name we want to use
95
- def set_mailbox
96
- imap.select(@mailbox.name) if logged_in?
97
- end
98
-
99
- # is the response for a new message?
100
- # @param response [Net::IMAP::TaggedResponse] the imap response from idle
101
- # @return [Boolean]
102
- def message_exists?(response)
103
- response.respond_to?(:name) && response.name == 'EXISTS'
104
- end
105
-
106
- # @private
107
- def idle_handler
108
- lambda {|response| imap.idle_done if message_exists?(response)}
109
- end
110
-
111
- # maintain an imap idle connection
112
- def idle
113
- return unless ready_to_idle?
114
-
115
- @mailbox.logger.info({ context: @mailbox.context, action: "Idling" })
116
- @idling = true
117
-
118
- imap.idle(@mailbox.idle_timeout, &idle_handler)
119
- ensure
120
- @idling = false
121
- end
122
-
123
- # trigger the idle to finish and wait for the thread to finish
124
- def stop_idling
125
- return unless idling?
126
-
127
- imap.idle_done
128
-
129
- # idling_thread.join
130
- # self.idling_thread = nil
131
- end
132
-
133
- def process_mailbox
134
- return unless @new_message_handler
135
- @mailbox.logger.info({ context: @mailbox.context, action: "Processing started" })
136
-
137
- msgs = new_messages
138
-
139
- any_deletions = msgs.
140
- # deliver each new message, collect success
141
- map(&@new_message_handler).
142
- # include messages with success
143
- zip(msgs).
144
- # filter failed deliveries, collect message
145
- select(&:first).map(&:last).
146
- # scrub delivered messages
147
- map { |message| scrub(message) }.
148
- any?
149
-
150
- imap.expunge if @mailbox.expunge_deleted && any_deletions
151
- end
152
-
153
- def scrub(message)
154
- if @mailbox.delete_after_delivery
155
- imap.store(message.seqno, "+FLAGS", [Net::IMAP::DELETED])
156
- true
157
- end
158
- end
159
-
160
- # @private
161
- # fetch all messages for the new message ids
162
- def new_messages
163
- # Both of these calls may results in
164
- # imap raising an EOFError, we handle
165
- # this exception in the watcher
166
- messages_for_ids(new_message_ids)
167
- end
168
-
169
- # TODO: label messages?
170
- # @imap.store(id, "+X-GM-LABELS", [label])
171
-
172
- # @private
173
- # search for all new (unseen) message ids
174
- # @return [Array<Integer>] message ids
175
- def new_message_ids
176
- # uid_search still leaves messages UNSEEN
177
- all_unread = @imap.uid_search(@mailbox.search_command)
178
-
179
- to_deliver = all_unread.select { |uid| @mailbox.deliver?(uid) }
180
- @mailbox.logger.info({ context: @mailbox.context, action: "Getting new messages", unread: {count: all_unread.count, ids: all_unread}, to_be_delivered: { count: to_deliver.count, ids: all_unread } })
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<Net::IMAP::FetchData>] 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.uid_fetch(uids, "RFC822")
19
+ def quit
20
+ raise NotImplementedError
193
21
  end
194
22
  end
195
23
  end
@@ -2,13 +2,15 @@ module MailRoom
2
2
  # Coordinate the mailbox watchers
3
3
  # @author Tony Pitale
4
4
  class Coordinator
5
- attr_accessor :watchers, :running
5
+ attr_accessor :watchers, :running, :health_check
6
6
 
7
7
  # build watchers for a set of mailboxes
8
8
  # @params mailboxes [Array<MailRoom::Mailbox>] mailboxes to be watched
9
- def initialize(mailboxes)
9
+ # @params health_check <MailRoom::HealthCheck> health checker to run
10
+ def initialize(mailboxes, health_check = nil)
10
11
  self.watchers = []
11
12
 
13
+ @health_check = health_check
12
14
  mailboxes.each {|box| self.watchers << MailboxWatcher.new(box)}
13
15
  end
14
16
 
@@ -16,10 +18,11 @@ module MailRoom
16
18
 
17
19
  # start each of the watchers to running
18
20
  def run
21
+ health_check&.run
19
22
  watchers.each(&:run)
20
-
23
+
21
24
  self.running = true
22
-
25
+
23
26
  sleep_while_running
24
27
  ensure
25
28
  quit
@@ -27,6 +30,7 @@ module MailRoom
27
30
 
28
31
  # quit each of the watchers when we're done running
29
32
  def quit
33
+ health_check&.quit
30
34
  watchers.each(&:quit)
31
35
  end
32
36
 
@@ -1,30 +1,26 @@
1
+ require 'date'
1
2
 
2
3
  module MailRoom
3
4
  class CrashHandler
5
+ SUPPORTED_FORMATS = %w[json none]
4
6
 
5
- attr_reader :error, :format
6
-
7
- SUPPORTED_FORMATS = %w[json plain]
8
-
9
- def initialize(error:, format:)
10
- @error = error
11
- @format = format
7
+ def initialize(stream=STDOUT)
8
+ @stream = stream
12
9
  end
13
10
 
14
- def handle
11
+ def handle(error, format)
15
12
  if format == 'json'
16
- puts json
13
+ @stream.puts json(error)
17
14
  return
18
15
  end
19
16
 
20
- # 'plain' is equivalent to outputting the error into stdout as-is
21
17
  raise error
22
18
  end
23
19
 
24
20
  private
25
21
 
26
- def json
27
- { time: Time.now, severity: :fatal, message: error.message, backtrace: error.backtrace }.to_json
22
+ def json(error)
23
+ { time: DateTime.now.iso8601(3), severity: :fatal, message: error.message, backtrace: error.backtrace }.to_json
28
24
  end
29
25
  end
30
26
  end