gitlab-mail_room 0.0.2

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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.gitlab-ci.yml +27 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +10 -0
  6. data/CHANGELOG.md +125 -0
  7. data/CODE_OF_CONDUCT.md +24 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +361 -0
  11. data/Rakefile +6 -0
  12. data/bin/mail_room +5 -0
  13. data/lib/mail_room.rb +16 -0
  14. data/lib/mail_room/arbitration.rb +16 -0
  15. data/lib/mail_room/arbitration/noop.rb +18 -0
  16. data/lib/mail_room/arbitration/redis.rb +58 -0
  17. data/lib/mail_room/cli.rb +62 -0
  18. data/lib/mail_room/configuration.rb +36 -0
  19. data/lib/mail_room/connection.rb +195 -0
  20. data/lib/mail_room/coordinator.rb +41 -0
  21. data/lib/mail_room/crash_handler.rb +29 -0
  22. data/lib/mail_room/delivery.rb +24 -0
  23. data/lib/mail_room/delivery/letter_opener.rb +34 -0
  24. data/lib/mail_room/delivery/logger.rb +37 -0
  25. data/lib/mail_room/delivery/noop.rb +22 -0
  26. data/lib/mail_room/delivery/postback.rb +72 -0
  27. data/lib/mail_room/delivery/que.rb +63 -0
  28. data/lib/mail_room/delivery/sidekiq.rb +88 -0
  29. data/lib/mail_room/logger/structured.rb +21 -0
  30. data/lib/mail_room/mailbox.rb +182 -0
  31. data/lib/mail_room/mailbox_watcher.rb +62 -0
  32. data/lib/mail_room/version.rb +4 -0
  33. data/logfile.log +1 -0
  34. data/mail_room.gemspec +34 -0
  35. data/spec/fixtures/test_config.yml +16 -0
  36. data/spec/lib/arbitration/redis_spec.rb +146 -0
  37. data/spec/lib/cli_spec.rb +61 -0
  38. data/spec/lib/configuration_spec.rb +29 -0
  39. data/spec/lib/connection_spec.rb +65 -0
  40. data/spec/lib/coordinator_spec.rb +61 -0
  41. data/spec/lib/crash_handler_spec.rb +41 -0
  42. data/spec/lib/delivery/letter_opener_spec.rb +29 -0
  43. data/spec/lib/delivery/logger_spec.rb +46 -0
  44. data/spec/lib/delivery/postback_spec.rb +107 -0
  45. data/spec/lib/delivery/que_spec.rb +45 -0
  46. data/spec/lib/delivery/sidekiq_spec.rb +76 -0
  47. data/spec/lib/logger/structured_spec.rb +55 -0
  48. data/spec/lib/mailbox_spec.rb +132 -0
  49. data/spec/lib/mailbox_watcher_spec.rb +64 -0
  50. data/spec/spec_helper.rb +32 -0
  51. metadata +277 -0
