gitlab-mail_room 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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