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