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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.gitlab-ci.yml +27 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +125 -0
- data/CODE_OF_CONDUCT.md +24 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +361 -0
- data/Rakefile +6 -0
- data/bin/mail_room +5 -0
- data/lib/mail_room.rb +16 -0
- data/lib/mail_room/arbitration.rb +16 -0
- data/lib/mail_room/arbitration/noop.rb +18 -0
- data/lib/mail_room/arbitration/redis.rb +58 -0
- data/lib/mail_room/cli.rb +62 -0
- data/lib/mail_room/configuration.rb +36 -0
- data/lib/mail_room/connection.rb +195 -0
- data/lib/mail_room/coordinator.rb +41 -0
- data/lib/mail_room/crash_handler.rb +29 -0
- data/lib/mail_room/delivery.rb +24 -0
- data/lib/mail_room/delivery/letter_opener.rb +34 -0
- data/lib/mail_room/delivery/logger.rb +37 -0
- data/lib/mail_room/delivery/noop.rb +22 -0
- data/lib/mail_room/delivery/postback.rb +72 -0
- data/lib/mail_room/delivery/que.rb +63 -0
- data/lib/mail_room/delivery/sidekiq.rb +88 -0
- data/lib/mail_room/logger/structured.rb +21 -0
- data/lib/mail_room/mailbox.rb +182 -0
- data/lib/mail_room/mailbox_watcher.rb +62 -0
- data/lib/mail_room/version.rb +4 -0
- data/logfile.log +1 -0
- data/mail_room.gemspec +34 -0
- data/spec/fixtures/test_config.yml +16 -0
- data/spec/lib/arbitration/redis_spec.rb +146 -0
- data/spec/lib/cli_spec.rb +61 -0
- data/spec/lib/configuration_spec.rb +29 -0
- data/spec/lib/connection_spec.rb +65 -0
- data/spec/lib/coordinator_spec.rb +61 -0
- data/spec/lib/crash_handler_spec.rb +41 -0
- data/spec/lib/delivery/letter_opener_spec.rb +29 -0
- data/spec/lib/delivery/logger_spec.rb +46 -0
- data/spec/lib/delivery/postback_spec.rb +107 -0
- data/spec/lib/delivery/que_spec.rb +45 -0
- data/spec/lib/delivery/sidekiq_spec.rb +76 -0
- data/spec/lib/logger/structured_spec.rb +55 -0
- data/spec/lib/mailbox_spec.rb +132 -0
- data/spec/lib/mailbox_watcher_spec.rb +64 -0
- data/spec/spec_helper.rb +32 -0
- 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
|
data/logfile.log
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Logfile created on 2019-09-26 08:59:35 -0500 by logger.rb/66358
|
data/mail_room.gemspec
ADDED
@@ -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
|