gitlab-mail_room 0.0.4 → 0.0.10
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.
- checksums.yaml +4 -4
- data/.gitlab-ci.yml +14 -5
- data/.gitlab/issue_templates/Release.md +7 -0
- data/.ruby-version +1 -1
- data/.travis.yml +3 -2
- data/README.md +16 -1
- data/lib/mail_room.rb +2 -0
- data/lib/mail_room/cli.rb +2 -2
- data/lib/mail_room/configuration.rb +11 -1
- data/lib/mail_room/connection.rb +7 -179
- data/lib/mail_room/coordinator.rb +8 -4
- data/lib/mail_room/crash_handler.rb +8 -12
- data/lib/mail_room/health_check.rb +60 -0
- data/lib/mail_room/imap.rb +8 -0
- data/lib/mail_room/imap/connection.rb +200 -0
- data/lib/mail_room/imap/message.rb +19 -0
- data/lib/mail_room/logger/structured.rb +15 -1
- data/lib/mail_room/mailbox.rb +7 -4
- data/lib/mail_room/mailbox_watcher.rb +9 -2
- data/lib/mail_room/message.rb +16 -0
- data/lib/mail_room/version.rb +2 -2
- data/mail_room.gemspec +5 -3
- data/spec/fixtures/test_config.yml +3 -0
- data/spec/lib/arbitration/redis_spec.rb +3 -2
- data/spec/lib/cli_spec.rb +30 -15
- data/spec/lib/configuration_spec.rb +9 -2
- data/spec/lib/coordinator_spec.rb +27 -11
- data/spec/lib/crash_handler_spec.rb +10 -9
- data/spec/lib/delivery/letter_opener_spec.rb +9 -5
- data/spec/lib/delivery/logger_spec.rb +7 -9
- data/spec/lib/delivery/postback_spec.rb +11 -27
- data/spec/lib/delivery/que_spec.rb +5 -8
- data/spec/lib/health_check_spec.rb +57 -0
- data/spec/lib/{connection_spec.rb → imap/connection_spec.rb} +13 -17
- data/spec/lib/imap/message_spec.rb +36 -0
- data/spec/lib/logger/structured_spec.rb +34 -2
- data/spec/lib/mailbox_spec.rb +14 -17
- data/spec/lib/mailbox_watcher_spec.rb +9 -12
- data/spec/lib/message_spec.rb +35 -0
- data/spec/spec_helper.rb +1 -1
- metadata +45 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba503479ca9b110e2cf338a688df900d2ae393118c79a47b9e0e8b4737b5bc3c
|
4
|
+
data.tar.gz: be44255c80b29ccbad7d7c38d1ba18e5d7e9262041b36645deff6feb5266d70a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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.
|
1
|
+
2.7.2
|
data/.travis.yml
CHANGED
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 `
|
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(
|
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
|
data/lib/mail_room/connection.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
95
|
-
|
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
|
-
|
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
|
-
|
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:
|
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
|