@@ -0,0 +1,62 @@
1
+ module MailRoom
2
+ # TODO: split up between processing and idling?
3
+
4
+ # Watch a Mailbox
5
+ # @author Tony Pitale
6
+ class MailboxWatcher
7
+ attr_accessor :watching_thread
8
+
9
+ # Watch a new mailbox
10
+ # @param mailbox [MailRoom::Mailbox] the mailbox to watch
11
+ def initialize(mailbox)
12
+ @mailbox = mailbox
13
+
14
+ @running = false
15
+ @connection = nil
16
+ end
17
+
18
+ # are we running?
19
+ # @return [Boolean]
20
+ def running?
21
+ @running
22
+ end
23
+
24
+ # run the mailbox watcher
25
+ def run
26
+ @mailbox.logger.info({ context: @mailbox.context, action: "Setting up watcher" })
27
+ @running = true
28
+
29
+ connection.on_new_message do |message|
30
+ @mailbox.deliver(message)
31
+ end
32
+
33
+ self.watching_thread = Thread.start do
34
+ while(running?) do
35
+ connection.wait
36
+ end
37
+ end
38
+
39
+ watching_thread.abort_on_exception = true
40
+ end
41
+
42
+ # stop running, cleanup connection
43
+ def quit
44
+ @mailbox.logger.info({ context: @mailbox.context, action: "Quitting connection..." })
45
+ @running = false
46
+
47
+ if @connection
48
+ @connection.quit
49
+ @connection = nil
50
+ end
51
+
52
+ if self.watching_thread
53
+ self.watching_thread.join
54
+ end
55
+ end
56
+
57
+ private
58
+ def connection
59
+ @connection ||= Connection.new(@mailbox)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,4 @@
1
+ module MailRoom
2
+ # Current version of MailRoom gem
3
+ VERSION = "0.0.2"
4
+ end
@@ -0,0 +1 @@
1
+ # Logfile created on 2019-09-26 08:59:35 -0500 by logger.rb/66358
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mail_room/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "gitlab-mail_room"
8
+ gem.version = MailRoom::VERSION
9
+ gem.authors = ["Tony Pitale"]
10
+ gem.email = ["tpitale@gmail.com"]
11
+ gem.description = %q{mail_room will proxy email (gmail) from IMAP to a delivery method}
12
+ gem.summary = %q{mail_room will proxy email (gmail) from IMAP to a callback URL, logger, or letter_opener}
13
+ gem.homepage = "http://github.com/tpitale/mail_room"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_development_dependency "rake"
21
+ gem.add_development_dependency "rspec"
22
+ gem.add_development_dependency "mocha"
23
+ gem.add_development_dependency "bourne"
24
+ gem.add_development_dependency "simplecov"
25
+
26
+ # for testing delivery methods
27
+ gem.add_development_dependency "faraday"
28
+ gem.add_development_dependency "mail"
29
+ gem.add_development_dependency "letter_opener"
30
+ gem.add_development_dependency "redis", "~> 3.3.1"
31
+ gem.add_development_dependency "redis-namespace"
32
+ gem.add_development_dependency "pg"
33
+ gem.add_development_dependency "charlock_holmes"
34
+ end
@@ -0,0 +1,16 @@
1
+ ---
2
+ :mailboxes:
3
+ -
4
+ :email: "user1@gmail.com"
5
+ :password: "password"
6
+ :name: "inbox"
7
+ :delivery_url: "http://localhost:3000/inbox"
8
+ :delivery_token: "abcdefg"
9
+ :logger:
10
+ :log_path: "logfile.log"
11
+ -
12
+ :email: "user2@gmail.com"
13
+ :password: "password"
14
+ :name: "inbox"
15
+ :delivery_url: "http://localhost:3000/inbox"
16
+ :delivery_token: "abcdefg"
@@ -0,0 +1,146 @@
1
+ require 'spec_helper'
2
+ require 'mail_room/arbitration/redis'
3
+
4
+ describe MailRoom::Arbitration::Redis do
5
+ let(:mailbox) {
6
+ build_mailbox(
7
+ arbitration_options: {
8
+ namespace: "mail_room"
9
+ }
10
+ )
11
+ }
12
+ let(:options) { described_class::Options.new(mailbox) }
13
+ subject { described_class.new(options) }
14
+
15
+ # Private, but we don't care.
16
+ let(:redis) { subject.send(:client) }
17
+
18
+ describe '#deliver?' do
19
+ context "when called the first time" do
20
+ after do
21
+ redis.del("delivered:123")
22
+ end
23
+
24
+ it "returns true" do
25
+ expect(subject.deliver?(123)).to be_truthy
26
+ end
27
+
28
+ it "increments the delivered flag" do
29
+ subject.deliver?(123)
30
+
31
+ expect(redis.get("delivered:123")).to eq("1")
32
+ end
33
+
34
+ it "sets an expiration on the delivered flag" do
35
+ subject.deliver?(123)
36
+
37
+ expect(redis.ttl("delivered:123")).to be > 0
38
+ end
39
+ end
40
+
41
+ context "when called the second time" do
42
+ before do
43
+ #Short expiration, 1 second, for testing
44
+ subject.deliver?(123, 1)
45
+ end
46
+
47
+ after do
48
+ redis.del("delivered:123")
49
+ end
50
+
51
+ it "returns false" do
52
+ expect(subject.deliver?(123, 1)).to be_falsey
53
+ end
54
+
55
+ it "after expiration returns true" do
56
+ # Fails locally because fakeredis returns 0, not false
57
+ expect(subject.deliver?(123, 1)).to be_falsey
58
+ sleep(redis.ttl("delivered:123")+1)
59
+ expect(subject.deliver?(123, 1)).to be_truthy
60
+ end
61
+ end
62
+
63
+ context "when called for another uid" do
64
+ before do
65
+ subject.deliver?(123)
66
+ end
67
+
68
+ after do
69
+ redis.del("delivered:123")
70
+ redis.del("delivered:124")
71
+ end
72
+
73
+ it "returns true" do
74
+ expect(subject.deliver?(124)).to be_truthy
75
+ end
76
+ end
77
+ end
78
+
79
+ context 'redis client connection params' do
80
+ context 'when only url is present' do
81
+ let(:redis_url) { "redis://localhost:6379" }
82
+ let(:mailbox) {
83
+ build_mailbox(
84
+ arbitration_options: {
85
+ redis_url: redis_url
86
+ }
87
+ )
88
+ }
89
+
90
+ after do
91
+ redis.del("delivered:123")
92
+ end
93
+
94
+ it 'client has same specified url' do
95
+ subject.deliver?(123)
96
+
97
+ expect(redis.client.options[:url]).to eq redis_url
98
+ end
99
+
100
+ it 'client is a instance of Redis class' do
101
+ expect(redis).to be_a Redis
102
+ end
103
+ end
104
+
105
+ context 'when namespace is present' do
106
+ let(:namespace) { 'mail_room' }
107
+ let(:mailbox) {
108
+ build_mailbox(
109
+ arbitration_options: {
110
+ namespace: namespace
111
+ }
112
+ )
113
+ }
114
+
115
+ it 'client has same specified namespace' do
116
+ expect(redis.namespace).to eq(namespace)
117
+ end
118
+
119
+ it 'client is a instance of RedisNamespace class' do
120
+ expect(redis).to be_a ::Redis::Namespace
121
+ end
122
+ end
123
+
124
+ context 'when sentinel is present' do
125
+ let(:redis_url) { 'redis://:mypassword@sentinel-master:6379' }
126
+ let(:sentinels) { [{ host: '10.0.0.1', port: '26379' }] }
127
+ let(:mailbox) {
128
+ build_mailbox(
129
+ arbitration_options: {
130
+ redis_url: redis_url,
131
+ sentinels: sentinels
132
+ }
133
+ )
134
+ }
135
+
136
+ before { ::Redis::Client::Connector::Sentinel.any_instance.stubs(:resolve).returns(sentinels) }
137
+
138
+ it 'client has same specified sentinel params' do
139
+ expect(redis.client.instance_variable_get(:@connector)).to be_a Redis::Client::Connector::Sentinel
140
+ expect(redis.client.options[:host]).to eq('sentinel-master')
141
+ expect(redis.client.options[:password]).to eq('mypassword')
142
+ expect(redis.client.options[:sentinels]).to eq(sentinels)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe MailRoom::CLI do
4
+ let(:config_path) {File.expand_path('../fixtures/test_config.yml', File.dirname(__FILE__))}
5
+ let!(:configuration) {MailRoom::Configuration.new({:config_path => config_path})}
6
+ let(:coordinator) {stub(:run => true, :quit => true)}
7
+
8
+ describe '.new' do
9
+ let(:args) {["-c", "a path"]}
10
+
11
+ before :each do
12
+ MailRoom::Configuration.stubs(:new).returns(configuration)
13
+ MailRoom::Coordinator.stubs(:new).returns(coordinator)
14
+ end
15
+
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'})
19
+ end
20
+
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)
24
+ end
25
+ end
26
+
27
+ describe '#start' do
28
+ let(:cli) {MailRoom::CLI.new([])}
29
+
30
+ before :each do
31
+ cli.configuration = configuration
32
+ cli.coordinator = coordinator
33
+ end
34
+
35
+ it 'starts running the coordinator' do
36
+ cli.start
37
+
38
+ expect(coordinator).to have_received(:run)
39
+ end
40
+
41
+ context 'on error' do
42
+ let(:error_message) { "oh noes!" }
43
+ let(:coordinator) { OpenStruct.new(run: true, quit: true) }
44
+
45
+ before do
46
+ cli.instance_variable_set(:@options, {exit_error_format: error_format})
47
+ coordinator.stubs(:run).raises(RuntimeError, error_message)
48
+ end
49
+
50
+ context 'json format provided' do
51
+ let(:error_format) { 'json' }
52
+
53
+ it 'passes onto CrashHandler' do
54
+ cli.start
55
+
56
+ expect(MailRoom::CrashHandler).to have_received(:new).with a_hash_including({format: error_format})
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe MailRoom::Configuration do
4
+ let(:config_path) {File.expand_path('../fixtures/test_config.yml', File.dirname(__FILE__))}
5
+
6
+ describe 'set_mailboxes' do
7
+ context 'with config_path' do
8
+ let(:configuration) { MailRoom::Configuration.new(:config_path => config_path) }
9
+
10
+ it 'parses yaml into mailbox objects' do
11
+ MailRoom::Mailbox.stubs(:new).returns('mailbox1', 'mailbox2')
12
+
13
+ expect(configuration.mailboxes).to eq(['mailbox1', 'mailbox2'])
14
+ end
15
+ end
16
+
17
+ context 'without config_path' do
18
+ let(:configuration) { MailRoom::Configuration.new }
19
+
20
+ it 'sets mailboxes to an empty set' do
21
+ MailRoom::Mailbox.stubs(:new)
22
+
23
+ expect(configuration.mailboxes).to eq([])
24
+
25
+ expect(MailRoom::Mailbox).to have_received(:new).never
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ describe MailRoom::Connection do
4
+ let(:imap) {stub}
5
+ let(:mailbox) {build_mailbox(delete_after_delivery: true, expunge_deleted: true)}
6
+
7
+ before :each do
8
+ Net::IMAP.stubs(:new).returns(imap)
9
+ end
10
+
11
+ context "with imap set up" do
12
+ let(:connection) {MailRoom::Connection.new(mailbox)}
13
+
14
+ before :each do
15
+ imap.stubs(:starttls)
16
+ imap.stubs(:login)
17
+ imap.stubs(:select)
18
+ end
19
+
20
+ it "is logged in" do
21
+ expect(connection.logged_in?).to eq(true)
22
+ end
23
+
24
+ it "is not idling" do
25
+ expect(connection.idling?).to eq(false)
26
+ end
27
+
28
+ it "is not disconnected" do
29
+ imap.stubs(:disconnected?).returns(false)
30
+
31
+ expect(connection.disconnected?).to eq(false)
32
+ end
33
+
34
+ it "is ready to idle" do
35
+ expect(connection.ready_to_idle?).to eq(true)
36
+ end
37
+
38
+ it "waits for a message to process" do
39
+ new_message = 'a message'
40
+ new_message.stubs(:seqno).returns(8)
41
+
42
+ connection.on_new_message do |message|
43
+ expect(message).to eq(new_message)
44
+ true
45
+ end
46
+
47
+ mailbox.stubs(:deliver?).returns(true)
48
+
49
+ imap.stubs(:idle)
50
+ imap.stubs(:uid_search).returns([]).then.returns([1])
51
+ imap.stubs(:uid_fetch).returns([new_message])
52
+ imap.stubs(:store)
53
+ imap.stubs(:expunge)
54
+
55
+ connection.wait
56
+
57
+ expect(imap).to have_received(:idle)
58
+ expect(imap).to have_received(:uid_search).with(mailbox.search_command).twice
59
+ expect(imap).to have_received(:uid_fetch).with([1], "RFC822")
60
+ expect(mailbox).to have_received(:deliver?).with(1)
61
+ expect(imap).to have_received(:store).with(8, "+FLAGS", [Net::IMAP::DELETED])
62
+ expect(imap).to have_received(:expunge).once
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe MailRoom::Coordinator do
4
+ describe '#initialize' do
5
+ it 'builds a watcher for each mailbox' do
6
+ MailRoom::MailboxWatcher.stubs(:new).returns('watcher1', 'watcher2')
7
+
8
+ coordinator = MailRoom::Coordinator.new(['mailbox1', 'mailbox2'])
9
+
10
+ expect(coordinator.watchers).to eq(['watcher1', 'watcher2'])
11
+
12
+ expect(MailRoom::MailboxWatcher).to have_received(:new).with('mailbox1')
13
+ expect(MailRoom::MailboxWatcher).to have_received(:new).with('mailbox2')
14
+ end
15
+
16
+ it 'makes no watchers when mailboxes is empty' do
17
+ coordinator = MailRoom::Coordinator.new([])
18
+ expect(coordinator.watchers).to eq([])
19
+ end
20
+ end
21
+
22
+ describe '#run' do
23
+ it 'runs each watcher' do
24
+ watcher = stub
25
+ watcher.stubs(:run)
26
+ watcher.stubs(:quit)
27
+ MailRoom::MailboxWatcher.stubs(:new).returns(watcher)
28
+ coordinator = MailRoom::Coordinator.new(['mailbox1'])
29
+ coordinator.stubs(:sleep_while_running)
30
+ coordinator.run
31
+ expect(watcher).to have_received(:run)
32
+ expect(watcher).to have_received(:quit)
33
+ end
34
+
35
+ it 'should go to sleep after running watchers' do
36
+ coordinator = MailRoom::Coordinator.new([])
37
+ coordinator.stubs(:running=)
38
+ coordinator.stubs(:running?).returns(false)
39
+ coordinator.run
40
+ expect(coordinator).to have_received(:running=).with(true)
41
+ expect(coordinator).to have_received(:running?)
42
+ end
43
+
44
+ it 'should set attribute running to true' do
45
+ coordinator = MailRoom::Coordinator.new([])
46
+ coordinator.stubs(:sleep_while_running)
47
+ coordinator.run
48
+ expect(coordinator.running).to eq(true)
49
+ end
50
+ end
51
+
52
+ describe '#quit' do
53
+ it 'quits each watcher' do
54
+ watcher = stub(:quit)
55
+ MailRoom::MailboxWatcher.stubs(:new).returns(watcher)
56
+ coordinator = MailRoom::Coordinator.new(['mailbox1'])
57
+ coordinator.quit
58
+ expect(watcher).to have_received(:quit)
59
+ end
60
+ end
61
+ end